Bölüm 14: Hash Tabloları

C ile Veri Yapıları ve Algoritmalar
Doğrudan adreslemeHash fonksiyonlarıÇakışmaDoğrusal / karesel yoklamaÇift hashZincirlemeYük faktörüSilindi işareti
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Fikri açıklamak

Anahtardan konumu hesaplayarak ortalama O(1) arama yapan hash tablosunu tanımlamak.

Fonksiyon seçmek

Bölme, çarpma, katlama ve dizgi hash'ini hesaplamak; iyi fonksiyonun özelliklerini saymak.

Çakışmayı çözmek

Doğrusal/karesel yoklama, çift hash ve zincirlemeyi adım adım uygulamak.

C ile yazmak

Ekleme, arama ve silme yordamlarını malloc/free ve boş durumlarıyla yazmak.

Silmeyi doğru yapmak

Açık adreslemede neden silindi işareti (tombstone) gerektiğini göstermek.

Analiz etmek

Yük faktörünün maliyete etkisini ve yeniden boyutlandırmanın amortize maliyetini açıklamak.

Hash Fikri

Arama maliyetini sabite indirmek

YapıAraEkleSilNot
Sırasız diziO(n)O(1)O(n)sona eklenir, baştan taranır
Sıralı diziO(lg n)O(n)O(n)ikili arama + kaydırma
Dengeli arama ağacıO(lg n)O(lg n)O(lg n)sıralı gezinme de verir
Hash tablosuO(1)*O(1)*O(1)** ortalama; en kötü O(n)
Hash tablosu karşılaştırma yapmaz: anahtardan konumu hesaplar. Bedeli: sıralı gezinme, en küçük/en büyük ve aralık sorguları desteklenmez; en kötü durum O(n)'dir.

Hash Fikri

Önce en basiti: doğrudan adresleme tablosu

Anahtar evreni U = {0, …, 9}, kullanılan K = {2, 3, 5, 8}0122·Ali33·Can455·Ece6788·Naz9T[k] doğrudan k'ye bakar: ara, ekle, sil O(1) — ama tablo |U| kadar büyük olmalı.
typedef struct { int var; char ad[16]; } kayit;
kayit T[10];               /* anahtar = indis */

kayit *da_ara(int k) {
  return T[k].var ? &T[k] : NULL;
}
Sorun

Öğrenci numarası gibi 9 haneli anahtarlarda |U| = 109: tablo sığmaz, çoğu boş kalır.

Çözüm

Anahtarı küçük bir aralığa eşleyen bir hash fonksiyonu h: U → {0, …, m−1}.

Hash Fikri

Anahtar → hash → kova

AliNazCanEceh(k)012345← çakışma: h(Ali) = h(Ece) = 3
TerimAnlamı
kova / yuva (bucket, slot)tablonun bir konumu, 0 … m−1
ev adresih(k): anahtarın ilk bakılan yeri
çakışma (collision)k₁ ≠ k₂ ama h(k₁) = h(k₂)
eş anlamlılar (synonyms)aynı ev adresine düşen anahtarlar
yük faktörü αn / m (dolu oranı)
m < |U| olduğundan (güvercin yuvası ilkesi) çakışma kaçınılmazdır. İki soru kalır: iyi bir h nasıl seçilir, çakışma nasıl çözülür?

Hash Fikri

Soyut veri tipi: sözlük (dictionary)

typedef struct { int anahtar; /* + değer */ } oge;

bool hash_ekle(oge x);      /* varsa günceller */
oge *hash_ara(int anahtar); /* yoksa NULL      */
bool hash_sil(int anahtar); /* yoksa false     */
Sözlük

Anahtar → değer eşlemesi; her anahtar en çok bir kez bulunur.

Kullanım

Derleyicide sembol tablosu (Horowitz'in çıkış örneği), önbellek, tekrar denetimi, küme gerçeklemesi.

C standart kitaplığında hazır bir hash tablosu yoktur (POSIX'te sınırlı hsearch vardır); bu yüzden C programcısı kendi tablosunu yazmayı bilmelidir.

Hash Fonksiyonları

İyi bir hash fonksiyonunun özellikleri

Hızlı

Hesaplaması O(anahtar uzunluğu); birkaç aritmetik işlem.

Düzgün dağıtan

Anahtarlar m kovaya olabildiğince eşit dağılmalı (simple uniform hashing varsayımı).

Tüm bitleri kullanan

Yalnızca son basamaklara bakan fonksiyonlar örüntülü veride kümelenir.

Deterministik

Aynı anahtar her zaman aynı kovaya gider; rastgelelik yalnızca fonksiyon seçilirken olabilir.

Hash Fonksiyonları

Bölme yöntemi: h(k) = k mod m

k1020304050601727
k mod 1000000077
k mod 11109876565
  • m = 10: son basamak dışında her şey yok sayılır; 10, 20, …, 60 aynı kovaya düşer.
  • m için 2'nin kuvvetine yakın olmayan bir asal sayı seçmek iyi bir kuraldır (CLRS).
  • m = 2p ise h(k) yalnızca k'nin en düşük p bitidir: kaçınılmalı.
unsigned h_bolme(unsigned k, unsigned m) { return k % m; }

C'de % negatif sayıda negatif sonuç verebilir: anahtar unsigned tutulur ya da ((k % m) + m) % m kullanılır.

Hash Fonksiyonları

Çarpma yöntemi: h(k) = ⌊m · (k·A mod 1)⌋

  • 0 < A < 1 sabit; k·A'nın kesir kısmı m ile çarpılır.
  • m'nin seçimi kritik değildir; m = 2p seçilebilir.
  • Knuth'un önerisi A ≈ (√5 − 1)/2 = 0,6180339887…
  • Örnek (CLRS): k = 123456, p = 14 (m = 16384) → h(k) = 67.
/* w = 32 bit, m = 2^p; s = A * 2^32 */
uint32_t h_carpma(uint32_t k, int p) {
  const uint32_t s = 2654435769u;
  return (uint32_t)(k * s) >> (32 - p);
}
Tamsayı sürümünde k·s çarpımı 232'ye göre taşar; taşan kısım atılır (kesir kısmını almakla aynı). Kalan 32 bitin en üst p biti h(k)'dir. Kayan noktalı sürüm aynı sonucu verir: ⌊16384 · 0,…⌋ = 67. (<stdint.h> gerekir.)

Hash Fonksiyonları

Katlama (folding) ve orta-kare yöntemleri

Anahtar x = 12320324111220 üç haneli parçalara bölünüp toplanır (Horowitz örneği):

123P1203P2241P3112P420P5kaydırmalı: 123+203+241+112+20 = 69912330224121120sınırda: 123+302+241+211+20 = 897
Kaydırmalı katlama

Parçalar olduğu gibi toplanır: 699.

Sınırda katlama

Çift sıradaki parçalar ters çevrilir (kâğıt katlar gibi): 897.

Orta-kare

k² hesaplanır, ortadaki r bit alınır: m = 2r. Orta bitler k'nin tüm bitlerine bağlıdır.

Sonuç tablo boyutundan büyükse ayrıca mod m alınır.

Hash Fonksiyonları

Dizgi anahtarlar: polinom (Horner) hash

unsigned dizgi_hash(const char *s, unsigned m) {
  unsigned h = 0;
  for (; *s != '\0'; s++)
    h = (h * 31 + (unsigned char)*s) % m;
  return h;
}
unsigned long djb2(const char *s) {  /* Bernstein */
  unsigned long h = 5381;
  int c;
  while ((c = (unsigned char)*s++) != 0)
    h = h * 33 + c;
  return h;       /* sonra: h % m */
}
Toplama kötüdür

Harf kodlarını toplamak "abc", "cba" gibi anagramları aynı kovaya atar.

Polinom

h = c₀·31n−1 + … + cn−1: Horner kuralıyla her karakterde bir çarpma. Konum bilgisi hash'e girer.

Taşma

Her adımda % m almak değerleri küçük tutar; unsigned taşması C'de tanımlıdır (mod 2w).

Dizgi hash'i · kodu adım adım çalıştır

Kod izleme: dizgi_hash("kedi", 11)

k107e101d100i105\0karakterler ve kodlarıh = ?m = 11

dizgi_hash("kedi", 11) çağrılır.

k107e101d100i105\0karakterler ve kodlarıh = 0m = 11

h = 0 ile başlanır.

k107e101d100i105\0karakterler ve kodlarıh = 0m = 11

*s = 'k' ≠ '\0' → döngü sürer.

k107e101d100i105\0karakterler ve kodlarıh = 8m = 11

h = (0·31 + 107) % 11 = 107 % 11 = 8.

k107e101d100i105\0karakterler ve kodlarıh = 8m = 11

*s = 'e' ≠ '\0' → döngü sürer.

k107e101d100i105\0karakterler ve kodlarıh = 8m = 11

h = (8·31 + 101) % 11 = 349 % 11 = 8.

k107e101d100i105\0karakterler ve kodlarıh = 8m = 11

*s = 'd' ≠ '\0' → döngü sürer.

k107e101d100i105\0karakterler ve kodlarıh = 7m = 11

h = (8·31 + 100) % 11 = 348 % 11 = 7.

k107e101d100i105\0karakterler ve kodlarıh = 7m = 11

*s = 'i' ≠ '\0' → döngü sürer.

k107e101d100i105\0karakterler ve kodlarıh = 3m = 11

h = (7·31 + 105) % 11 = 322 % 11 = 3.

k107e101d100i105\0karakterler ve kodlarıh = 3m = 11

*s == '\0': dizgi bitti.

k107e101d100i105\0karakterler ve kodlarıh = 3m = 11

return 3: "kedi" anahtarının ev adresi 3. Maliyet dizgi uzunluğuyla doğrusal.

unsigned dizgi_hash(const char *s, unsigned m) {
  unsigned h = 0;
  for (; *s != '\0'; s++)
    h = (h * 31 + (unsigned char)*s) % m;
  return h;
}

Denetim: tam polinom 3287903 ve 3287903 mod 11 = 3; her adımda mod almak sonucu değiştirmez.

Açık Adresleme

Çakışma: iki temel çözüm yolu

Açık adresleme: tabloda başka yuva aranır0221121331145611 → ev 0 dolu → 1 → 2 → 3Zincirleme: kovada liste tutulur[0][1][2]1122113
Doğum günü paradoksu

23 kişilik bir sınıfta iki kişinin doğum gününün aynı olma olasılığı ≈ %50.7. m = 365 yuvaya 23 anahtar: çakışma şaşırtıcı derecede erken gelir.

Açık adresleme

Tüm öğeler tablonun içinde; α ≤ 1. Yoklama (probe) dizisiyle boş yuva aranır.

Zincirleme

Her kova bir bağlı listenin başı; α > 1 olabilir. Ekleme O(1).

Açık Adresleme

Doğrusal yoklama: h(k, i) = (h(k) + i) mod m

012345678910m = 11, h(k) = k mod 11boş tablo

Sırasıyla 22, 1, 13, 11, 24, 33 eklenecek. Ev adresleri: 0, 1, 2, 0, 2, 0.

02212345678910▲ ievekle(22) · doğrusal1 deneme

22 → ev 0: yuva 0.

022112345678910▲ ievekle(1) · doğrusal1 deneme

1 → ev 1: yuva 1.

02211213345678910▲ ievekle(13) · doğrusal1 deneme

13 → ev 2: yuva 2.

02211213345678910▲ ievekle(11) · doğrusaldeneme 0: yuva 0 dolu

11: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

02211213345678910▲ ev▲ iekle(11) · doğrusaldeneme 1: yuva 1 dolu

11: yuva 1 dolu (1) → sıradaki yuvaya bakılır.

02211213345678910▲ ev▲ iekle(11) · doğrusaldeneme 2: yuva 2 dolu

11: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

0221121331145678910▲ ev▲ iekle(11) · doğrusal4 deneme

11 → ev 0, yoklama dizisi 0, 1, 2, 3: yuva 3.

0221121331145678910▲ ievekle(24) · doğrusaldeneme 0: yuva 2 dolu

24: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

0221121331145678910▲ ev▲ iekle(24) · doğrusaldeneme 1: yuva 3 dolu

24: yuva 3 dolu (11) → sıradaki yuvaya bakılır.

022112133114245678910▲ ev▲ iekle(24) · doğrusal3 deneme

24 → ev 2, yoklama dizisi 2, 3, 4: yuva 4.

022112133114245678910▲ ievekle(33) · doğrusaldeneme 0: yuva 0 dolu

33: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

022112133114245678910▲ ev▲ iekle(33) · doğrusaldeneme 1: yuva 1 dolu

33: yuva 1 dolu (1) → sıradaki yuvaya bakılır.

022112133114245678910▲ ev▲ iekle(33) · doğrusaldeneme 2: yuva 2 dolu

33: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

022112133114245678910▲ ev▲ iekle(33) · doğrusaldeneme 3: yuva 3 dolu

33: yuva 3 dolu (11) → sıradaki yuvaya bakılır.

022112133114245678910▲ ev▲ iekle(33) · doğrusaldeneme 4: yuva 4 dolu

33: yuva 4 dolu (24) → sıradaki yuvaya bakılır.

02211213311424533678910▲ ev▲ iekle(33) · doğrusal6 deneme

33 → ev 0, yoklama dizisi 0, 1, 2, 3, 4, 5: yuva 5.

Birincil kümelenme (primary clustering): dolu yuvalar art arda bir blok oluşturur; bloğa düşen her yeni anahtar bloğu büyütür. 33 için 6 deneme gerekti.

Açık Adresleme

C ile açık adresleme: yuva durumu ve ekleme

#define M 11
typedef enum { BOS, DOLU, SILINDI } durum_t;
typedef struct {
  int anahtar;          /* anahtar >= 0 varsayılır */
  durum_t durum;
} yuva;
yuva tablo[M];          /* global: hepsi 0 = BOS */
Başlangıç

Global dizi sıfırla başlar; BOS = 0 olduğu için tüm yuvalar boştur.

Ekleme

Ev adresinden başlanır, DOLU olmayan ilk yuvaya yazılır; en çok m deneme.

Yuva üç durumdan birindedir: BOS (hiç kullanılmadı), DOLU, SILINDI (silindi işareti). Ekleme SILINDI yuvayı yeniden kullanabilir; ancak aynı anahtarın tabloda zaten olmadığı önce ara ile denetlenmelidir.

Açık adresleme · kodu adım adım çalıştır

Kod izleme: ekle(33) — doğrusal yoklama

022112133114245678910ekle(33)n = 5, α = 5/11

ekle(33): tabloda 22, 1, 13, 11, 24 var (yuva 0–4 dolu blok).

022112133114245678910▲ ievekle(33)

Ev adresi i = 33 % 11 = 0.

022112133114245678910▲ ievekle(33)

d = 0 < 11: 0. deneme, yuva 0.

022112133114245678910▲ ievekle(33)

Yuva 0 DOLU (22) → sonraki: i = (0+1) % 11.

022112133114245678910▲ ev▲ iekle(33)

d = 1 < 11: 1. deneme, yuva 1.

022112133114245678910▲ ev▲ iekle(33)

Yuva 1 DOLU (1) → sonraki: i = (1+1) % 11.

022112133114245678910▲ ev▲ iekle(33)

d = 2 < 11: 2. deneme, yuva 2.

022112133114245678910▲ ev▲ iekle(33)

Yuva 2 DOLU (13) → sonraki: i = (2+1) % 11.

022112133114245678910▲ ev▲ iekle(33)

d = 3 < 11: 3. deneme, yuva 3.

022112133114245678910▲ ev▲ iekle(33)

Yuva 3 DOLU (11) → sonraki: i = (3+1) % 11.

022112133114245678910▲ ev▲ iekle(33)

d = 4 < 11: 4. deneme, yuva 4.

022112133114245678910▲ ev▲ iekle(33)

Yuva 4 DOLU (24) → sonraki: i = (4+1) % 11.

022112133114245678910▲ ev▲ iekle(33)

d = 5 < 11: 5. deneme, yuva 5.

022112133114245678910▲ ev▲ iekle(33)

Yuva 5 BOS → buraya yazılacak.

02211213311424533678910▲ ev▲ iekle(33)n = 6

tablo[5] = {33, DOLU}.

02211213311424533678910ekle(33)n = 6

return 5: 6 yoklama. Birincil kümelenme yüzünden ev adresinden 5 yuva uzağa düştü.

int ekle(int k) {
  int i = k % M;                 /* ev adresi */
  for (int d = 0; d < M; d++, i = (i + 1) % M) {
    if (tablo[i].durum != DOLU) {  /* BOS/SILINDI */
      tablo[i].anahtar = k;
      tablo[i].durum = DOLU;
      return i;
    }
  }
  return -1;                     /* tablo dolu */
}

Açık adresleme · kodu adım adım çalıştır

Kod izleme: ara(24) ve ara(44)

02211213311424533678910ara(24)

ara(24) çağrılır.

02211213311424533678910▲ ievara(24)

Ev adresi i = 24 % 11 = 2.

02211213311424533678910▲ ievara(24)

Yuva 2: başka anahtar (13) → i = 3.

02211213311424533678910▲ ev▲ iara(24)

Yuva 3: başka anahtar (11) → i = 4.

02211213311424533678910▲ ev▲ iara(24)3 yoklama

Yuva 4 DOLU ve anahtar 24 → return 4 (3 yoklama).

02211213311424533678910ara(44)

ara(44) çağrılır.

02211213311424533678910▲ ievara(44)

Ev adresi i = 44 % 11 = 0.

02211213311424533678910▲ ievara(44)

Yuva 0: başka anahtar (22) → i = 1.

02211213311424533678910▲ ev▲ iara(44)

Yuva 1: başka anahtar (1) → i = 2.

02211213311424533678910▲ ev▲ iara(44)

Yuva 2: başka anahtar (13) → i = 3.

02211213311424533678910▲ ev▲ iara(44)

Yuva 3: başka anahtar (11) → i = 4.

02211213311424533678910▲ ev▲ iara(44)

Yuva 4: başka anahtar (24) → i = 5.

02211213311424533678910▲ ev▲ iara(44)

Yuva 5: başka anahtar (33) → i = 6.

02211213311424533678910▲ ev▲ iara(44)7 yoklama

Yuva 6 BOS: 44 olsaydı en geç burada olurdu → return −1 (yok).

int ara(int k) {
  int i = k % M;
  for (int d = 0; d < M; d++, i = (i + 1) % M) {
    if (tablo[i].durum == BOS) return -1;
    if (tablo[i].durum == DOLU &&
        tablo[i].anahtar == k) return i;
  }
  return -1;
}
bool sil(int k) {
  int i = ara(k);
  if (i < 0) return false;
  tablo[i].durum = SILINDI;   /* silindi işareti */
  return true;
}

Açık Adresleme

Karesel yoklama: h(k, i) = (h(k) + i²) mod m

012345678910m = 11, h(k) = k mod 11boş tablo

Aynı anahtarlar: 22, 1, 13, 11, 24, 33. Denemeler ev adresinden 1, 4, 9, 16… uzağa sıçrar.

02212345678910▲ ievekle(22) · karesel1 deneme

22 → ev 0: yuva 0.

022112345678910▲ ievekle(1) · karesel1 deneme

1 → ev 1: yuva 1.

02211213345678910▲ ievekle(13) · karesel1 deneme

13 → ev 2: yuva 2.

02211213345678910▲ ievekle(11) · kareseldeneme 0: yuva 0 dolu

11: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

02211213345678910▲ ev▲ iekle(11) · kareseldeneme 1: yuva 1 dolu

11: yuva 1 dolu (1) → sıradaki yuvaya bakılır.

0221121334115678910▲ ev▲ iekle(11) · karesel3 deneme

11 → ev 0, yoklama dizisi 0, 1, 4: yuva 4.

0221121334115678910▲ ievekle(24) · kareseldeneme 0: yuva 2 dolu

24: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

022112133244115678910▲ ev▲ iekle(24) · karesel2 deneme

24 → ev 2, yoklama dizisi 2, 3: yuva 3.

022112133244115678910▲ ievekle(33) · kareseldeneme 0: yuva 0 dolu

33: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

022112133244115678910▲ ev▲ iekle(33) · kareseldeneme 1: yuva 1 dolu

33: yuva 1 dolu (1) → sıradaki yuvaya bakılır.

022112133244115678910▲ ev▲ iekle(33) · kareseldeneme 2: yuva 4 dolu

33: yuva 4 dolu (11) → sıradaki yuvaya bakılır.

02211213324411567893310▲ ev▲ iekle(33) · karesel4 deneme

33 → ev 0, yoklama dizisi 0, 1, 4, 9: yuva 9.

Birincil kümelenme kalkar; ev adresi aynı olanlar yine aynı diziyi izler (ikincil kümelenme). m asal ve α ≤ 1/2 ise boş yuva bulunması garantidir.

Açık Adresleme

Çift hash: h(k, i) = (h₁(k) + i · h₂(k)) mod m

012345678910h₁ = k mod 11, h₂ = 1 + (k mod 10)boş tablo

Adım boyu anahtara göre değişir: 11 → h₂ = 2, 24 → h₂ = 5, 33 → h₂ = 4.

02212345678910▲ ievekle(22) · çift hash1 deneme

22 → ev 0: yuva 0.

022112345678910▲ ievekle(1) · çift hash1 deneme

1 → ev 1: yuva 1.

02211213345678910▲ ievekle(13) · çift hash1 deneme

13 → ev 2: yuva 2.

02211213345678910▲ ievekle(11) · çift hashdeneme 0: yuva 0 dolu

11: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

02211213345678910▲ ev▲ iekle(11) · çift hashdeneme 1: yuva 2 dolu

11: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

0221121334115678910▲ ev▲ iekle(11) · çift hash3 deneme

11 → ev 0, yoklama dizisi 0, 2, 4: yuva 4.

0221121334115678910▲ ievekle(24) · çift hashdeneme 0: yuva 2 dolu

24: yuva 2 dolu (13) → sıradaki yuvaya bakılır.

022112133411567248910▲ ev▲ iekle(24) · çift hash2 deneme

24 → ev 2, yoklama dizisi 2, 7: yuva 7.

022112133411567248910▲ ievekle(33) · çift hashdeneme 0: yuva 0 dolu

33: yuva 0 dolu (22) → sıradaki yuvaya bakılır.

022112133411567248910▲ ev▲ iekle(33) · çift hashdeneme 1: yuva 4 dolu

33: yuva 4 dolu (11) → sıradaki yuvaya bakılır.

02211213341156724833910▲ ev▲ iekle(33) · çift hash3 deneme

33 → ev 0, yoklama dizisi 0, 4, 8: yuva 8.

h₂(k) hiçbir zaman 0 olmamalı ve m ile aralarında asal olmalı (m asal seçilirse kendiliğinden sağlanır). Yoklama dizisi anahtara özgü olduğundan kümelenme en azdır.

Açık Adresleme

Aynı tablo, üç yoklama: 33 nereye düşer?

Yöntemi. deneme33'ün yoklama dizisiDeneme
Doğrusalh + i0, 1, 2, 3, 4, 56
Kareselh + i²0, 1, 4, 94
Çift hashh₁ + i·h₂0, 4, 83
YöntemKümelenmeFarklı yoklama dizisi sayısı
Doğrusalbirincil (bloklar)m
Kareselikincil (aynı ev adresi)m
Çift hashen az≈ m²
Doğrusal yoklama önbellek (cache) dostudur ve α küçükken çok hızlıdır; α büyüdükçe kümelenme yüzünden maliyet hızla artar.

Zincirleme

Zincirleme (separate chaining): kova başına bir liste

#define MZ 5
typedef struct dugum {
  int anahtar;
  struct dugum *sonraki;
} dugum;
dugum *kova[MZ];           /* hepsi NULL */
bool zincir_ekle(int k) {
  int i = k % MZ;
  dugum *yeni = malloc(sizeof *yeni);
  if (yeni == NULL) return false;  /* bellek yok */
  yeni->anahtar = k;
  yeni->sonraki = kova[i];         /* başa ekle */
  kova[i] = yeni;
  return true;
}
Başa ekle

Liste sırası önemsiz: başa ekleme O(1). (Tekrar istenmiyorsa önce aranmalı.)

Arama

Yalnızca kendi kovasının listesinde gezinilir: ortalama 1 + α/2 düğüm.

Silme kolay

Bağlı listeden silmek yeter; silindi işareti gerekmez.

Zincirleme · kodu adım adım çalıştır

Kod izleme: zincir_ekle 12, 7, 22 (hepsi kova 2)

ekle(12)kova[0]10∅kova[1]∅kova[2]∅kova[3]∅kova[4]4∅

zincir_ekle(12).

ekle(12)kova[0]10∅kova[1]∅kova[2]∅kova[3]∅kova[4]4∅

i = 12 % 5 = 2.

ekle(12)kova[0]10∅kova[1]∅kova[2]∅kova[3]∅kova[4]4∅?yeni (malloc)

Yeni düğüm için bellek alınır; NULL değil, devam.

ekle(12)kova[0]10∅kova[1]∅kova[2]∅kova[3]∅kova[4]4∅12yeni (malloc)

anahtar = 12; sonraki = kova[2] (NULL): yeni düğüm eski başı gösterir.

ekle(12)kova[0]10∅kova[1]∅kova[2]12∅kova[3]∅kova[4]4∅

kova[2] = yeni: 12 listenin başında. O(1).

ekle(7)kova[0]10∅kova[1]∅kova[2]12∅kova[3]∅kova[4]4∅

zincir_ekle(7).

ekle(7)kova[0]10∅kova[1]∅kova[2]12∅kova[3]∅kova[4]4∅

i = 7 % 5 = 2.

ekle(7)kova[0]10∅kova[1]∅kova[2]12∅kova[3]∅kova[4]4∅?yeni (malloc)

Yeni düğüm için bellek alınır; NULL değil, devam.

ekle(7)kova[0]10∅kova[1]∅kova[2]12∅kova[3]∅kova[4]4∅7yeni (malloc)

anahtar = 7; sonraki = kova[2] (&düğüm(12)): yeni düğüm eski başı gösterir.

ekle(7)kova[0]10∅kova[1]∅kova[2]712∅kova[3]∅kova[4]4∅

kova[2] = yeni: 7 listenin başında. O(1).

ekle(22)kova[0]10∅kova[1]∅kova[2]712∅kova[3]∅kova[4]4∅

zincir_ekle(22).

ekle(22)kova[0]10∅kova[1]∅kova[2]712∅kova[3]∅kova[4]4∅

i = 22 % 5 = 2.

ekle(22)kova[0]10∅kova[1]∅kova[2]712∅kova[3]∅kova[4]4∅?yeni (malloc)

Yeni düğüm için bellek alınır; NULL değil, devam.

ekle(22)kova[0]10∅kova[1]∅kova[2]712∅kova[3]∅kova[4]4∅22yeni (malloc)

anahtar = 22; sonraki = kova[2] (&düğüm(7)): yeni düğüm eski başı gösterir.

ekle(22)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅

kova[2] = yeni: 22 listenin başında. O(1).

bool zincir_ekle(int k) {
  int i = k % MZ;
  dugum *yeni = malloc(sizeof *yeni);
  if (yeni == NULL) return false;  /* bellek yok */
  yeni->anahtar = k;
  yeni->sonraki = kova[i];         /* başa ekle */
  kova[i] = yeni;
  return true;
}

Zincirleme · kodu adım adım çalıştır

Kod izleme: zincir_ara(12) ve zincir_ara(17)

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅

zincir_ara(12): kova 12 % 5 = 2.

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = kova[2]: listenin başı (22).

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar = 22 ≠ 12 → ilerlenir.

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = p->sonraki → düğüm(7).

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar = 7 ≠ 12 → ilerlenir.

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = p->sonraki → düğüm(12).

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar == 12: döngü koşulu yanlış, durulur.

ara(12)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

return p: bulundu (3 düğüm gezildi).

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅

zincir_ara(17): kova 17 % 5 = 2.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = kova[2]: listenin başı (22).

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar = 22 ≠ 17 → ilerlenir.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = p->sonraki → düğüm(7).

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar = 7 ≠ 17 → ilerlenir.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p = p->sonraki → düğüm(12).

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p->anahtar = 12 ≠ 17 → ilerlenir.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅◄ p = NULL

p = p->sonraki → NULL.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅◄ p = NULL

p == NULL: liste bitti, döngüden çıkılır.

ara(17)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅◄ p = NULL

return NULL: 17 tabloda yok. Tüm zincir (3 düğüm) tarandı.

dugum *zincir_ara(int k) {
  dugum *p = kova[k % MZ];
  while (p != NULL && p->anahtar != k)
    p = p->sonraki;
  return p;                 /* NULL: yok */
}

Zincirleme · kodu adım adım çalıştır

Kod izleme: zincir_sil(7)

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅

zincir_sil(7): 7 zincirin ortasında (22 → 7 → 12).

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅

i = 7 % 5 = 2.

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

p listenin başında, onceki = NULL.

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅p

22 ≠ 7 → döngüye girilir.

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅oncekip

onceki = 22'nin düğümü, p = 7'nin düğümü.

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅oncekip

p->anahtar == 7 → döngü biter.

sil(7)kova[0]10∅kova[1]∅kova[2]22712∅kova[3]∅kova[4]4∅oncekip

p NULL değil: anahtar bulundu.

sil(7)kova[0]10∅kova[1]∅kova[2]2212∅kova[3]∅kova[4]4∅7free()

onceki NULL değil → onceki->sonraki = p->sonraki: 22 artık doğrudan 12'yi gösterir; 7 zincirden çıktı.

sil(7)kova[0]10∅kova[1]∅kova[2]2212∅kova[3]∅kova[4]4∅7free()

free(p): düğümün belleği geri verilir. Bundan sonra p kullanılmamalı (sarkan işaretçi).

sil(7)kova[0]10∅kova[1]∅kova[2]2212∅kova[3]∅kova[4]4∅

return true. Baştaki düğüm silinseydi (onceki == NULL) kova[i] güncellenirdi.

bool zincir_sil(int k) {
  int i = k % MZ;
  dugum *p = kova[i], *onceki = NULL;
  while (p != NULL && p->anahtar != k) {
    onceki = p;
    p = p->sonraki;
  }
  if (p == NULL) return false;           /* yok */
  if (onceki == NULL) kova[i] = p->sonraki;
  else onceki->sonraki = p->sonraki;
  free(p);
  return true;
}

Yük Faktörü ve Silme

Silmede tuzak: yuvayı boşaltmak aramayı keser

02211213311424533678910başlangıç22, 1, 13, 11, 24, 33

13'ü sileceğiz. 24'ün ev adresi 2; 24, 2 ve 3 dolu olduğu için 4'e yerleşmişti.

022112311424533678910yanlış: yuva 2 = BOS

Yuvayı BOS yaparsak…

022112311424533678910▲ ievara(24) → yuva 2 BOS−1 (yanlış!)

…ara(24) ev adresinde boş yuva görür ve “yok” der. Oysa 24 tabloda!

022112✝311424533678910doğru: yuva 2 = SILINDI (✝)

Doğrusu yuvayı silindi işareti (SILINDI) ile işaretlemektir.

022112✝311424533678910▲ ev▲ iara(24): 2 ✝ → 3 → 4bulundu

Arama silindi işaretini boş saymaz, devam eder; ekleme ise silindi işaretini yeniden kullanabilir.

Çok silme yapılan tablolarda silindi işaretleri birikir ve aramalar uzar: periyodik olarak yeniden hash'lemek (temizlik) gerekir. Zincirlemede bu sorun yoktur.

Silindi işareti · kodu adım adım çalıştır

Kod izleme: sil(13), ardından ara(24)

02211213311424533678910sil(13)

sil(13) çağrılır; önce yeri bulunmalı.

02211213311424533678910▲ ievara(13)

Ev adresi i = 13 % 11 = 2.

02211213311424533678910▲ ievara(13)1 yoklama

Yuva 2 DOLU ve anahtar 13 → return 2 (1 yoklama).

02211213311424533678910▲ isil(13)

ara 2 döndürdü; i ≥ 0, silinecek öğe var.

022112✝311424533678910sil(13)

tablo[2].durum = SILINDI: silindi işareti.

022112✝311424533678910sil(13)

return true.

022112✝311424533678910▲ ievara(24)

Ev adresi i = 24 % 11 = 2.

022112✝311424533678910▲ ievara(24)

Yuva 2: silindi işareti: silinmiş, aramaya devam → i = 3.

022112✝311424533678910▲ ev▲ iara(24)

Yuva 3: başka anahtar (11) → i = 4.

022112✝311424533678910▲ ev▲ iara(24)3 yoklama

Yuva 4 DOLU ve anahtar 24 → return 4 (3 yoklama).

int ara(int k) {
  int i = k % M;
  for (int d = 0; d < M; d++, i = (i + 1) % M) {
    if (tablo[i].durum == BOS) return -1;
    if (tablo[i].durum == DOLU &&
        tablo[i].anahtar == k) return i;
  }
  return -1;
}
bool sil(int k) {
  int i = ara(k);
  if (i < 0) return false;
  tablo[i].durum = SILINDI;   /* silindi işareti */
  return true;
}

Yük Faktörü ve Silme

Yük faktörü α = n/m ve beklenen yoklama sayısı

αZincir ✓Zincir ✗Doğrusal ✓Doğrusal ✗Düzgün* ✓Düzgün* ✗
0,251,121,251,21,41,151,3
0,51,251,501,52,51,392,0
0,751,381,752,58,51,854,0
0,91,451,905,550,52,5610,0

✓ başarılı, ✗ başarısız arama. Formüller: zincirleme 1 + α/2 ve 1 + α (Θ(1 + α)); doğrusal yoklama (Knuth) ½(1 + 1/(1−α)) ve ½(1 + 1/(1−α)²); * düzgün hash varsayımıyla açık adresleme (1/α)·ln(1/(1−α)) ve 1/(1−α) (CLRS).

α sabit tutulursa tüm işlemler ortalama O(1)'dir. Pratik eşik: açık adreslemede α ≤ 0,5–0,7, zincirlemede α ≤ 1 civarı. Eşik aşılınca tablo büyütülür.

Yük Faktörü ve Silme

Yeniden boyutlandırma (rehashing)

typedef struct {
  yuva *a;      /* m yuvalık dinamik dizi */
  int m, n;     /* boyut, dolu yuva sayısı */
} Tablo;
/* ekledikten sonra: */
if (2 * t->n > t->m)             /* α > 1/2 */
  yeniden_boyutla(t, sonraki_asal(2 * t->m));
Neden yeniden hash?

h(k) = k mod m, m'ye bağlıdır: öğeler yeni tabloda başka yuvalara düşer; kopyalamak yetmez.

Amortize O(1)

Boyut her seferinde iki katına çıkınca n ekleme için toplam kopyalama ≤ 2n: ekleme başına sabit.

Silindi işaretleri

Yeniden hash'leme silindi işaretlerini de temizler.

Yeniden boyutlandırma · kodu adım adım çalıştır

Kod izleme: yeniden_boyutla(t, 11)

eski (m = 5)012731243α = 0,6

n = 3, m = 5 → α = 0,6 > 0,5: tablo 11 yuvaya büyütülecek (≈ 2m, asal).

eski (m = 5)012731243

Eski dizinin adresi ve boyutu saklanır.

eski (m = 5)012731243yeni (m = 11)012345678910

calloc sıfırlanmış 11 yuva verir: hepsi BOS (= 0).

eski (m = 5)012731243yeni (m = 11)012345678910

Bellek alındı; tablo yeni diziye bağlanır, n = 0.

eski (m = 5)012731243▲ iyeni (m = 11)012345678910

eski[0] BOS: atlanır.

eski (m = 5)012731243▲ iyeni (m = 11)012345678910

eski[1] BOS: atlanır.

eski (m = 5)012731243▲ iyeni (m = 11)0123456778910

eski[2] = 7 DOLU → yeniden hash: 7 % 11 = 7 → yuva 7. (Eski ev adresi 7 % 5 = 2 idi.)

eski (m = 5)012731243▲ iyeni (m = 11)011223456778910

eski[3] = 12 DOLU → yeniden hash: 12 % 11 = 1 → yuva 1. (Eski ev adresi 12 % 5 = 2 idi.)

eski (m = 5)012731243▲ iyeni (m = 11)0112233456778910

eski[4] = 3 DOLU → yeniden hash: 3 % 11 = 3 → yuva 3. (Eski ev adresi 3 % 5 = 3 idi.)

eski (m = 5) — free()012731243yeni (m = 11)0112233456778910

Eski dizi free edilir. Yeni α = 3/11 ≈ 0,27.

eski (m = 5) — free()012731243yeni (m = 11)0112233456778910α ≈ 0,27

return true. Tüm öğeler yeniden eklendi: O(n + m).

bool yeniden_boyutla(Tablo *t, int yeni_m) {
  yuva *eski = t->a;
  int eski_m = t->m;
  yuva *a = calloc(yeni_m, sizeof *a);  /* BOS */
  if (a == NULL) return false;
  t->a = a; t->m = yeni_m; t->n = 0;
  for (int i = 0; i < eski_m; i++)
    if (eski[i].durum == DOLU)
      tablo_ekle(t, eski[i].anahtar);
  free(eski);
  return true;
}

Özet

Zincirleme mi, açık adresleme mi?

Zincirleme

  • α > 1 olabilir; dolmaz, yavaşlar.
  • Silme basit, silindi işareti yok.
  • Kötü hash'e karşı daha dayanıklı.
  • Düğüm başına işaretçi + malloc maliyeti; önbellek dostu değil.

Açık adresleme

  • Ek işaretçi yok; bellek bitişik, önbellek dostu.
  • α ≤ 1; α → 1 iken maliyet patlar.
  • Silme için silindi işareti gerekir.
  • Kümelenmeye karşı iyi yoklama (çift hash) seçilmeli.
Kural: öğe sayısı öngörülemiyorsa ve çok silme varsa zincirleme; bellek ve hız kritikse, silme az ve α düşük tutulabiliyorsa açık adresleme.

Özet

Karmaşıklık özeti

İşlemOrtalama (α sabit)En kötüNot
araO(1)O(n)tüm anahtarlar aynı kovaya düşerse
ekleO(1)O(n)zincirde başa ekleme her zaman O(1)
silO(1)O(n)açık adreslemede silindi işareti
yeniden boyutlandırO(n + m)O(n + m)ekleme başına amortize O(1)
sıralı gezinme / min / maxO(n + m)O(n lg n)hash sıra tutmaz
En kötü durum kötü niyetli girdilerle tetiklenebilir (hash flooding). Önlem: fonksiyonu çalışma anında rastgele bir aileden seçmek — evrensel hash (universal hashing, CLRS 11.3.3).

Özet

Sık yapılan hatalar

1Negatif anahtarda k % m'nin negatif çıkıp dizi dışına yazması.
2m'yi 10 ya da 2'nin kuvveti seçip yalnızca son basamaklara/bitlere bakmak.
3Açık adreslemede silinen yuvayı BOS yapmak: aramalar yarıda kalır.
4Tablo doluyken sonsuz döngüye girmek: deneme sayısı m ile sınırlanmalı.
5malloc sonucunu denetlememek; free sonrası düğümü kullanmak.
6Yeniden boyutlandırmada öğeleri eski indisleriyle kopyalamak (yeniden hash'lemeden).

Özet

Alıştırmalar

  1. m = 7, h(k) = k mod 7, doğrusal yoklama: 10, 17, 24, 3 sırayla eklenince tablo ne olur?
  2. Aynı anahtarları zincirleme (başa ekleme) ile yerleştirin; en uzun zincir kaç düğümdür?
  3. m = 11 ve karesel yoklamada 0, 1, 4, 9, 16, 25 (mod 11) yoklama dizisini yazın.
  4. α = 0,9 olan doğrusal yoklamalı tabloda başarısız arama kaç yoklama bekler?
  5. "ab" ve "ba" için toplama hash'i ve 31 tabanlı polinom hash'i (mod 11) hesaplayın.
  6. Açık adreslemede ekle, SILINDI yuvaya yazmadan önce neden ara çağırmalıdır?
Cevapları göster
  1. 10 → 3, 17 → 3 dolu → 4, 24 → 3, 4 dolu → 5, 3 → 3, 4, 5 dolu → 6: yuva 3–6 = 10, 17, 24, 3.
  2. Hepsi kova 3'te: 3 → 24 → 17 → 10 (4 düğüm). Kötü hash seçimi zinciri uzatır.
  3. 0, 1, 4, 9, 5, 3 (16 mod 11 = 5, 25 mod 11 = 3).
  4. ½(1 + 1/0,01) = 50,5 yoklama: α → 1 iken maliyet patlar.
  5. Toplama: 97 + 98 = 195 → ikisi de 195 % 11 = 8. Polinom: "ab" = 97·31 + 98 = 3105 → 3; "ba" = 98·31 + 97 = 3135 → 0.
  6. Aynı anahtar silindi işaretinden sonraki bir yuvada zaten olabilir; kontrol edilmezse tabloya iki kez girer.

Özet

Bölüm özeti

Fikir

Anahtardan konum hesaplanır: ortalama O(1) ara/ekle/sil.

Hash fonksiyonu

Bölme (m asal), çarpma (A ≈ 0,618), katlama, polinom dizgi hash'i.

Açık adresleme

Doğrusal, karesel, çift hash; kümelenme ve silindi işareti.

Zincirleme

Kova başına bağlı liste; başa ekleme O(1), silme kolay.

Yük faktörü

α = n/m küçük tutulmalı; eşik aşılınca yeniden boyutlandırılır.

Sınırlar

Sıra bilgisi yok; en kötü O(n); evrensel hash ile koruma.

Özet

Kaynaklar ve devam

  • E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, Bölüm 8 (Hashing).
  • T. H. Cormen vd., Introduction to Algorithms (CLRS), Bölüm 11 (Hash Tables).
  • R. Sedgewick, Algorithms in C, Kısım 1–4, Bölüm 14 (Hashing).
  • D. E. Knuth, The Art of Computer Programming, Cilt 3, Bölüm 6.4.

Son

Bölüm 14: Hash Tabloları
Sonraki bölüm: Bölüm 15: Çizgeler · Ders sayfası
1 / 1 C ile Veri Yapıları