Bapati
Yazılım Geliştirme

Big-O Karmaşıklığı Nedir? Algoritma Performansını Anlamak

27 Temmuz 2026
6 dk okuma
Big-O Karmaşıklığı Nedir? Algoritma Performansını Anlamak

Big-O gösterimi nedir, bir algoritmanın performansı nasıl değerlendirilir? Karmaşıklık sınıflarını gerçek kod örnekleriyle açıklıyoruz.

Big-O Ne Söyler, Ne Söylemez?

Big-O gösterimi bir kodun kaç saniye süreceğini söylemez. Söylediği şey, veri miktarı arttıkça çalışma süresinin nasıl büyüyeceğidir.

Bu ayrım önemlidir: küçük veri kümelerinde O(n²) bir çözüm, O(n log n) bir çözümden daha hızlı çalışabilir. Fark, veri büyüdükçe ortaya çıkar. Yani Big-O bir ölçüm değil, bir eğilim tahminidir. Doğru soru "şu an ne kadar sürüyor" değil, "veri on kat artarsa ne olur" sorusudur.

Aynı işi yapan beş farklı yaklaşımın, veri büyüdükçe nereye gittiğini tek tabloda görmek meseleyi kapatır:

Karmaşıklıkn = 10n = 1.000n = 1.000.000Tipik örnek
O(1)111Sözlükten anahtarla okuma
O(log n)31020İkili arama, index taraması
O(n)101.0001.000.000Listeyi bir kez dolaşmak
O(n log n)3310.00020.000.000Verimli sıralama
O(n²)1001.000.0001.000.000.000.000İç içe iki döngü

Son satıra dikkat edin: bir milyon kayıtta O(n²), O(n)'e göre bir milyon kat daha fazla iş yapar. Bu, "biraz yavaş" değil "hiç bitmiyor" demektir.

O(1): Veri Ne Kadar Büyürse Büyüsün

Sabit zamanlı işlemler, veri miktarından etkilenmez. Bir dizinin belirli bir indisindeki elemana erişmek buna örnektir. Hash tabanlı yapılarda anahtarla arama da ortalama durumda O(1) kabul edilir; sözlük ve küme yapılarını bu kadar kullanışlı yapan özellik budur.

# n ne olursa olsun tek adım
kullanicilar = {"a1": "Ayşe", "b2": "Mehmet", "c3": "Zeynep"}
ad = kullanicilar["b2"]        # O(1)

liste = ["Ayşe", "Mehmet", "Zeynep"]
ad = liste[1]                  # O(1)

"Ortalama durumda" ifadesi önemli: hash çakışması yoğunlaşırsa bu erişim kötü senaryoda O(n)'e kadar düşebilir. Pratikte standart kütüphanelerin sözlük uygulamaları bunu sizin yerinize yönetir, ama kendi hash fonksiyonunuzu yazıyorsanız akılda tutulmalıdır.

O(log n): Her Adımda Yarıya Bölmek

Logaritmik karmaşıklık, her adımda arama alanını ikiye bölen algoritmalarda karşımıza çıkar. İkili arama klasik örnektir.

def ikili_arama(sirali_dizi, hedef):
    """Ön koşul: dizi SIRALI olmalı. Sıralı değilse sonuç anlamsızdır."""
    alt, ust = 0, len(sirali_dizi) - 1
    adim = 0
    while alt <= ust:
        adim += 1
        orta = (alt + ust) // 2          # her turda alan yarıya iner
        if sirali_dizi[orta] == hedef:
            return orta, adim
        if sirali_dizi[orta] < hedef:
            alt = orta + 1
        else:
            ust = orta - 1
    return -1, adim

dizi = list(range(1_000_000))
print(ikili_arama(dizi, 999_999))   # (999999, 20) — bir milyon eleman, 20 adım

Bir milyon elemanlı sıralı bir dizide aranan değeri bulmak yaklaşık yirmi adım sürer. Aynı işi baştan sona tarayarak yapmak bir milyon adım demektir. Veritabanı index'lerinin sorguları hızlandırmasının arkasındaki mantık da budur — ve aynı sebeple index'siz bir sütunda arama, tablo büyüdükçe doğrusal olarak yavaşlar.

O(n): Doğrusal Artış

Bir koleksiyonu baştan sona bir kez dolaşan her işlem doğrusaldır. Veri iki katına çıktığında süre de yaklaşık iki katına çıkar. Çoğu günlük işlem bu sınıfa girer ve pratikte gayet kabul edilebilirdir.

Dikkat edilmesi gereken nokta, doğrusal bir işlemin başka bir doğrusal işlemin içine yerleştirilmesidir; o noktada karmaşıklık sessizce kareselleşir. Ve bu, çoğu zaman iç içe iki for gibi görünmez:

# Görünürde tek döngü — ama içindeki `in` araması listede O(n)
def ortak_olanlar(a, b):
    return [x for x in a if x in b]      # O(n × m) — gizli kareselleşme

# Aynı iş, küme ile: `in` araması O(1)'e iner
def ortak_olanlar_hizli(a, b):
    b_kume = set(b)                      # O(m), bir kez
    return [x for x in a if x in b_kume] # O(n)

İki liste de 10.000 elemanlıysa üstteki 100 milyon karşılaştırma yapar, alttaki 20 bin. Kod neredeyse aynı görünür; fark yalnızca bir set() çağrısıdır.

O(n²): Sessiz Performans Katili

İç içe iki döngü, çoğu zaman farkında olmadan yazılır. Bin elemanlı bir liste için bu bir milyon işlem demektir; on bin eleman için yüz milyon. Uygulamanın test ortamında sorunsuz çalışıp üretimde yavaşlamasının en yaygın nedeni budur: test verisi küçüktür, gerçek veri değildir.

Aynı tuzağın en pahalı hâli veritabanında görülür ve adı N+1 sorgu problemidir:

// YANLIŞ: 1 sorgu liste için, her sipariş için 1 sorgu daha
var siparisler = db.Siparisler.ToList();          // 1 sorgu
foreach (var s in siparisler)
    Console.WriteLine(s.Musteri.Ad);              // her turda ayrı sorgu!
// 1.000 sipariş -> 1.001 veritabanı gidiş dönüşü

// DOĞRU: ilişkili veriyi tek seferde getir
var siparisler = db.Siparisler
                   .Include(s => s.Musteri)
                   .ToList();                      // 1 sorgu
foreach (var s in siparisler)
    Console.WriteLine(s.Musteri.Ad);               // ek sorgu yok

Buradaki maliyet CPU değil ağ gecikmesidir. Her sorgu 2 ms sürse bile 1.000 sorgu 2 saniye eder; sayfa açılmaz. Çözüm genellikle iç döngüyü bir sözlük araması ya da tek bir toplu sorgu ile değiştirmektir.

Bellek de Bir Karmaşıklıktır

Big-O yalnızca süre için değil, bellek için de kullanılır. Bazen ikisi arasında bilinçli bir takas yaparsınız: yukarıdaki set(b) örneği O(m) ek bellek harcayarak süreyi O(n×m)'den O(n)'e indirir.

YaklaşımSüreEk bellekNe zaman
Doğrudan taramaO(n×m)O(1)Veri küçük, bellek kısıtlı
Küme/sözlük kurmaO(n+m)O(m)Veri büyük — varsayılan tercih
Önce sıralayıp ikili aramaO(m log m + n log m)O(1)Sıralı erişim de gerekiyorsa

Bir milyonluk veride "biraz bellek harcayıp çok zaman kazanmak" neredeyse her zaman doğru taraftır; bunun istisnası gömülü sistemler gibi belleğin gerçekten kıt olduğu ortamlardır.

Amortize Edilmiş Karmaşıklık: Ortalama Değil, Yayılmış

Bazı işlemler çoğu zaman ucuzdur ama arada bir pahalıya patlar. Dinamik dizinin sonuna eleman eklemek buna örnektir: yer varken O(1)'dir, dolduğunda tüm dizi daha büyük bir alana kopyalanır ve o tek işlem O(n) olur.

Buna rağmen listeye ekleme "O(1) amortize" sayılır. Sebebi şu: kapasite her seferinde ikiye katlandığı için pahalı kopyalama giderek seyrekleşir; n ekleme işleminin toplam maliyeti O(n)'de kalır, yani ekleme başına ortalama sabit.

İşlemTipik durumKötü durumAmortize
Listeye sona eklemeO(1)O(n) — yeniden boyutlandırmaO(1)
Sözlüğe eklemeO(1)O(n) — yeniden hash'lemeO(1)
Listenin başına eklemeO(n)O(n)O(n)

Bu ayrım gerçek zamanlı sistemlerde önemlidir: amortize O(1) "her işlem hızlı" demek değil, "uzun vadede ortalama hızlı" demektir. Her isteğin sabit sürede bitmesi gereken bir yerde, o arada bir gelen O(n) tırmanışı gecikme grafiğinde diken olarak görünür.

Bilinen bir boyut varsa kapasiteyi baştan ayırmak bu dikenleri tamamen ortadan kaldırır:

// 100.000 elemanlık büyümede ~17 kez yeniden boyutlandırma yapar
var liste = new List<int>();

// Kapasite baştan ayrıldı: hiç yeniden boyutlandırma yok
var liste = new List<int>(100_000);

Doğru Veri Yapısı, Yarısı Çözülmüş Problem

Karmaşıklık çoğu zaman algoritmadan değil, seçilen veri yapısından gelir. Listede arama yapmak doğrusaldır; sözlükte arama yapmak sabittir.

İşlemListe / DiziSözlük (hash)Küme
İndisle erişimO(1)——
Değer aramaO(n)O(1)O(1)
Sona eklemeO(1)O(1)O(1)
Başa eklemeO(n)——
Silme (değere göre)O(n)O(1)O(1)
Sıra korunur muEvetEkleme sırasıHayır

Sık arama yapacaksanız sözlük, sıralı erişim gerekiyorsa liste, benzersizlik gerekiyorsa küme uygun seçimdir. Bu seçimleri baştan doğru yapmak, sonradan yapılacak optimizasyon çalışmalarının çoğunu gereksiz kılar.

Ne Zaman Önemsemeli?

Her kod parçası için karmaşıklık analizi yapmak gereksiz bir yüktür. Yüz elemanlık bir liste üzerinde O(n²) çalışmak kimseyi rahatsız etmez. Asıl dikkat edilmesi gereken yerler, veri miktarının zamanla büyüyeceği kısımlardır: kullanıcı listeleri, log kayıtları, sipariş geçmişi.

Pratik yaklaşım şudur: önce çalışan ve okunabilir kodu yaz, ölç, sonra yalnızca gerçekten darboğaz olan yeri iyileştir. Ölçmek de tahmin etmekten kolaydır:

import time

def sure_olc(fonksiyon, *args):
    bas = time.perf_counter()
    sonuc = fonksiyon(*args)
    return sonuc, (time.perf_counter() - bas) * 1000   # ms

# Aynı işi iki boyutta ölç: süre 10 kat mı, 100 kat mı arttı?
# 10 kat  -> O(n)      · sorun yok
# 100 kat -> O(n²)     · veri büyüdükçe patlayacak

Bu tek ölçüm, karmaşıklık analizi yapmadan da sınıfı size söyler: veriyi on katına çıkarın, süreye bakın. Süre on katına çıktıysa doğrusalsınız; yüz katına çıktıysa kareselsiniz ve o kod parçası er ya da geç sorun çıkaracaktır.


Daha fazlası: Big-O'yu görsel örneklerle anlattığımız bölüm, 'Her Yazılımcının Bilmesi Gerekenler' serimizin bir parçası; seride veri yapılarından eşzamanlılığa kadar 15 temel konu yer alıyor.

Videoyu sitemizde izle  ·  YouTube'da aç  ·  Kanala abone ol


Daha fazlası: Bilgisayarların içinde neler olup bittiğini anlattığımız video, performans konusuna farklı bir açıdan bakıyor.

Videoyu sitemizde izle  ·  YouTube'da aç  ·  Bapati kanalına abone ol

Bir projeniz mi var?
Birlikte hayata geçirelim!

Dijital Dönüşümünüzü Başlatın

Yazılım geliştirme, sistem altyapısı, teknik danışmanlık veya eğitim! Hangi alanda ihtiyacınız varsa hemen görüşelim. İlk adımı siz atın, gerisini birlikte çözelim!