Big-O Notation adalah bahasa formal untuk mengukur pertumbuhan kompleksitas algoritma terhadap ukuran input. Notasi ini memungkinkan kita membandingkan algoritma tanpa harus menjalankannya di mesin tertentu. Pada chapter ini kita akan mengimplementasikan enam kelas kompleksitas utama—O(1), O(log n), O(n), O(n log n), O(n²), dan O(2ⁿ)—dan mengukur jumlah operasi konkret untuk berbagai ukuran input. Pembaca akan melihat mengapa algoritma O(n²) menjadi tidak praktis untuk n = 10000, dan mengapa Fibonacci rekursif naif dengan O(2ⁿ) berjalan sangat lambat pada n = 50.
Kelas Kompleksitas Utama #
| Kelas | Nama | Contoh algoritma | Untuk n=1000 |
|---|---|---|---|
O(1) | Konstan | Akses elemen array | 1 operasi |
O(log n) | Logaritmik | Binary search | ~10 operasi |
O(n) | Linier | Linear search, jumlah | 1000 operasi |
O(n log n) | Linieritmik | Merge sort, heap sort | ~10000 operasi |
O(n²) | Kuadratik | Bubble sort, selection sort | 1 juta operasi |
O(2ⁿ) | Eksponensial | Fibonacci rekursif naif | Tak terbayakan |
Selisih antara kelas bertetangga bisa dramatis. O(n²) untuk n = 10000 menghasilkan 100 juta operasi, sementara O(n log n) untuk n yang sama hanya sekitar 130 ribu operasi—perbedaan 700 kali lipat.
Implementasi dan Pengukuran #
// Demo Big-O Notation: implementasi dan pengukuran berbagai kelas kompleksitas.
// O(1) - konstan: akses elemen array by index
fun aksesKonstan(arr: IntArray, index: Int): Int = arr[index]
// O(log n) - logaritmik: binary search
fun binarySearch(arr: IntArray, target: Int): Int {
var low = 0
var high = arr.lastIndex
while (low <= high) {
val mid = (low + high) / 2
when {
arr[mid] == target -> return mid
arr[mid] < target -> low = mid + 1
else -> high = mid - 1
}
}
return -1
}
// O(n) - linier: jumlah elemen
fun jumlahkan(arr: IntArray): Int {
var total = 0
for (x in arr) total += x
return total
}
// O(n log n) - linieritmik: merge sort (count operasi)
var hitungMerge = 0
fun mergeSort(arr: IntArray, left: Int = 0, right: Int = arr.lastIndex) {
if (left >= right) return
val mid = (left + right) / 2
mergeSort(arr, left, mid)
mergeSort(arr, mid + 1, right)
val temp = IntArray(right - left + 1)
var i = left
var j = mid + 1
var k = 0
while (i <= mid && j <= right) {
hitungMerge++
if (arr[i] <= arr[j]) temp[k++] = arr[i++]
else temp[k++] = arr[j++]
}
while (i <= mid) { temp[k++] = arr[i++]; hitungMerge++ }
while (j <= right) { temp[k++] = arr[j++]; hitungMerge++ }
for (x in temp.indices) arr[left + x] = temp[x]
}
// O(n^2) - kuadratik: bubble sort
var hitungBubble = 0
fun bubbleSort(arr: IntArray) {
val n = arr.size
for (i in 0 until n - 1) {
for (j in 0 until n - i - 1) {
hitungBubble++
if (arr[j] > arr[j + 1]) {
val t = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = t
}
}
}
}
// O(2^n) - eksponensial: Fibonacci rekursif
var hitungFib = 0
fun fibNaif(n: Int): Int {
hitungFib++
return if (n <= 1) n else fibNaif(n - 1) + fibNaif(n - 2)
}
fun main() {
println("=== Demo Big-O Notation ===")
val dataSorted = intArrayOf(2, 5, 8, 12, 16, 23, 38, 56, 72, 91)
println("Array: ${dataSorted.toList()}")
println("O(1) akses index 5: ${aksesKonstan(dataSorted, 5)}")
val idx = binarySearch(dataSorted, 23)
println("O(log n) binary search 23 -> index $idx")
println("O(n) jumlah elemen: ${jumlahkan(dataSorted)}")
println()
println("--- Perbandingan jumlah operasi ---")
println("%-10s %-10s %-12s %-12s %-12s".format("n", "O(log n)", "O(n)", "O(n log n)", "O(n^2)"))
val ukuran = listOf(10, 50, 100, 500)
for (n in ukuran) {
val log = jumlahOperasiLogN(n)
val lin = n
val nLogN = (n * log)
val kuad = n * n
println("%-10d %-10d %-12d %-12d %-12d".format(n, log, lin, nLogN, kuad))
}
println()
println("--- Fibonacci eksponensial O(2^n) ---")
for (n in listOf(5, 10, 15, 20, 25)) {
hitungFib = 0
val hasil = fibNaif(n)
println("fib($n) = $hasil, pemanggilan: $hitungFib")
}
}
fun jumlahOperasiLogN(n: Int): Int {
var hitung = 0
var i = n
while (i > 1) { i /= 2; hitung++ }
return hitung
}Output yang Dihasilkan:
=== Demo Big-O Notation ===
Array: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
O(1) akses index 5: 23
O(log n) binary search 23 -> index 5
O(n) jumlah elemen: 323
--- Perbandingan jumlah operasi ---
n O(log n) O(n) O(n log n) O(n^2)
10 3 10 30 100
50 5 50 250 2500
100 6 100 600 10000
500 8 500 4000 250000
--- Fibonacci eksponensial O(2^n) ---
fib(5) = 5, pemanggilan: 15
fib(10) = 55, pemanggilan: 177
fib(15) = 610, pemanggilan: 1973
fib(20) = 6765, pemanggilan: 21891
fib(25) = 75025, pemanggilan: 242785Bedah Kode Baris-per-Baris #
Mari kita amati bagian kunci:
- Baris 4 (
aksesKonstan): Akses elemen array by index adalah satu operasi, terlepas dari ukuran array. KompleksitasO(1)—konstan. - Baris 8-19 (
binarySearch): Setiap iterasi membagi rentang pencarian menjadi dua. Jumlah iterasi maksimumlog₂(n). KompleksitasO(log n). - Baris 22-26 (
jumlahkan): Loop tunggal mengunjungi setiap elemen sekali. KompleksitasO(n)—linier. - Baris 29-46 (
mergeSort): Membagi array menjadi dua bagian rekursif lalu menggabungkan. Tinggi pohon rekursilog n, setiap level melakukanO(n)kerja. TotalO(n log n). - Baris 53-67 (
fibNaif): Setiap panggilan memicu dua panggilan baru, membentuk pohon biner lengkap. Jumlah total pemanggilan tumbuh eksponensial:O(2ⁿ).
Best, Worst, Average Case #
Big-O menggambarkan kasus terburuk (worst case) kecuali dinyatakan lain. Namun ada juga best case dan average case yang penting dipahami:
| Algoritma | Best | Average | Worst |
|---|---|---|---|
| Linear search | O(1) (elemen pertama) | O(n) | O(n) (elemen terakhir atau tidak ada) |
| Binary search | O(1) (elemen tengah) | O(log n) | O(log n) |
| Quick sort | O(n log n) | O(n log n) | O(n²) (pivot buruk) |
| Merge sort | O(n log n) | O(n log n) | O(n log n) |
Memahami ketiga kasus penting untuk memilih algoritma yang tepat. Quick sort misalnya, average case-nya O(n log n) dan konstantanya kecil sehingga sering lebih cepat dari merge sort di praktik, meskipun worst case-nya O(n²).
Bahaya Eksponensial #
Algoritma O(2ⁿ) tidak praktis bahkan untuk n sedang. fib(25) pada contoh kita menghasilkan 242.785 pemanggilan function. fib(50) akan menghasilkan sekitar 2 triliun pemanggilan—bukan detik, tetapi jam atau hari. Inilah mengapa Fibonacci rekursif naif adalah contoh klasik mengapa kita perlu dynamic programming, yang akan kita bahas di chapter khusus.
Penutup #
Big-O Notation adalah alat komunikasi universal untuk mendiskusikan kinerja algoritma. Memahami enam kelas utama dan best/worst/average case-nya memberi kita kerangka berpikir untuk memilih algoritma yang tepat. Pada chapter berikutnya kita akan masuk ke struktur data konkret, dimulai dari Array dan Linked List.
Referensi #
Referensi Lanjutan #
- Kotlin Collections overview — pengantar koleksi Kotlin yang menyertakan catatan kompleksitas operasi dasar
- Kotlin Sequences — dokumentasi Sequences untuk membandingkan kompleksitas evaluasi lazy vs eager