Amortized Analysis — Struktur Data

Kamu sering dengar kalimat seperti "JavaScript Array.push itu O(1) amortized". Apa sih maksudnya? Amortized analysis adalah cara mengukur kompleksitas rata-rata

Kamu sering dengar kalimat seperti "JavaScript Array.push itu O(1) amortized". Apa sih maksudnya?

Amortized analysis adalah cara mengukur kompleksitas rata-rata per operasi dalam jangka panjang, bukan dalam satu operasi tunggal. Beberapa operasi boleh lambat (O(n)), asal cukup jarang sehingga rata-ratanya tetap cepat.

Motivasi: kenapa "worst case" saja tidak cukup

Worst case melihat operasi terburuk yang mungkin terjadi sekali. Tapi dalam praktik, yang kita peduli adalah total biaya untuk N operasi. Kalau dari N operasi cuma 1 yang lambat dan 999 cepat, rata-rata per operasi tetap cepat — dan itu yang dilihat pengguna.

Analogi: bayar parkir Rp 3.000/hari setiap hari = sama mahalnya dengan bayar Rp 90.000 sekaligus sekali sebulan. Biaya totalnya identik; hanya distribusinya berbeda.

Contoh klasik: dynamic array resize

JavaScript Array (dan Python list, Java ArrayList, Go slice) adalah dynamic array — ukurannya bertambah otomatis. Tapi memori fisik array harus berurutan, jadi saat kapasitas penuh, implementasi melakukan:

  1. Alokasi array baru, biasanya 2× ukuran lama
  2. Copy semua elemen lama ke array baru
  3. Tambahkan elemen baru
  4. Buang array lama

Langkah 2 itu O(n). Jadi sekilas, push kadang O(1) (ada ruang) dan kadang O(n) (harus resize).

Apakah pushnya "benar-benar" O(n)? Tidak — karena resize sangat jarang.

Hitung dengan aggregate method: untuk n operasi push mulai dari array kosong, berapa total operasi?

Push ke-n Apa yang terjadi Biaya
1 Tulis 1 slot 1
2 Resize (copy 1) + tulis 2
3 Resize (copy 2) + tulis 3
4 Tulis 1
5 Resize (copy 4) + tulis 5
6, 7, 8 Tulis 1 masing-masing
9 Resize (copy 8) + tulis 9
10..16 Tulis 1 masing-masing

Total biaya untuk n push ≈ n + (1 + 2 + 4 + 8 + ... + n) ≤ n + 2n = 3n. Rata-rata per push = 3n / n = O(1) amortized.

Artinya: "push boleh sesekali O(n), tapi frekuensinya menurun eksponensial, jadi rata-ratanya tetap konstan."

const arr = [];
// 1 juta push
for (let i = 0; i < 1_000_000; i++) {
  arr.push(i); // total ~3 juta operasi dasar, bukan n²
}

Contoh kedua: hash table rehash

Hash table juga resize saat load factor (size / capacity) melewati threshold (biasanya 0.75). Saat rehash, setiap key dihitung ulang hashnya dan dipindah ke bucket baru — itu O(n).

Tapi seperti dynamic array, rehash terjadi setiap ~2× pertumbuhan, sehingga amortized cost set / get / delete tetap O(1).

Banker's method: intuisi "simpan tabungan"

Cara lain memandangnya: setiap operasi "cepat" membayar biaya ekstra (misalnya 3 unit, bukan 1) — kelebihannya disimpan sebagai tabungan di setiap elemen.

Saat resize datang, tabungan itu dipakai untuk membayar copy. Karena tiap elemen sudah menabung cukup sebelum resize, biaya resize sudah lunas dari sisi akunting — jadi tidak membebani operasi apapun saat itu.

Ini hanya trik pembuktian — intuisinya: biaya mahal sudah "di-prepay" oleh banyak operasi murah sebelumnya.

Amortized vs Average case — jangan tertukar

Istilah Arti
Worst case Operasi paling mahal yang mungkin terjadi sekali
Average case Ekspektasi atas distribusi input acak (probabilistik)
Amortized Rata-rata biaya per operasi dalam urutan operasi apapun (tanpa asumsi probabilistik)

Amortized lebih kuat dari average case — dia berlaku untuk sekuens operasi apapun, bahkan adversarial. Tapi dia berlaku pada sekuens, bukan pada satu operasi. Jadi kalau kamu peduli latency per operasi (misalnya real-time systems), amortized tidak cukup — kamu mungkin butuh struktur dengan worst case yang terjamin (misalnya balanced BST dibanding hash table).

Intinya

🎭 Analogi sehari-hari: Bayar parkir pakai kartu pulsa.

Itulah dynamic array push: 99% cepat, 1% resize lambat, rata-ratanya tetap O(1).

💡 Mengapa 2× resize, bukan +10 tiap kali? Resize +konstan = O(n²) total cost. Resize ×2 = total cost = sum of geometric series ≤ 2n = O(n) total = O(1) per push amortized. Inilah kenapa hampir semua dynamic array (Vector di C++, ArrayList di Java, list di Python, slice di Go) pake growth factor multiplier, bukan additive.

⚠️ Jebakan umum:

🎯 Worst-case vs Amortized vs Average:

🧪 Tebakan cepat: arr.push(x) 1jt kali. Total operasi? Worst-case naive: O(n²) = 1 triliun. Amortized real: ~3jt = O(n) total = O(1) per push. Beda: 1 juta kali lipat.

TL;DR: Amortized = rata-rata per operasi atas urutan operasi. Resize sesekali OK karena dibagi banyak operasi cepat. Aman untuk loop, tapi awas latency real-time.

Yang akan kamu pelajari