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 #

KelasNamaContoh algoritmaUntuk n=1000
O(1)KonstanAkses elemen array1 operasi
O(log n)LogaritmikBinary search~10 operasi
O(n)LinierLinear search, jumlah1000 operasi
O(n log n)LinieritmikMerge sort, heap sort~10000 operasi
O(n²)KuadratikBubble sort, selection sort1 juta operasi
O(2ⁿ)EksponensialFibonacci rekursif naifTak 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 #

kotlin
// 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:

text
=== 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: 242785

Bedah Kode Baris-per-Baris #

Mari kita amati bagian kunci:

  1. Baris 4 (aksesKonstan): Akses elemen array by index adalah satu operasi, terlepas dari ukuran array. Kompleksitas O(1)—konstan.
  2. Baris 8-19 (binarySearch): Setiap iterasi membagi rentang pencarian menjadi dua. Jumlah iterasi maksimum log₂(n). Kompleksitas O(log n).
  3. Baris 22-26 (jumlahkan): Loop tunggal mengunjungi setiap elemen sekali. Kompleksitas O(n)—linier.
  4. Baris 29-46 (mergeSort): Membagi array menjadi dua bagian rekursif lalu menggabungkan. Tinggi pohon rekursi log n, setiap level melakukan O(n) kerja. Total O(n log n).
  5. 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:

AlgoritmaBestAverageWorst
Linear searchO(1) (elemen pertama)O(n)O(n) (elemen terakhir atau tidak ada)
Binary searchO(1) (elemen tengah)O(log n)O(log n)
Quick sortO(n log n)O(n log n)O(n²) (pivot buruk)
Merge sortO(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 #