İçeriğe geç
Educora
Üniversite25 dk48 / 59

Hash tabloları

Hash fonksiyonlarının anahtarı indekse nasıl çevirdiğini, çakışmaların zincirleme ve açık adreslemeyle nasıl çözüldüğünü ve doluluk oranının ortalama O(1) süreyi nasıl sağladığını öğren.

Kendini test et
Bu derste öğreneceklerin
  • Bir metin anahtarı için polinom hash değerini ve indeksi hesaplamak
  • Tabloyu zincirleme ve doğrusal sondalamayla adım adım doldurmak
  • Doluluk oranından beklenen sondalama sayısını tahmin etmek

Telefon rehberinde Elvin'in numarasını tüm listeyi tarayarak bulmak O(n), sıralı bir listede ikili aramayla bulmak O(log n) sürer. Peki adım sayısı n'ye hiç bağlı olmasaydı? Hash tablosu (özet tablosu) tam olarak bunu yapar: anahtarı doğrudan bir dizi indeksine çevirir. Python'un dict ve set türleri, veri tabanlarının hash indeksleri, önbellekler ve derleyicilerin sembol tabloları bu fikir üzerine kuruludur.

Hash fonksiyonu

Tanım
Hash tablosu

m boyutlu bir dizi (hücrelerine kova denir) ve bir anahtarı 0 … m − 1 aralığında bir indekse eşleyen hash fonksiyonu h. “Anahtar → değer” çifti h(anahtar) kovasında saklanır ve arama doğrudan o kovadan başlar.

h(s) = (s₀ · pᵏ⁻¹ + s₁ · pᵏ⁻² + … + sₖ₋₁ · p⁰) mod m
burada:
  • s₀ … sₖ₋₁metnin karakterlerinin sayısal kodları
  • ptaban (genellikle 31 gibi küçük bir asal sayı)
  • kmetnin uzunluğu
  • mkova sayısı

Polinom hash hem karakterlere hem de sıralarına bağlıdır: “ab” ile “ba” farklı hash değerleri alır.

Örnek 1: bir metnin hash değeri

Harflere a = 1, b = 2, c = 3 kodlarını verelim. p = 31 ve m = 11 iken “cab” anahtarı hangi kovaya düşer?

Çözümü göster
h = 3 · 31² + 1 · 31 + 2 = 3 · 961 + 31 + 2 = 2883 + 33 = 2916.
2916 = 11 · 265 + 1 olduğundan 2916 mod 11 = 1.
“cab” 1 numaralı kovaya düşer. Uygulamada Horner yöntemi kullanılır: h = (h · 31 + kod); her karakter için bir çarpma ve bir toplama, yani O(k).
  • Belirlenimcilik: aynı anahtar her zaman aynı hash değerini verir.
  • Düzgün dağılım: anahtarlar kovalara yaklaşık eşit dağılır.
  • Hız: hesaplama O(anahtar uzunluğu) sürer.
  • Anahtarın tamamını kullanır: yalnızca ilk harfe bakan bir fonksiyon “Aysel” ile “Anar”ı aynı kovaya atar.

Çakışmalar ve çözümleri

Olası anahtarlar kovalardan çok daha fazladır; bu yüzden güvercin yuvası ilkesine göre iki farklı anahtarın aynı kovaya düşmesi, yani çakışma kaçınılmazdır. Üstelik beklenenden erken olur: buna “doğum günü paradoksu” denir. Yalnızca 23 kişilik bir grupta iki kişinin aynı gün doğmuş olma olasılığı %50'yi aşar.

P(çakışma yok) ≈ e^(−k(k − 1) / (2m))
burada:
  • kyerleştirilen anahtar sayısı
  • mkova sayısı

m = 365, k = 23: k(k − 1)/2 = 253, 253/365 ≈ 0,693, e^(−0,693) ≈ 0,5. Çakışmaların başlaması için yaklaşık √m anahtar yeterlidir.

  • Zincirleme (separate chaining): her kova bir liste tutar; çakışan anahtarlar bu listeye eklenir.
  • Açık adresleme: tüm anahtarlar dizinin kendisinde durur; kova doluysa bir sonraki boş yer aranır. Doğrusal sondalama: h(k), h(k) + 1, h(k) + 2, … (mod m).
  • Karesel sondalama (h(k) + i²) ve çift hash (h₁(k) + i · h₂(k)), anahtarların bir yere yığılmasını (kümelenmeyi) azaltır.
Örnek 2: doğrusal sondalama

m = 11, h(k) = k mod 11. 12, 44, 13, 88, 23, 94, 11, 39, 20 anahtarlarını doğrusal sondalamayla yerleştir.

Çözümü göster
12 → 1; 44 → 0; 13 → 2.
88 → 0 dolu, 1 dolu, 2 dolu → 3 (4 sondalama).
23 → 1, 2, 3 dolu → 4 (4 sondalama).
94 → 6.
11 → 0, 1, 2, 3, 4 dolu → 5 (6 sondalama).
39 → 6 dolu → 7; 20 → 9.
Sonuç: [44, 12, 13, 88, 23, 11, 94, 39, —, 20, —], α = 9/11 ≈ 0,82.
0–7 hücreleri bir küme oluşturdu: oraya düşen her yeni anahtar uzun bir yol gidecek.
Python
def insert_linear(table, key):
    m = len(table)
    i = key % m
    probes = 1
    while table[i] is not None:
        i = (i + 1) % m
        probes += 1
    table[i] = key
    return probes

table = [None] * 11
for k in [12, 44, 13, 88, 23, 94, 11, 39, 20]:
    p = insert_linear(table, k)
    print(k, '-> slot', table.index(k), 'probes:', p)
print(table)
print('load factor:', round(9 / 11, 2))
▸ Beklenen çıktı
12 -> slot 1 probes: 1
44 -> slot 0 probes: 1
13 -> slot 2 probes: 1
88 -> slot 3 probes: 4
23 -> slot 4 probes: 4
94 -> slot 6 probes: 1
11 -> slot 5 probes: 6
39 -> slot 7 probes: 2
20 -> slot 9 probes: 1
[44, 12, 13, 88, 23, 11, 94, 39, None, 20, None]
load factor: 0.82
Program elle yaptığımız izlemeyi doğrular. Zincirlemede aynı anahtarlar şöyle yerleşirdi: 0: 44 → 88 → 11, 1: 12 → 23, 2: 13, 6: 94 → 39, 9: 20.

Doluluk oranı ve ortalama O(1)

α = n / mα = n / m
burada:
  • αdoluluk oranı (yük faktörü)
  • ntablodaki anahtar sayısı
  • mkova sayısı

Zincirlemede α ortalama zincir uzunluğudur ve 1'i aşabilir; açık adreslemede her zaman α < 1'dir.

E(zincirleme) = 1 + α; E(doğrusal sondalama) ≈ ½ · (1 + 1 / (1 − α)²)E(zincirleme) = 1 + α; E(doğrusal sondalama) ≈ ½ · (1 + 1 / (1 − α)²)
burada:
  • Ebaşarısız aramada (anahtar yok) beklenen sondalama sayısı

Düzgün dağılan bir hash varsayımıyla. Doğrusal sondalama formülü Knuth'un analizinden gelir; α → 1 iken sondalama sayısı hızla artar.

αZincirleme: 1 + αDoğrusal sondalama (başarısız)Doğrusal sondalama (başarılı) ½(1 + 1/(1 − α))
0,51,52,51,5
0,751,758,52,5
0,91,950,55,5
Beklenen sondalama sayısı. Açık adreslemeli tablolar α ≈ 0,5–0,75'in üzerinde doldurulmamalıdır.

α bir eşiği aştığında (örneğin Java HashMap'te 0,75) tablo yaklaşık iki katına büyütülür ve bütün anahtarlar yeni m için yeniden hash'lenir (rehashing). Bu ara sıra yapılan O(n) iş, dinamik dizideki gibi amortize edilir; bu yüzden ekleme, arama ve silme ortalamada O(1) kalır. En kötü durumda, yani tüm anahtarlar tek kovaya düştüğünde, O(n)'dir.

Python
class HashMap:
    def __init__(self, m=5):
        self.buckets, self.n = [[] for _ in range(m)], 0
    def _index(self, key):
        h = 0
        for ch in key:
            h = h * 31 + ord(ch)
        return h % len(self.buckets)
    def put(self, key, value):
        bucket = self.buckets[self._index(key)]
        for pair in bucket:
            if pair[0] == key:
                pair[1] = value
                return
        bucket.append([key, value])
        self.n += 1
        if self.n / len(self.buckets) > 0.75:
            pairs = [p for b in self.buckets for p in b]
            self.buckets = [[] for _ in range(2 * len(self.buckets) + 1)]
            for p in pairs:
                self.buckets[self._index(p[0])].append(p)
    def get(self, key):
        for k, v in self.buckets[self._index(key)]:
            if k == key:
                return v

caps = HashMap()
for pair in ['Azerbaijan:Baku', 'Georgia:Tbilisi', 'Japan:Tokyo', 'Italy:Rome', 'Egypt:Cairo']:
    caps.put(*pair.split(':'))
print(caps.get('Azerbaijan'), caps.get('Japan'), caps.get('France'))
print(len(caps.buckets), [len(b) for b in caps.buckets])
▸ Beklenen çıktı
Baku Tokyo None
11 [1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0]
Zincirlemeli küçük bir hash tablosu: 4. anahtar geldiğinde α = 4/5 = 0,8 > 0,75 olur ve tablo 5'ten 11 kovaya büyür. Beş anahtarın hepsi ayrı kovalara düşer, olmayan anahtar için None döner.
YapıAramaEklemeSıra / aralık sorgusu
Sırasız diziO(n)amortize O(1)yok
Sıralı diziO(log n)O(n)var
Dengeli arama ağacıO(log n)O(log n)var
Hash tablosuortalama O(1), en kötü O(n)ortalama O(1)yok
Hash tablosu “var mı?” sorusuna en hızlı yanıtı verir, ama sıra tutmaz: en küçük değer ya da “70'ten 80'e” gibi sorgular için ağaç gerekir.

Önemli noktalar

  • Hash tablosu anahtarı h(k) mod m indeksine çevirir; işlemler ortalamada O(1), en kötü durumda O(n)'dir.
  • Çakışmalar kaçınılmazdır (doğum günü paradoksu): yaklaşık √m anahtardan sonra başlar.
  • Zincirleme kovalarda liste tutar; açık adresleme bir sonraki boş hücreyi arar ve silinen yerleri işaretler.
  • Doluluk oranı α = n/m; α eşiği aşınca tablo büyür ve yeniden hash'lenir (amortize O(1)).
  • Hash tablosu sıra tutmaz: aralık ve en küçük değer sorguları için ağaç seç.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
m = 7, h(k) = k mod 7, doğrusal sondalama. Boş tabloya sırayla 10, 17, 24 ekleniyor. 24 hangi hücreye yerleşir?