🗂️ 8.1: Hash Table (Hash Tablosu)

O(1) Arama, Ekleme ve Silme - Detaylı Anlatım

📌 Hash Table Nedir?

Hash Table (diğer adlarıyla Hash Map, Dictionary, Associative Array), anahtar-değer (key-value) çiftlerini depolayan ve ortalama O(1) yani sabit zamanda arama, ekleme, silme yapabilen güçlü bir veri yapısıdır.

🚀 Neden Bu Kadar Hızlı?

Normal bir dizide "Ali" isimli kişiyi bulmak için tüm elemanları tek tek kontrol etmeniz gerekir → O(n)

Hash Table ise anahtarı matematiksel bir formülle doğrudan bellek adresine çevirir → O(1)

🆚 Karşılaştırma: "Ali" İsimli Kişiyi Bul

Yöntem İşlem Karmaşıklık
Liste ile Arama Baştan sona tüm elemanları kontrol et O(n) - 1 milyon kayıt = 1 milyon kontrol
Hash Table hash("Ali") → direkt indekse git O(1) - 1 milyon kayıt = 1 hesaplama!

🔑 Hash Fonksiyonu Nasıl Çalışır?

Hash Fonksiyonu, herhangi bir veriyi (string, sayı, obje) sabit boyutlu bir sayıya dönüştüren matematiksel fonksiyondur.

Anahtar → Hash Değeri → Dizi İndeksi

Anahtar
"Ali"
hash("Ali")
193419287
% 11
İndeks
3

Formül: indeks = hash(anahtar) % tablo_boyutu

% (modülo) operatörü, bölümden kalanı verir. Bu sayede her zaman 0 ile (tablo_boyutu-1) arası bir indeks elde ederiz.

🐍 Python'da hash() Fonksiyonu

Python Kodu
# Python'un yerleşik hash() fonksiyonu
# Her obje için benzersiz bir tamsayı üretir

# String'lerin hash değerleri
print("String hash değerleri:")
print(f"hash('Ali') = {hash('Ali')}")
print(f"hash('Ayşe') = {hash('Ayşe')}")
print(f"hash('ali') = {hash('ali')}")  # Büyük/küçük harf fark eder!

# Sayıların hash değerleri (kendileri)
print(f"\nhash(42) = {hash(42)}")
print(f"hash(3.14) = {hash(3.14)}")

# Tuple hash'lenebilir, list hash'lenemez!
print(f"\nhash((1, 2, 3)) = {hash((1, 2, 3))}")
# hash([1, 2, 3])  # TypeError: unhashable type: 'list'

# Modülo ile indeks hesaplama
tablo_boyutu = 11
anahtar = "Ali"
indeks = hash(anahtar) % tablo_boyutu
print(f"\nTablo boyutu: {tablo_boyutu}")
print(f"'{anahtar}' için indeks: {indeks}")
Çıktı bekleniyor...
⚠️ Dikkat: Python'da hash() değerleri program her çalıştığında değişebilir (güvenlik nedeniyle). Bu yüzden hash değerlerini dosyaya kaydetmeyin!

🎮 İnteraktif Hash Table Simülasyonu

11 hücreli bir hash table ile deney yapın. Anahtar-değer çiftleri ekleyin ve çarpışmaları gözlemleyin!

🧪 Deneyin: "Ali", "Ayşe", "Mehmet", "Fatma", "Zeynep", "Ahmet", "Elif" ekleyin ve çarpışmaları gözlemleyin!

💥 Çarpışma (Collision) Nedir?

Farklı anahtarlar aynı indekse hash'lendiğinde çarpışma (collision) oluşur. Bu kaçınılmazdır çünkü sonsuz sayıda anahtar, sınırlı sayıda hücreye eşlenir.

Örnek Çarpışma

hash("Ali") % 11 = 3
hash("Veli") % 11 = 3 ← Aynı indeks! ÇARPIŞMA!

🛠️ Çarpışma Çözüm Yöntemleri

🔗 1. Chaining (Zincirleme)

Her hücrede bir linked list tutulur. Çarpışan elemanlar listeye eklenir.

[0] → boş
[1] → (Ayşe:22)
[2] → boş
[3] → (Ali:25) → (Veli:30) → (Can:28)
[4] → (Fatma:27)
...

✅ Avantaj: Silme kolay, tablo hiç dolmaz
❌ Dezavantaj: Ek bellek (pointer'lar)

📍 2. Open Addressing

Çarpışmada başka boş hücre aranır. Tüm veriler tablonun içinde kalır.

Linear Probing: i+1, i+2, i+3...
Quadratic Probing: i+1², i+2², i+3²...
Double Hashing: i + j×hash2(key)

✅ Avantaj: Cache-friendly (veriler bitişik)
❌ Dezavantaj: Clustering problemi

📚 Terimler Sözlüğü

Cache-friendly: Veriler bellekte yan yana olduğunda CPU önbelleği (cache) daha verimli çalışır. Open addressing cache-friendly çünkü tüm veriler aynı dizide.
Clustering: Linear probing'de çarpışan elemanlar gruplar (kümeler) oluşturur. Bu gruplar büyüdükçe arama yavaşlar.
Load Factor (Yük Faktörü): Tablodaki dolu hücre oranı. load_factor = eleman_sayısı / tablo_boyutu. Genelde 0.7'yi geçince tablo büyütülür.

⚡ Zaman Karmaşıklığı Analizi

🔍 Arama
O(1)*
En kötü: O(n)
➕ Ekleme
O(1)*
En kötü: O(n)
🗑️ Silme
O(1)*
En kötü: O(n)
💾 Alan
O(n)
n eleman için n hücre

* Ortalama durum, iyi bir hash fonksiyonu ve düşük load factor ile

⚠️ En Kötü Durum Ne Zaman?

Kötü bir hash fonksiyonu tüm anahtarları aynı indekse atarsa, hash table linked list'e dönüşür → O(n). Bu yüzden iyi hash fonksiyonu kritik!

🐍 Python dict: Güçlü Bir Hash Table

Python'un dict (dictionary) yapısı, C dilinde yazılmış yüksek performanslı bir hash table implementasyonudur.

Python dict Kullanımı
# ===== PYTHON DICT TEMELLERİ =====

# 1. Oluşturma - Süslü parantez {} ile
ogrenciler = {
    "Ali": 85,      # anahtar: değer
    "Ayşe": 92,
    "Mehmet": 78
}
print(f"Öğrenciler: {ogrenciler}")

# 2. Ekleme - O(1) ortalama
# Yeni bir anahtar ile değer atama
ogrenciler["Fatma"] = 88
ogrenciler["Zeynep"] = 95
print(f"\nYeni eklemeler: {ogrenciler}")

# 3. Arama/Erişim - O(1) ortalama
# Köşeli parantez ile değere erişim
ali_notu = ogrenciler["Ali"]
print(f"\nAli'nin notu: {ali_notu}")

# 4. Güncelleme - O(1) ortalama
# Var olan anahtara yeni değer atama
ogrenciler["Ali"] = 90  # 85'ten 90'a güncellendi
print(f"Ali'nin yeni notu: {ogrenciler['Ali']}")

# 5. Silme - O(1) ortalama
# del anahtar kelimesi ile
del ogrenciler["Mehmet"]
print(f"\nMehmet silindi: {ogrenciler}")

# 6. Anahtar Kontrolü - O(1) ortalama
# 'in' operatörü ile
print(f"\n'Ayşe' var mı? {'Ayşe' in ogrenciler}")  # True
print(f"'Zehra' var mı? {'Zehra' in ogrenciler}")  # False

# 7. Güvenli Erişim - get() metodu
# Anahtar yoksa hata vermez, varsayılan değer döner
zehra_notu = ogrenciler.get("Zehra", "Kayıt yok")
print(f"Zehra'nın notu: {zehra_notu}")

# 8. Tüm Anahtarlar ve Değerler
print(f"\nTüm anahtarlar: {list(ogrenciler.keys())}")
print(f"Tüm değerler: {list(ogrenciler.values())}")
print(f"Tüm çiftler: {list(ogrenciler.items())}")
Çıktı bekleniyor...

🔧 Python dict İleri Kullanım

İleri dict Teknikleri
# ===== DİCT COMPREHENSION =====
# Liste comprehension'ın dict versiyonu
# Sözdizimi: {anahtar: değer for eleman in iterable}

# 1-10 arası sayıların kareleri
kareler = {x: x**2 for x in range(1, 11)}
print(f"Kareler: {kareler}")

# ===== SETDEFAULT =====
# Anahtar yoksa ekle, varsa mevcut değeri döndür
sayac = {}
kelimeler = ["elma", "armut", "elma", "kiraz", "elma"]

for kelime in kelimeler:
    # Eğer kelime yoksa 0 ile başlat, sonra 1 ekle
    sayac[kelime] = sayac.get(kelime, 0) + 1

print(f"\nKelime sayıları: {sayac}")

# ===== DEFAULTDICT =====
# collections modülünden - varsayılan değer fabrikası
from collections import defaultdict

# int() varsayılan olarak 0 döner
sayac2 = defaultdict(int)
for kelime in kelimeler:
    sayac2[kelime] += 1  # Olmayan anahtar otomatik 0 olur

print(f"defaultdict ile: {dict(sayac2)}")

# list() varsayılan olarak [] döner
gruplar = defaultdict(list)
ogrenciler = [("A", "Ali"), ("B", "Ayşe"), ("A", "Ahmet"), ("B", "Berk")]
for sinif, isim in ogrenciler:
    gruplar[sinif].append(isim)

print(f"\nSınıf grupları: {dict(gruplar)}")

# ===== COUNTER =====
# Sayma işlemi için özel dict
from collections import Counter

metin = "merhaba dünya merhaba python"
kelime_sayisi = Counter(metin.split())
print(f"\nCounter ile: {kelime_sayisi}")
print(f"En sık 2 kelime: {kelime_sayisi.most_common(2)}")
Çıktı bekleniyor...

📝 Python Syntax Açıklamaları

Dict Comprehension: {x: x**2 for x in range(5)} → Liste comprehension gibi ama dict üretir. x: x**2 anahtar-değer çiftini tanımlar.
get(key, default): Anahtar varsa değerini, yoksa default'u döner. d.get("yok", 0) → KeyError vermez, 0 döner.
defaultdict(factory): Olmayan anahtara erişildiğinde factory fonksiyonunu çağırıp varsayılan değer oluşturur. int → 0, list → [], str → ""
Counter: Elemanları sayan özel dict. most_common(n) en sık n elemanı döner.

🏗️ Kendi Hash Table Sınıfımızı Yazalım

Hash Table Implementasyonu (Chaining)
class HashTable:
    """
    Chaining yöntemi ile Hash Table implementasyonu.
    Her bucket bir liste (linked list yerine Python list).
    """

    def __init__(self, size=11):
        """
        Hash table oluştur.

        Args:
            size: Bucket sayısı (asal sayı tercih edilir)
        """
        self.size = size
        # Her bucket için boş liste oluştur
        self.buckets = [[] for _ in range(size)]
        self.count = 0  # Eleman sayısı

    def _hash(self, key):
        """Anahtarı indekse çevir."""
        return hash(key) % self.size

    def put(self, key, value):
        """Anahtar-değer çifti ekle veya güncelle."""
        index = self._hash(key)
        bucket = self.buckets[index]

        # Anahtar zaten var mı kontrol et
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)  # Güncelle
                return

        # Yeni eleman ekle
        bucket.append((key, value))
        self.count += 1

    def get(self, key):
        """Anahtarın değerini döndür."""
        index = self._hash(key)
        bucket = self.buckets[index]

        for k, v in bucket:
            if k == key:
                return v

        raise KeyError(f"Anahtar bulunamadı: {key}")

    def remove(self, key):
        """Anahtarı sil."""
        index = self._hash(key)
        bucket = self.buckets[index]

        for i, (k, v) in enumerate(bucket):
            if k == key:
                del bucket[i]
                self.count -= 1
                return v

        raise KeyError(f"Anahtar bulunamadı: {key}")

    def __contains__(self, key):
        """'in' operatörü için."""
        try:
            self.get(key)
            return True
        except KeyError:
            return False

    def __str__(self):
        """Görselleştirme."""
        result = []
        for i, bucket in enumerate(self.buckets):
            if bucket:
                items = ", ".join(f"{k}:{v}" for k, v in bucket)
                result.append(f"[{i}] → {items}")
            else:
                result.append(f"[{i}] → boş")
        return "\n".join(result)

# Test
ht = HashTable(7)

# Eklemeler
for isim, yas in [("Ali", 25), ("Ayşe", 22), ("Mehmet", 30),
                   ("Fatma", 28), ("Zeynep", 24), ("Ahmet", 27)]:
    ht.put(isim, yas)
    print(f"Eklendi: {isim} → {yas}")

print(f"\n=== Hash Table Durumu ===")
print(ht)
print(f"\nToplam eleman: {ht.count}")

# Arama
print(f"\nAli'nin yaşı: {ht.get('Ali')}")
print(f"'Zeynep' var mı? {'Zeynep' in ht}")
Çıktı bekleniyor...

🧩 Pratik Örnekler

Örnek 1: İki Sayının Toplamı (Two Sum)

LeetCode #1 - Two Sum
def two_sum(nums, target):
    """
    Dizide toplamı target olan iki sayının indekslerini bul.

    Brute Force: O(n²) - Her çift için kontrol
    Hash Table: O(n) - Tek geçiş!

    Mantık: Her sayı için "target - sayı" değerini hash'te ara.
    """
    # Gördüğümüz sayıları ve indekslerini sakla
    seen = {}  # {değer: indeks}

    for i, num in enumerate(nums):
        # enumerate(liste): (indeks, değer) çiftleri döner
        complement = target - num  # Aranan tamamlayıcı

        if complement in seen:
            # Bulundu! Tamamlayıcının indeksi ve şimdiki indeks
            return [seen[complement], i]

        # Bu sayıyı kaydet
        seen[num] = i

    return []  # Bulunamadı

# Test
nums = [2, 7, 11, 15]
target = 9
print(f"Dizi: {nums}")
print(f"Hedef: {target}")
print(f"Sonuç: {two_sum(nums, target)}")  # [0, 1] çünkü 2 + 7 = 9

nums2 = [3, 2, 4]
target2 = 6
print(f"\nDizi: {nums2}")
print(f"Hedef: {target2}")
print(f"Sonuç: {two_sum(nums2, target2)}")  # [1, 2] çünkü 2 + 4 = 6
Çıktı bekleniyor...

Örnek 2: İlk Tekrar Eden Karakter

First Repeating Character
def first_repeating(s):
    """
    String'deki ilk tekrar eden karakteri bul.

    Hash Set kullanarak O(n) çözüm.
    Set, sadece anahtarları tutan bir hash table'dır.
    """
    seen = set()  # Gördüğümüz karakterler

    for char in s:
        if char in seen:  # O(1) kontrol
            return char
        seen.add(char)  # O(1) ekleme

    return None  # Tekrar yok

# Test
test_strings = ["abcabc", "abcdef", "aabbcc", "python"]

for s in test_strings:
    result = first_repeating(s)
    if result:
        print(f"'{s}' → İlk tekrar: '{result}'")
    else:
        print(f"'{s}' → Tekrar yok")
Çıktı bekleniyor...

🔬 Python dict İç Yapısı

Python'un dict implementasyonu çok optimize edilmiştir. Python 3.7+ ile dict'ler ekleme sırasını korur.

# Python dict iç yapısı (basitleştirilmiş)

# Her dict şunları tutar:
# 1. Hash tablosu (sparse array) - indeksler
# 2. Compact array - anahtar-değer çiftleri

# Örnek: {"a": 1, "b": 2, "c": 3}

# Hash tablosu (8 slot):
# [None, 0, None, None, 1, None, 2, None]
#           ↓               ↓          ↓

# Compact array (sıralı):
# [(hash_a, "a", 1), (hash_b, "b", 2), (hash_c, "c", 3)]

# Bu tasarım:
# ✅ Bellek verimli (eski dict'lerden %25 daha az)
# ✅ Ekleme sırasını korur
# ✅ Hızlı iteration

🌍 Gerçek Hayat Kullanım Alanları

📚 Veritabanı İndeksleri

Veritabanları kayıtları hızlı bulmak için hash indeksleri kullanır. SELECT * WHERE id = 12345 → O(1)

🔐 Şifre Depolama

Şifreler düz metin değil, hash'lenmiş hali saklanır. bcrypt, sha256 gibi güvenli hash fonksiyonları kullanılır.

🌐 DNS Cache

Tarayıcınız domain → IP eşlemelerini hash table'da cache'ler. google.com → 142.250.185.78

📦 Cache Sistemleri

Redis, Memcached gibi sistemler tamamen hash table tabanlıdır. Web sitelerinde sık erişilen veriler cache'lenir.

🐍 Python İç Yapısı

Python'da her şey dict: global/local değişkenler, class attribute'ları, module namespace'leri...

🔍 Derleyiciler

Symbol table'lar hash table'dır. Değişken isimlerini hızlıca bulup türünü, adresini getirir.

🆚 Hash Table vs Diğer Veri Yapıları

İşlem Array Linked List BST (Dengeli) Hash Table
Arama (key) O(n) O(n) O(log n) O(1)*
Ekleme O(n) O(1)** O(log n) O(1)*
Silme O(n) O(n) O(log n) O(1)*
Sıralı Gezinme ✅ O(n) ✅ O(n) ✅ O(n) ❌ Sıra yok
Min/Max Bulma O(n) O(n) O(log n) O(n)

* Ortalama durum, ** Başa ekleme

📋 Özet ve Önemli Noktalar

✅ Ne Zaman Hash Table Kullan?

  • Hızlı arama gerektiğinde: O(1) arama kritik
  • Anahtar-değer eşlemesi: İsim → telefon, ID → kayıt
  • Duplicate kontrolü: set kullanarak
  • Sayma/gruplama: Counter, groupby işlemleri
  • Caching: Hesaplanmış sonuçları sakla

❌ Ne Zaman Hash Table Kullanma?

  • Sıralı veri gerektiğinde: Hash table sıra tutmaz (Python 3.7+ hariç)
  • Range query: "10-20 arası değerler" gibi sorgular
  • Min/max sık gerekiyorsa: Heap daha iyi
  • Bellek çok kısıtlı: Hash table boş alan bırakır