Hash Tabloları
📌 Neden Hash Tabloları?
Binary Search’ün Özellikleri:
- Worst-case: O(log n) - çok verimli
- Gereksinimler: Sıralı veri + Doğrudan erişim
- Sıralı ekleme: O(n) - pratik olarak yavaş
Hash Tabloları ile İyileştirme:
- Ekleme: Genelde O(1), worst-case O(n)
- Arama: Genelde O(1), worst-case O(n)
🔑 Hash Fonksiyonu
Hash fonksiyonu, bir anahtarı (key) tablo indeksine dönüştürür.
String için Örnek Hash Fonksiyonu
public static int hash(String wort) {
int m = 13; // Tablo boyutu (asal sayı olmalı)
int k = 0;
for (int i = 0; i < wort.length(); i++) {
k += (int) wort.charAt(i); // ASCII/Unicode değerlerinin toplamı
}
return k % m; // Modulo ile indeks hesaplama
}
Hash Fonksiyonunun Özellikleri
Uzay U (Sonsuz anahtar uzayı)
↓
Hash Fonksiyonu h(k)
↓
Hash Tablosu T [0...m-1] (Sonlu alan)
- Evrensel küme (U): Tüm olası anahtarlar
- Anahtar kümesi (S): Gerçekte kullanılan anahtarlar
- Hash tablosu (T): Sabit boyutlu m elemanlı dizi
- Önemli: Hash fonksiyonları genelde injektif değildir (farklı anahtarlar aynı indeksi verebilir)
⚠️ Önemli Not
Tablo boyutu asal sayı olmalı! Bu, daha iyi dağılım sağlar.
💥 Kollizyon (Çakışma) Problemi
İki farklı anahtar aynı hash değerine sahip olabilir:
hash("Ali") = 5
hash("Veli") = 5 ← Kollizyon!
🔍 Sondaj Stratejileri (Collision Resolution)
1. Lineer Sondaj (Linear Probing)
Boş yer bulunana kadar sırayla ilerle:
Adım 0: hash(key) = i
Adım 1: (i + 1) % m
Adım 2: (i + 2) % m
Adım 3: (i + 3) % m
...
Örnek:
Tablo: [_, _, X, X, _, _, _]
0 1 2 3 4 5 6
hash(key) = 2 (dolu)
→ index 3'e bak (dolu)
→ index 4'e bak (boş) ✓ Buraya yerleştir!
❌ Problem: Kümelenme (Clustering)
- Aynı hash değerine sahip elemanlar yan yana birikir
- Arama performansı düşer
2. Karesel Sondaj (Quadratic Probing)
Kare sayılarla atlayarak ilerle:
Adım 0: hash(key) = i
Adım 1: (i + 1²) % m
Adım 2: (i + 2²) % m
Adım 3: (i + 3²) % m
...
⚠️ Potansiyel Problem:
- Tüm boş yerleri bulamayabilir
- Kümelenmeyi azaltır ama tam çözüm değil
3. Zincirleme (Chaining) - En Yaygın Yöntem
Her indekste bağlı liste kullan:
Hash Tablosu:
[0] → [Ali] → [Mehmet] → NULL
[1] → NULL
[2] → [Ayşe] → [Fatma] → [Zeynep] → NULL
[3] → [Veli] → NULL
[4] → NULL
...
✅ Avantajlar:
- Tüm elemanlar muhakkak saklanır
- Tablo doluluk oranı %100’ü aşabilir
- Ekleme her zaman mümkün
📊 Performans:
- Ortalama liste uzunluğu: α = n/m (yük faktörü)
- İdeal: α < 1 (az kollizyon)
🔎 Arama İşlemi
// Pseudo-kod
search(key) {
index = hash(key)
// Zincirleme yöntemi için
list = table[index]
for (element in list) {
if (element.key == key)
return element
}
return null // Bulunamadı
}
Adımlar:
- Hash değerini hesapla:
index = hash(key) - İlgili indeksteki listeyi tara
- Anahtar eşleşirse döndür
- Yoksa null döndür
📈 Performans Analizi
| İşlem | Ortalama | Worst-Case |
|---|---|---|
| Ekleme | O(1) | O(n) |
| Arama | O(1) | O(n) |
| Silme | O(1) | O(n) |
Worst-case ne zaman olur?
- Tüm elemanlar aynı indekse düşerse
- Tek bir uzun liste oluşur
- Binary Search Tree’ye benzer durum
💡 Pratik Öneriler
1. Tablo boyutu asal sayı olsun
İyi: 13, 31, 97, 199
Kötü: 10, 16, 100
2. Yük faktörünü kontrol et
α = n/m
α < 0.7 ise performans iyi
α > 0.9 ise tablo büyütülmeli
3. İyi hash fonksiyonu seç
- Anahtarları düzgün dağıtsın
- Hızlı hesaplansın
- Deterministik olsun
🎯 Özet
Hash Tabloları = Hız + Pratiklik
✅ Avantajlar
- Ortalamada çok hızlı: O(1)
- Basit implementasyon
- Dinamik boyutlandırma mümkün
⚠️ Dikkat Edilmesi Gerekenler
- İyi hash fonksiyonu kritik
- Kollizyon yönetimi önemli
- Worst-case O(n) unutulmamalı
📚 Karşılaştırma Tablosu
| Özellik | Binary Search | Hash Tablosu |
|---|---|---|
| Arama (avg) | O(log n) | O(1) |
| Arama (worst) | O(log n) | O(n) |
| Ekleme (avg) | O(n) | O(1) |
| Ekleme (worst) | O(n) | O(n) |
| Sıralama Gerekli | Evet | Hayır |
| Bellek | Verimli | Daha fazla |