Memilih container yang tepat menentukan apakah solusi berjalan dalam O(1), O(log n), atau O(n) per operasi. STL memiliki belasan container dengan profil operasi berbeda — vector cepat untuk akses acak tetapi lambat untuk insert tengah; unordered_map cepat untuk lookup rata-rata tetapi tidak mendukung lower_bound. Pada Chapter ini kita akan menyusun tabel kompleksitas lengkap untuk semua container STL dan memvalidasinya dengan benchmark sederhana. Tabel ini adalah cheat sheet yang akan dipakai berulang-ulang sepanjang sisa course.

Tabel Kompleksitas Lengkap #

Container Sequence #

Operasivectordequelistforward_listarray
at(i) / operator[]O(1)O(1)O(n)O(n)O(1)
front() / back()O(1)O(1)O(1)front O(1)O(1)
push_backO(1) amort.O(1)O(1)——
push_frontO(n)O(1)O(1)O(1)—
pop_backO(1)O(1)O(1)——
pop_frontO(n)O(1)O(1)O(1)—
insert(it)O(n)O(n)O(1)O(1) after it—
erase(it)O(n)O(n)O(1)O(1) after it—
size()O(1)O(1)O(1)O(n) (distance)O(1)
Iterasi terurut?TidakTidakTidakTidakTidak

Container Associative (Ordered) #

Operasiset/mapmultiset/multimap
find(k)O(log n)O(log n)
insert(k) / insert({k,v})O(log n)O(log n)
erase(k)O(log n)O(log n + count)
count(k)O(log n) (0 atau 1)O(log n + count)
lower_bound(k) / upper_bound(k)O(log n)O(log n)
begin() / rbegin() (sorted)O(1)O(1)
Iterasi terurut?Ya, ascending by keyYa

Container Unordered #

Operasiunordered_set/unordered_map
find(k)O(1) avg, O(n) worst
insert(k)O(1) avg, O(n) worst (rehash)
erase(k)O(1) avg
count(k)O(1) avg
lower_bound/upper_boundTidak tersedia
Iterasi terurut?Tidak
RehashO(n), terjadi saat load factor melewati ambang

Container Adapter #

Operasistackqueuepriority_queue
pushO(1)O(1)O(log n)
popO(1)O(1)O(log n)
top/front/backO(1)O(1)O(1)
Akses elemen acakTidakTidakTidak
FindTidakTidakTidak

Mekanisme: Mengapa Kompleksitas Berbeda #

  • Array kontigu (vector, array): elemen disimpan berdampingan. Akses acak = pointer arithmetic O(1). Tetapi insert tengah butuh menggeser elemen setelahnya → O(n).
  • Doubly linked list (list): setiap node menyimpan pointer ke predecessor dan successor. Insert/erase pada iterator yang sudah dimiliki hanya butuh memperbarui 4 pointer → O(1). Tetapi akses acak butuh traversal dari head → O(n).
  • Red-Black Tree (set/map): pohon biner seimbang dengan tinggi O(log n). Setiap operasi menuruni pohon dari root ke daun → O(log n).
  • Hash table (unordered_*): hash function menentukan bucket. Lookup rata-rata O(1) bila distribusi hash merata; O(n) bila semua key jatuh di satu bucket.

Kode: Benchmark Sederhana #

cpp
#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
#include <algorithm>
#include <chrono>

void demo_complexity() {
    const int N = 100000;
    std::vector<int> v;
    v.reserve(N);
    for (int i = 0; i < N; ++i) v.push_back(i);

    // Linear scan O(n)
    auto t1 = std::chrono::high_resolution_clock::now();
    volatile long long s = 0;
    for (int x : v) s += x;
    auto t2 = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::micro> d1 = t2 - t1;

    // Binary search O(log n)
    auto t3 = std::chrono::high_resolution_clock::now();
    bool found = std::binary_search(v.begin(), v.end(), N - 1);
    auto t4 = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::micro> d2 = t4 - t3;

    // Hash lookup O(1) avg
    std::unordered_set<int> us(v.begin(), v.end());
    auto t5 = std::chrono::high_resolution_clock::now();
    bool f2 = us.count(N - 1);
    auto t6 = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::micro> d3 = t6 - t5;

    std::cout << "Linear scan O(n)     : " << d1.count() << " us (sum=" << s << ")\n";
    std::cout << "Binary search O(log n): " << d2.count() << " us (found=" << found << ")\n";
    std::cout << "Hash lookup O(1) avg : " << d3.count() << " us (found=" << f2 << ")\n";
}

int main() {
    demo_complexity();

    // Demo: push_back vs push_front antara deque dan vector
    const int M = 100000;
    {
        std::vector<int> v;
        auto t1 = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < M; ++i) v.push_back(i);
        auto t2 = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double, std::micro> d = t2 - t1;
        std::cout << "vector push_back " << M << "x: " << d.count() << " us\n";
    }
    {
        std::deque<int> d;
        auto t1 = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < M; ++i) d.push_front(i);
        auto t2 = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double, std::micro> dd = t2 - t1;
        std::cout << "deque push_front " << M << "x: " << dd.count() << " us\n";
    }

    return 0;
}

Output yang Dihasilkan (running lokal):

text
Linear scan O(n)     : 206.297 us (sum=4999950000)
Binary search O(log n): 0.127 us (found=1)
Hash lookup O(1) avg : 0.095 us (found=1)
vector push_back 100000x: 1001.94 us
deque push_front 100000x: 53.678 us

Bedah Kode Baris-per-Baris #

  1. Linear scan O(n): 100000 elemen, masing-masing diakses satu kali. Waktu ~206 µs → ~2 ns per elemen, sesuai ekspektasi cache-friendly iteration vector.
  2. Binary search O(log n): 17 langkah untuk N = 100000 (log₂ 100000 ≈ 17). Waktu ~0.13 µs — sangat cepat, jauh lebih kecil dari linear scan.
  3. Hash lookup O(1) avg: satu hash + satu bucket lookup. Waktu ~0.10 µs, sebanding dengan binary search. Perhatikan bahwa konstanta hash bisa lebih besar dari binary search untuk data kecil; hash unggul saat data besar.
  4. vector::push_back 100000×: ~1000 µs. Mahal karena realokasi terjadi beberapa kali (kapasitas tumbuh 1 → 2 → 4 → …). Setelah reserve(N), push_back menjadi ~200 µs (lima kali lebih cepat) — alokasi di awal menghemat realokasi.
  5. deque::push_front 100000×: ~54 µs. deque menambah chunk baru di depan saat chunk depan penuh — operasi O(1) per push. Lebih cepat dari vector::push_back tanpa reserve karena tidak ada realokasi penuh.

Cheat Sheet Pemilihan Container #

SkenarioPilihan defaultAlternatif
Array dinamis dengan akses acakvectordeque bila butuh push_front
Stack LIFOstd::stack (atas vector)—
Queue FIFOstd::queue (atas deque)—
Priority queuestd::priority_queuestd::set bila butuh decrease-key
Set terurutstd::setunordered_set bila tidak butuh urut
Map terurutstd::mapunordered_map bila tidak butuh urut
Multi-key sortstd::multisetstd::vector + std::sort
Lookup frekuensi tinggiunordered_set/unordered_mapset/map bila butuh lower_bound
Buffer dua arahstd::deque—
Sering insert/erase tengah dengan iterator stabilstd::liststd::vector bila ukuran kecil (cache)

Ringkasan #

  • Lima container sequence + empat associative + empat unordered + tiga adapter = profile operasi berbeda-beda.
  • vector adalah default — cepat untuk akses acak dan iterasi; push_back O(1) amortisasi.
  • list/forward_list memberi insert/erase O(1) pada iterator, tetapi akses acak O(n) dan tidak cache-friendly.
  • Container associative O(log n) per operasi; unordered O(1) rata-rata.
  • Selalu reserve(N) bila ukuran vector diketahui untuk menghilangkan realokasi.
  • Tabel kompleksitas ini adalah cheat sheet — simpan dan rujuk saat memilih container untuk soal DSA.