Memilih struktur data yang tepat adalah keputusan paling berpengaruh terhadap performa solusi. Banyak pemula mengganti std::vector dengan std::list tanpa alasan, lalu heran saat kode justru melambat. Sebaliknya, memakai std::map untuk pencarian sementara std::unordered_map lebih cepat adalah jebakan klasik. Pada Chapter ini kita akan membangun tabel keputusan berdasarkan operasi yang dominan di soal, lalu membahas kasus umum dengan kode pembanding.

Kerangka: Operasi Apa yang Dominan? #

Aturan #

Sebelum memilih struktur data, tanyakan: operasi mana yang paling sering dilakukan? Berikut daftar operasi dan struktur data yang cocok:

Operasi DominanStruktur Data TerbaikCatatan
Akses acak indeksstd::vector / std::arrayO(1)
Insert/delete di ujungstd::vectorO(1) amortized
Insert/delete di depanstd::dequeO(1)
Insert/delete di tengahstd::list (jarang) / skip listO(1) bila iterator; O(n) cari
Pencarian key exactstd::unordered_map / std::unordered_setO(1) rata-rata
Pencarian dengan urutstd::map / std::set (BST)O(log n), traversal terurut
Range query statisPrefix sum / sparse tableO(1) setelah precompute
Range query dinamisSegment tree / Fenwick treeO(log n) update + query
Max/min dinamisPriority queue / heapO(log n) push/pop, O(1) top
Insert + max/minHeapO(log n) insert, O(1) max/min
Substring searchSuffix array / KMP / ZO(n) preprocessing

Mekanisme: Hitung Operasi #

Untuk n = 10⁵ operasi:

  • std::vector::push_back O(1) amortized → total 10⁵ × 1 = 10⁵ ops. Aman.
  • std::vector::insert(begin, ...) O(n) → total 10⁵ × 10⁵ = 10¹⁰ ops. TLE.
  • std::set::insert O(log n) → total 10⁵ × 17 = 1,7 × 10⁶ ops. Aman.
  • std::unordered_set::insert O(1) rata-rata → total 10⁵ ops. Aman dan lebih cepat dari std::set.

Pembanding: Vector vs List #

Aturan #

Banyak literatur lama menyatakan linked list lebih cepat untuk insert/delete di tengah. Realitas pada hardware modern: cache locality membuat std::vector jauh lebih cepat dalam hampir semua kasus kecuali benar-benar banyak insert/delete di tengah dengan iterator sudah ada.

Kode: Benchmark Vector vs List #

cpp
#include <iostream>
#include <vector>
#include <list>
#include <chrono>
#include <random>

int main() {
    const int N = 100000;
    std::mt19937 rng(42);
    std::uniform_int_distribution<int> dist(1, 1'000'000);

    // Skenario: insert 100k elemen ke depan
    auto t1 = std::chrono::high_resolution_clock::now();
    std::vector<int> v;
    for (int i = 0; i < N; ++i) {
        v.push_back(dist(rng));
    }
    auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << "vector push_back: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms\n";

    // Iterasi semua elemen
    long long sum_v = 0;
    t1 = std::chrono::high_resolution_clock::now();
    for (int x : v) sum_v += x;
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "vector iteration: "
              << std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count()
              << " us, sum=" << sum_v << '\n';

    // List
    t1 = std::chrono::high_resolution_clock::now();
    std::list<int> l;
    for (int i = 0; i < N; ++i) {
        l.push_back(dist(rng));
    }
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "list push_back: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms\n";

    long long sum_l = 0;
    t1 = std::chrono::high_resolution_clock::now();
    for (int x : l) sum_l += x;
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "list iteration: "
              << std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count()
              << " us, sum=" << sum_l << '\n';
    return 0;
}

Output yang Dihasilkan (sample, bervariasi per mesin):

text
vector push_back: 0 ms
vector iteration: 32 us, sum=49970910913
list push_back: 2 ms
list iteration: 277 us, sum=50073456928
  1. Baris v.push_back(dist(rng));: vector menyimpan elemen berurutan dalam memori. push_back amortized O(1).
  2. Baris for (int x : v) sum_v += x;: iterasi sequential — sangat cepat karena cache locality (CPU prefetching blok memori berurutan).
  3. Baris l.push_back(dist(rng));: linked list mengalokasikan node terpisah di heap. Setiap push_back adalah alokasi new — mahal.
  4. Baris for (int x : l) sum_l += x;: iterasi linked list mengikuti pointer next — cache miss setiap kali, jauh lebih lambat.
  5. Hasil pengamatan: vector ~3× lebih cepat untuk push_back dan ~8× lebih cepat untuk iterasi. Pola ini konsisten pada hardware modern.

Pembanding: unordered_map vs map #

Aturan #

std::map (BST merah-hitam) menjamin O(log n) untuk semua operasi. std::unordered_map (hash table) menjanjikan O(1) rata-rata tetapi O(n) terburuk. Untuk soal kompetisi dengan data random, unordered_map biasanya lebih cepat.

Kode: Benchmark unordered_map vs map #

cpp
#include <iostream>
#include <unordered_map>
#include <map>
#include <chrono>
#include <random>

int main() {
    const int N = 1000000;
    std::mt19937 rng(42);
    std::uniform_int_distribution<int> dist(1, N * 10);

    // unordered_map insert
    auto t1 = std::chrono::high_resolution_clock::now();
    std::unordered_map<int,int> um;
    for (int i = 0; i < N; ++i) {
        um[dist(rng)] = i;
    }
    auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << "unordered_map insert " << N << ": "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms, size=" << um.size() << '\n';

    // map insert
    t1 = std::chrono::high_resolution_clock::now();
    std::map<int,int> m;
    for (int i = 0; i < N; ++i) {
        m[dist(rng)] = i;
    }
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "map insert " << N << ": "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms, size=" << m.size() << '\n';

    // Lookup benchmark
    int found_um = 0;
    t1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        if (um.count(dist(rng))) found_um++;
    }
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "unordered_map lookup: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms, found=" << found_um << '\n';

    int found_m = 0;
    t1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        if (m.count(dist(rng))) found_m++;
    }
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "map lookup: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms, found=" << found_m << '\n';
    return 0;
}

Output yang Dihasilkan (sample, bervariasi per mesin):

text
unordered_map insert 1000000: 191 ms, size=951688
map insert 1000000: 587 ms, size=951308
unordered_map lookup: 72 ms, found=95114
map lookup: 642 ms, found=95236
  1. Baris std::unordered_map<int,int> um;: hash table. Lookup rata-rata O(1) asumsi hash bagus.
  2. Baris std::map<int,int> m;: BST merah-hitam. Lookup selalu O(log n).
  3. Hasil unordered_map insert 191 ms vs map insert 587 ms: unordered_map ~3× lebih cepat. Konstanta BST (pointer chasing) mahal.
  4. Hasil unordered_map lookup 72 ms vs map lookup 642 ms: unordered_map ~9× lebih cepat untuk lookup.
  5. Catatan: size=951688 vs 951308 berbeda karena RNG state dipakai bersama (dist(rng) menghasilkan nilai berbeda untuk run kedua).

Tabel Keputusan Lengkap #

Berdasarkan Operasi #

Operasi UtamaStruktur DataKompleksitas
Akses indeks O(1)vector, arrayO(1)
Push/pop back O(1)vectorO(1) amortized
Push/pop depan O(1)dequeO(1)
Insert/delete di posisi iteratorlist (jarang)O(1)
Search exact keyunordered_set/mapO(1) rata-rata
Search terurut (lower_bound)set/mapO(log n)
Range sum statisPrefix sumO(1) query
Range sum dinamisBITO(log n)
Range max/min statisSparse tableO(1) query
Range max/min dinamisSegment treeO(log n)
Max/min dinamisHeap (priority_queue)O(log n) insert, O(1) top
K-th terkecil dinamisOrder statistic treeO(log n)
Path findingBFS/DFS + adjacency listO(V + E)
Shortest pathDijkstra (heap)O((V+E) log V)
MSTKruskal (sort + DSU)O(E log E)
SCCTarjan / KosarajuO(V + E)

Berdasarkan Tipe Soal #

Tipe SoalStruktur Data Kanonik
Counting sort (small range)Array of counters
Two sum (sorted)Two pointers
Longest increasing subseqvector + lower_bound (patience sorting)
Count inversionsBIT + coordinate compression
Maximum sliding windowMonotonic deque
Frequency tableunordered_map
Sorted frequencymap atau multiset
Disjoint setsDSU
Path / SCCAdjacency list + BFS/DFS

Ringkasan #

  • Pertanyaan pertama: operasi apa yang paling sering? Pilih struktur data yang menjamin kompleksitas terbaik untuk operasi tersebut.
  • std::vector adalah default terbaik — cache locality membuatnya cepat untuk hampir semua use case kecuali insert/delete di tengah.
  • std::unordered_map umumnya lebih cepat dari std::map untuk lookup, tetapi rawan collision hash di test case adversarial. Gunakan custom hash bila perlu.
  • Range query: statis (prefix sum, sparse table) untuk array tidak berubah, dinamis (BIT, segment tree) untuk array yang diupdate.
  • Selalu benchmark bila ragu — asumsi O(·) tidak selalu mencerminkan kecepatan nyata.