📊 4.7: Linked List Analizi

Big O Karşılaştırması ve Seçim Kriterleri

🤔 Bu Sayfa Neden Önemli?

Şimdiye kadar Array ve Linked List yapılarını öğrendik. Peki hangisini ne zaman kullanmalıyız? Bu sorunun cevabı, her iki yapının performans özelliklerini anlamaktan geçiyor.

Programlamada "performans" derken genellikle iki şeyi kastediyoruz: işlemlerin ne kadar zaman aldığı ve ne kadar hafıza kullandığı. Big O notasyonu, bu özellikleri veri boyutu büyüdükçe nasıl değiştiğini anlamamıza yardımcı olur.

Aşağıdaki tabloda O(1) "sabit süre" demek - veri ne kadar büyürse büyüsün işlem aynı sürede biter. O(n) ise "lineer süre" demek - veri 10 kat büyürse işlem de kabaca 10 kat uzun sürer. Tabii ki O(1) daha iyidir!

⏱️ Zaman Karmaşıklığı Karşılaştırması

Aşağıdaki tablo, farklı veri yapılarında temel işlemlerin ne kadar sürdüğünü gösteriyor. Yeşil renkteki O(1) değerleri hızlı işlemleri, kırmızı renkteki O(n) değerleri ise yavaş işlemleri temsil ediyor.

Operasyon Array Singly LL Doubly LL Python list deque
Erişim (index) O(1) O(n) O(n) O(1) O(n)
Arama (değer) O(n) O(n) O(n) O(n) O(n)
Başa Ekleme O(n) O(1) O(1) O(n) O(1)
Sona Ekleme O(1)* O(n)/O(1)† O(1) O(1)* O(1)
Ortaya Ekleme O(n) O(n) O(n) O(n) O(n)
Baştan Silme O(n) O(1) O(1) O(n) O(1)
Sondan Silme O(1) O(n) O(1) O(1) O(1)
Ortadan Silme O(n) O(n) O(n)‡ O(n) O(n)

* Amortized O(1) - Çoğu durumda O(1), bazen yeniden boyutlandırma gerekir
† Tail pointer varsa O(1)
‡ Node verilmişse O(1), değer ile aranıyorsa O(n)

🔍 Tabloyu Nasıl Okumalı?

Erişim (index): "5. elemana git" dediğimizde Array doğrudan gidebilir çünkü tüm elemanlar yan yana duruyor. Linked List'te ise baştan başlayıp 5 kez "sonraki" dememiz gerekir - bu yüzden O(n).

Başa Ekleme: Array'in başına eleman eklemek için tüm elemanları bir sağa kaydırmamız gerekir (O(n)). Linked List'te ise sadece yeni node'un next pointer'ını ayarlarız - tek adım, O(1).

Sondan Silme: Singly Linked List'te son elemanı silmek için bir önceki elemana ulaşmamız gerekir, ama geriye gidemeyiz. Bu yüzden baştan başlayıp sondan bir öncekine kadar gitmemiz gerekir - O(n). Doubly Linked List'te ise tail.prev ile doğrudan ulaşırız - O(1).

💾 Alan Karmaşıklığı (Hafıza Kullanımı)

Bir veri yapısının ne kadar hafıza kullandığı, özellikle büyük veri setleriyle çalışırken çok önemlidir. Array ve Linked List bu konuda çok farklı davranır.

Array elemanlarını hafızada yan yana (bitişik) saklar. Bu sayede ekstra alan kullanmaz - sadece verinin kendisi için yer ayrılır. Ancak bir dezavantajı var: Array'i büyütmek istediğimizde, yeterli büyüklükte yeni bir bitişik alan bulup tüm veriyi oraya taşımamız gerekebilir.

Linked List ise her eleman için veri + pointer (işaretçi) saklar. Bu pointer, sonraki (veya önceki) elemanın hafızadaki adresini tutar. 64-bit bir sistemde her pointer 8 byte yer kaplar. Yani bir integer (4 byte) saklarken aslında 4 + 8 = 12 byte kullanmış oluruz. Bu %200 overhead demek!

📦 Array (Bitişik Hafıza)

10
20
30
40
-
-
Toplam: 4 × size(int) = 16 byte

🔗 Linked List (Dağınık Hafıza + Pointer)

10
ptr
20
ptr
30
ptr
40
null

Toplam: 4 × (size(int) + size(pointer)) = 4 × (4 + 8) = 48 byte (64-bit)

⚠️ Hafıza Overhead Neden Önemli?

Linked List her eleman için ekstra 8 byte (64-bit sistemde) pointer tutar. Doubly Linked List için bu 16 byte olur (prev + next pointer'ları için).

Küçük veri tiplerinde bu fark çok belirgin olur. Örneğin, bir milyon integer saklamak istediğinizi düşünün. Array ile 4 MB yeterli olurken, Singly Linked List ile 12 MB, Doubly Linked List ile 20 MB kullanırsınız. Mobil uygulamalar veya gömülü sistemler için bu fark kritik olabilir.

Ayrıca Linked List'in dağınık hafıza yapısı, modern işlemcilerin "cache" mekanizmasıyla iyi çalışmaz. Array'deki bitişik elemanlar cache'e birlikte yüklenir ve çok hızlı erişilir. Linked List'te ise her eleman farklı yerde olduğu için cache sürekli "miss" yapar - bu da pratikte 10-100 kat yavaşlık demek olabilir.

🎯 Ne Zaman Hangisini Kullanmalı?

Teorik Big O değerleri önemli, ama gerçek hayatta seçim yaparken duruma göre düşünmek gerekir. Aşağıda her iki yapının güçlü olduğu senaryoları açıklıyoruz.

📦 Array / List Tercih Et

Eğer verinize sık sık index ile erişiyorsanız (örneğin "10. elemanı getir" gibi), Array açık ara kazanır. Ayrıca Array'ler hafızada bitişik durduğu için işlemci cache'i çok verimli kullanır - bu da pratikte büyük hız farkı yaratır.

Örnek durumlar: Oyun skorları listesi, öğrenci notları, görüntü piksel verileri, sıralama algoritmaları, matematiksel hesaplamalar.

🔗 Linked List Tercih Et

Eğer sürekli başa veya ortaya ekleme/silme yapıyorsanız, Linked List mantıklı olur. Özellikle "gerçek zamanlı" sistemlerde (oyunlar, robotlar) Linked List'in tahmin edilebilir performansı önemlidir - Array'in ara sıra yaptığı "yeniden boyutlandırma" gecikmeleri sorun yaratabilir.

Örnek durumlar: Undo/Redo geçmişi, LRU Cache, müzik çalma listesi, tarayıcı geçmişi, işletim sistemi görev kuyruğu.

💡 Pratik Tavsiye

Python'da çoğu durumda standart list kullanmak en iyi seçimdir. Python listleri aslında dinamik array'dir ve çok optimize edilmiştir. Sadece şu durumlarda collections.deque (çift uçlu kuyruk, aslında doubly linked list) kullanmayı düşünün:

1. Listenin başına çok sık ekleme/silme yapıyorsanız (appendleft, popleft)
2. Sabit boyutlu bir "kayan pencere" tutuyorsanız (son N elemanı sakla gibi)
3. FIFO kuyruk (queue) implementasyonu yapıyorsanız

🧪 Pratik Performans Testi

Teorik Big O değerleri güzel, ama gerçekte ne kadar fark yaratıyor? Aşağıdaki kod, Python'da list (array tabanlı) ve deque (linked list tabanlı) arasındaki performans farkını ölçüyor. Kodu çalıştırarak sonuçları kendiniz görün!

Özellikle "başa ekleme" ve "baştan silme" işlemlerinde deque'in ne kadar hızlı olduğuna, ama "index erişimi"nde list'in nasıl ezdiğine dikkat edin.

Python Kodu
import time
from collections import deque

N = 30000
print(f"🧪 Performans Testi: {N} işlem\n")
print("=" * 55)

# ======= BAŞA EKLEME =======
print("\n📌 BAŞA EKLEME:")

# List
liste = []
start = time.time()
for i in range(N):
    liste.insert(0, i)
list_time = time.time() - start
print(f"  list.insert(0, x)   : {list_time:.4f} sn")

# Deque (LL benzeri)
d = deque()
start = time.time()
for i in range(N):
    d.appendleft(i)
deque_time = time.time() - start
print(f"  deque.appendleft(x) : {deque_time:.4f} sn")

print(f"  📊 Deque {list_time/deque_time:.0f}x hızlı!")

# ======= BAŞTAN SİLME =======
print("\n📌 BAŞTAN SİLME:")

liste = list(range(N))
start = time.time()
while liste:
    liste.pop(0)
list_time = time.time() - start
print(f"  list.pop(0)         : {list_time:.4f} sn")

d = deque(range(N))
start = time.time()
while d:
    d.popleft()
deque_time = time.time() - start
print(f"  deque.popleft()     : {deque_time:.4f} sn")

print(f"  📊 Deque {list_time/deque_time:.0f}x hızlı!")

# ======= RANDOM ACCESS =======
print("\n📌 RANDOM ACCESS (index erişimi):")

liste = list(range(N))
d = deque(range(N))

# List
start = time.time()
for i in range(N):
    _ = liste[i]
list_time = time.time() - start
print(f"  list[i]             : {list_time:.4f} sn")

# Deque
start = time.time()
for i in range(N):
    _ = d[i]
deque_time = time.time() - start
print(f"  deque[i]            : {deque_time:.4f} sn")

print(f"  📊 List {deque_time/list_time:.1f}x hızlı!")

print("\n" + "=" * 55)
print("\n📋 SONUÇ:")
print("  • Başa ekleme/silme → deque (Linked List)")
print("  • Index erişimi → list (Array)")
print("  • Genel kullanım → list genellikle yeterli")
Çıktı bekleniyor...

📚 Özet ve Son Sözler

Bu sayfada Array ve Linked List veri yapılarının performans özelliklerini karşılaştırdık. Aşağıdaki tablo, öğrendiklerimizi özetliyor.

Özellik Array Linked List
Hafıza Yerleşimi Bitişik (contiguous) - elemanlar yan yana Dağınık (scattered) - elemanlar hafızada farklı yerlerde
Cache Performansı ✅ Çok iyi - bitişik veriler cache'e birlikte yüklenir ❌ Zayıf - her elemana erişimde cache miss olabilir
Boyut Değişikliği Bazen maliyetli - yeniden tahsis ve kopyalama gerekebilir ✅ Kolay - sadece pointer ayarla
Hafıza Kullanımı ✅ Verimli - sadece veri saklanır Her eleman için +8 byte (64-bit) pointer overhead
Python'da Karşılığı list - genel amaçlı, çoğu durumda tercih edilmeli collections.deque - başa ekleme/silme yoğunsa

🎓 Öğrenilecek En Önemli Şey

Veri yapıları arasında seçim yaparken "en iyi" diye bir şey yoktur - sadece "bu durum için en uygun" vardır. Bir yarış arabasının şehir içi trafiğinde işe yaramayacağı gibi, her veri yapısının da güçlü ve zayıf yönleri vardır.

Gerçek projelerde çoğu zaman Python list kullanmak yeterlidir. Ama performans sorunuyla karşılaşırsanız, bu sayfada öğrendikleriniz sorunu teşhis etmenize ve doğru çözümü bulmanıza yardımcı olacak.