SEN2212 · 9. hafta · Analyzing and implementing hashing algorithms.

Hashing: zincirleme, açık adresleme ve rehash

Hash fonksiyonu, doluluk oranı, ayrı zincirleme, doğrusal/karesel yoklama, çift hashing, tembel silme ve rehash; sınavın klasik 'tabloyu doldur' sorularının adım adım çözümü.

hash tablehash functioncollisionload factorseparate chainingopen addressinglinear probingprimary clusteringquadratic probingsecondary clusteringdouble hashinglazy deletionrehashinghashCode

Hashing, “bu anahtar var mı?” sorusunu ortalama sabit zamanda cevaplamanın yoludur. BST bunu O(log⁡n)O(\log n)‘de yapar, hash tablosu O(1)O(1)‘de. Java’nın HashMap ve HashSet sınıfları bunun üzerine kurulu. Derleyicinin sembol tablosu, önbellekler, veritabanı indeksleri hep hash kullanır.

Sınavda bu haftadan en sık gelen soru: “Şu anahtarları şu tabloya şu yöntemle ekle, son tabloyu yaz.” Yöntemi doğru uygulayan tam puan alır. Altın kurallar:

  1. Ev hücresi h(k)=k mod Mh(k) = k \bmod M. Önce bunu her anahtar için hesapla, kenara yaz.
  2. Çarpışma olursa yöntemin formülüne göre sıradaki hücreye bak. Formülde hep ev hücresinden başlarsın, son denenen hücreden değil.
  3. Her şey mod M. Tablonun sonundan başa sararsın.
  4. Açık adreslemede silme tembel yapılır.
  5. λ eşiği aşılınca rehash: yeni boyut, yeni ev hücreleri.

Temel kavramlar

Hash fonksiyonu (hash function): anahtarı 0 ile M-1 arasında bir indekse çevirir. İyi bir hash fonksiyonu hızlı hesaplanır ve anahtarları tabloya eşit dağıtır. Tam sayı anahtarlarda standart seçim k mod Mk \bmod M, M asal.

Neden asal? M = 10 seçersen sadece son basamak önemli olur; anahtarların çoğu 0 ile bitiyorsa hepsi aynı hücreye düşer. Asal M anahtarın bütün basamaklarını karıştırır.

String’ler için Java şu formülü kullanır (Horner yöntemiyle hesaplanır):

h(s)=s0⋅31n−1+s1⋅31n−2+⋯+sn−1h(s) = s_0 \cdot 31^{n-1} + s_1 \cdot 31^{n-2} + \dots + s_{n-1}

System.out.println("ab".hashCode());   // 97*31 + 98
System.out.println("cat".hashCode());
System.out.println("act".hashCode());
3105
98262
96402

Karakter kodlarını sadece toplasaydın “cat” ve “act” ikisi de 312 verirdi. 31 ile çarpmak harflerin sırasını hesaba katar.

TUZAK · negatif hashCode. Uzun String’lerde toplam int’e sığmaz ve taşar, hashCode() negatif çıkabilir. h % M de negatif olur ve dizi indeksi patlar. Math.abs(h) % M bile güvenli değil: Math.abs(Integer.MIN_VALUE) yine negatiftir. Doğrusu Math.floorMod(h, M).

Çarpışma (collision): iki farklı anahtar aynı hücreye düşer. M, olası anahtar sayısından küçük olduğu için çarpışma kaçınılmazdır. Mesele onu nasıl çözdüğün.

Doluluk oranı (load factor): λ=n/M\lambda = n / M. Performansı belirleyen tek sayı budur.

Bu sayfanın bütün örneklerinde aynı anahtarları kullanacağız:

M = 11
anahtar  27  18  29  28  39  13  16
k mod 11  5   7   7   6   6   2   5

Ayrı zincirleme (separate chaining)

Her hücre bir bağlı liste tutar. Aynı hücreye düşen anahtarlar o listeye eklenir. Biz yeni anahtarı listenin başına ekliyoruz (O(1)O(1)).

0: -
1: -
2: 13
3: -
4: -
5: 16 -> 27
6: 39 -> 28
7: 29 -> 18
8: -
9: -
10: -

Arama: ev hücresine git, o listeyi tara. Ortalama liste uzunluğu λ olduğu için başarısız arama ortalama λ, başarılı arama yaklaşık 1+λ/21 + \lambda/2 eleman inceler. Zincirlemede λ = 1 civarı makul; λ > 1 de çalışır ama yavaşlar. Silme kolaydır: listeden çıkar.

Açık adresleme (open addressing)

Liste yok, anahtarlar doğrudan dizide durur. Çarpışmada bir yoklama dizisi (probe sequence) ile boş hücre aranır: h0,h1,h2,…h_0, h_1, h_2, \dots Burada λ≤1\lambda \le 1 zorunlu, pratikte λ≤0.5\lambda \le 0.5 hedeflenir.

Doğrusal yoklama (linear probing)

hi(k)=(h(k)+i) mod Mh_i(k) = (h(k) + i) \bmod M

Dolu ise bir sağa bak. Örnek tablonun iz kaydı (parantez içi: incelenen hücre sayısı):

27 -> 5                    (1)
18 -> 7                    (1)
29 -> 7x 8                 (2)
28 -> 6                    (1)
39 -> 6x 7x 8x 9           (4)
13 -> 2                    (1)
16 -> 5x 6x 7x 8x 9x 10    (6)

idx: 0  1  2  3  4  5  6  7  8  9  10
key: -  -  13 -  -  27 28 18 29 39 16

5’ten 10’a kadar tek bir dolu blok oluştu. Bloğa düşen her yeni anahtar sonuna eklenir ve blok daha da büyür. Buna birincil kümelenme (primary clustering) denir. Doğrusal yoklamanın en büyük zayıflığı budur: 16’nın ev hücresi 5’ti ama 6 hücre gezdi.

Beklenen yoklama sayıları:

λBaşarılı arama 12(1+11−λ)\tfrac12(1+\tfrac{1}{1-\lambda})Başarısız 12(1+1(1−λ)2)\tfrac12(1+\tfrac{1}{(1-\lambda)^2})
0.501.502.50
0.752.508.50
0.905.5050.50

λ 0.9’a çıkınca başarısız arama ortalama 50 hücre gezer. Rehash eşiği bu yüzden var.

Karesel yoklama (quadratic probing)

hi(k)=(h(k)+i2) mod Mh_i(k) = (h(k) + i^2) \bmod M

Sırayla ev + 0, +1, +4, +9, +16, … Kümeden hızla uzaklaşır.

27 -> 5
18 -> 7
29 -> 7x 8          (7+1)
28 -> 6
39 -> 6x 7x 10      (6+1, 6+4)
13 -> 2
16 -> 5x 6x 9       (5+1, 5+4)

idx: 0  1  2  3  4  5  6  7  8  9  10
key: -  -  13 -  -  27 28 18 29 16 39

16 bu sefer 3 yoklamada yerleşti. Birincil kümelenme gider ama aynı ev hücresine düşen anahtarlar yine aynı yolu izler: ikincil kümelenme (secondary clustering).

TUZAK · karesel yoklamada i² ev hücresine eklenir. 39 için ikinci deneme 7, üçüncü deneme 7 + 4 = 11 değil, 6 + 4 = 10. Her deneme h(k)+i2h(k) + i^2, bir önceki denemeye eklenmez.

Neden M asal ve λ ≤ 0.5? M asal değilse i2 mod Mi^2 \bmod M az sayıda farklı değer alır. M = 16 için ev hücresi 0’dan ulaşılabilen hücreler sadece {0, 1, 4, 9}. Tablo yarı boşken bile yer bulunamayabilir. M asal ise ilk ⌈M/2⌉\lceil M/2 \rceil deneme hep farklı hücrelere düşer; tablo en az yarı boşsa boş bir hücreye mutlaka varılır.

Çift hashing (double hashing)

hi(k)=(h(k)+i⋅h2(k)) mod M,h2(k)=R−(k mod R)h_i(k) = (h(k) + i \cdot h_2(k)) \bmod M, \qquad h_2(k) = R - (k \bmod R)

R, M’den küçük bir asal. Adım büyüklüğü anahtara bağlı olduğu için aynı ev hücresine düşen iki anahtar farklı yollar izler; ikincil kümelenme de gider. h2h_2 asla 0 olmamalı (yoksa yerinde sayar); R−(k mod R)R - (k \bmod R) 1 ile R arasında olduğu için güvenli.

R = 7 ile:

key  h  h2  yol
27   5  1   5
18   7  3   7
29   7  6   7x 2      (7+6=13, mod 11 = 2)
28   6  7   6
39   6  3   6x 9
13   2  1   2x 3      (2'de 29 var)
16   5  5   5x 10

idx: 0  1  2  3  4  5  6  7  8  9  10
key: -  -  29 13 -  27 28 18 -  39 16

Dikkat: 29’un 2’ye yerleşmesi, daha sonra ev hücresi 2 olan 13’ü itti. Sırayı karıştırma; anahtarları verilen sırayla ekle.

Silme: tembel silme (lazy deletion)

Doğrusal tabloda 29’u (indeks 8) silmek istiyoruz. Hücreyi boşaltırsak 39’u aramak bozulur: 39’un yolu 6 → 7 → 8 → 9. Arama 8’de boş hücre görür, “39 yok” der ve durur. Oysa 39 indeks 9’da duruyor.

Çözüm: hücreye DELETED işareti koymak.

idx: 0  1  2  3  4  5  6  7  8  9  10
key: -  -  13 -  -  27 28 18 D  39 16
  • Arama DELETED’i dolu gibi geçer: 39 için 6, 7, 8, 9 → 4 hücre, bulunur.
  • Ekleme DELETED’i boş gibi kullanabilir: 50 (h = 6) için 6, 7 dolu, 8 DELETED → 50 oraya yazılır.

Çok silme yapılırsa DELETED hücreler birikir ve aramalar uzar; rehash bunları da temizler. Zincirlemede bu sorun yok, düğüm listeden çıkarılır.

Rehashing

λ eşiği aşınca daha büyük bir tablo açılır (genelde 2M’den büyük ilk asal) ve her anahtar yeni M ile yeniden hash’lenir. Eski indeksleri kopyalamak yanlıştır, çünkü k mod Mk \bmod M değişti.

M = 7, doğrusal yoklama, ekleme sonrası λ > 0.5 ise rehash:

insert 10  λ=0.143  3:10
insert 3   λ=0.286  3:10 4:3
insert 17  λ=0.429  3:10 4:3 5:17
insert 24  λ=0.571  3:10 4:3 5:17 6:24  -> rehash
yeni M = ilk asal >= 14 = 17
10 -> 10   3 -> 3   17 -> 0   24 -> 7
0:17 3:3 7:24 10:10

Eski tabloda dört anahtar 3-6 arasında bir küme oluşturmuştu; yeni tabloda hiç çarpışma yok. Rehash O(n)O(n) sürer ama nadiren olur. Her seferinde boyut ikiye katlandığı için ekleme başına amortize maliyet O(1)O(1) kalır. Java’nın HashMap’i varsayılan olarak λ > 0.75 olunca kapasiteyi ikiye katlar.

Hangi yöntem ne zaman?

YöntemKümelenmeλ sınırıSilmeNot
Ayrı zincirlemeyok> 1 olabilirkolayek bellek: liste düğümleri
Doğrusalbirincil≤ 0.5 önerilirtembelönbellek dostu, basit
Kareselikincil≤ 0.5, M asaltembelyer garantisi şartlı
Çift hashingyok denecek kadar az≤ 0.5 önerilirtembeliki hash hesabı

Hash tablosu sıra bilmez. “Anahtarları sıralı yazdır”, “en küçük anahtar”, “10 ile 20 arasındaki anahtarlar” gibi işler için BST ya da AVL seç.

Sık yapılan hatalar

Yanlış düşünceDoğrusu
Karesel yoklamada her adımda öncekine i2i^2 ekleHer deneme ev hücresine eklenir: h+i2h + i^2
Çift hashing’de h2=k mod Rh_2 = k \bmod R0 çıkabilir; R−(k mod R)R - (k \bmod R) kullan
Açık adreslemede silme = hücreyi boşaltDELETED işareti; yoksa aramalar kopar
Rehash’te anahtarlar eski indekslerine kopyalanırYeni M ile hepsi yeniden hash’lenir
Zincirlemede yeni anahtar sona eklenirSoruda ne dendiyse; genelde başa, O(1)O(1)
Tablonun sonunda yoklama dururmod M ile başa sarar
Math.abs(h) % M her zaman güvenliInteger.MIN_VALUE’da negatif; floorMod
Hash tablosu ile min bulmak O(1)O(1)O(M)O(M); hash sıra tutmaz

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

A hash table of size 11 holds 7 keys. What is its load factor λ\lambda? For which collision strategy may λ\lambda exceed 1?

Cevabı göster
λ = 7/11 ≈ 0.64; separate chaining

λ=n/M=7/11≈0.636\lambda = n / M = 7/11 \approx 0.636. Açık adreslemede her hücrede bir anahtar olduğu için λ≤1\lambda \le 1 zorunlu. Ayrı zincirlemede listeler uzayabilir, λ>1\lambda > 1 olabilir (ama performans düşer).

Soru 2kolay

Insert 27, 18, 29, 28, 39, 13, 16 into a table of size 11 with h(k)=k mod 11h(k) = k \bmod 11 using separate chaining (new keys go to the front of the list). Give all non-empty chains.

Cevabı göster
2: [13]; 5: [16, 27]; 6: [39, 28]; 7: [29, 18]

Hash değerleri: 27→5, 18→7, 29→7, 28→6, 39→6, 13→2, 16→5.
Başa ekleme yapıldığı için sonradan gelen önde durur: indeks 5'te önce 27, sonra 16 başa gelir → [16, 27]. Aynı şekilde 6: [39, 28], 7: [29, 18].

Soru 3orta

Insert 27, 18, 29, 28, 39, 13, 16 into an empty table of size 11 using linear probing with h(k)=k mod 11h(k) = k \bmod 11. Give the final table (index:key).

Cevabı göster
2:13 5:27 6:28 7:18 8:29 9:39 10:16

27 → 5. 18 → 7. 29 → 7 dolu, 8.
28 → 6. 39 → 6 dolu, 7 dolu, 8 dolu, 9 boş (4 yoklama).
13 → 2. 16 → 5, 6, 7, 8, 9 dolu, 10 boş (6 yoklama).
5-10 arası tek bir küme oluştu: birincil kümelenme (primary clustering).

Soru 4orta

Same keys and table size, but with quadratic probing (h(k)+i2 mod 11h(k) + i^2 \bmod 11). Give the final table.

Cevabı göster
2:13 5:27 6:28 7:18 8:29 9:16 10:39

İlk dört anahtar doğrusal ile aynı: 27→5, 18→7, 29→7+1=8, 28→6.
39: h=6 dolu, 6+1=7 dolu, 6+4=10 boş → 10.
13 → 2.
16: h=5 dolu, 5+1=6 dolu, 5+4=9 boş → 9.

Soru 5zor

Same keys, size 11, double hashing with h2(k)=7−(k mod 7)h_2(k) = 7 - (k \bmod 7) and probe sequence h(k)+i⋅h2(k) mod 11h(k) + i \cdot h_2(k) \bmod 11. Give the final table.

Cevabı göster
2:29 3:13 5:27 6:28 7:18 9:39 10:16

27 → 5. 18 → 7.
29: h=7 dolu, h2=7−1=6h_2 = 7 - 1 = 6, (7+6) mod 11 = 2 → 2.
28 → 6.
39: h=6 dolu, h2=7−4=3h_2 = 7 - 4 = 3, 6+3 = 9 → 9.
13: h=2 dolu (29), h2=7−6=1h_2 = 7 - 6 = 1, 3 → 3.
16: h=5 dolu, h2=7−2=5h_2 = 7 - 2 = 5, 10 → 10.

Soru 6orta

In the linear probing table above (2:13 5:27 6:28 7:18 8:29 9:39 10:16), how many cells are examined by an unsuccessful search for key 49?

Cevabı göster
7

49 mod 11 = 5. Arama 5, 6, 7, 8, 9, 10'a bakar: altı dolu hücre, hiçbiri 49 değil. Sonra mod 11 ile 0'a sarar: boş hücre, arama biter. Boş hücre de incelenen hücre sayılır: 6 + 1 = 7.

Soru 7orta

What is printed?

System.out.println("ab".hashCode());
System.out.println("cat".hashCode() == "act".hashCode());
Cevabı göster
3105 and false

String.hashCode: s0⋅31n−1+⋯+sn−1s_0 \cdot 31^{n-1} + \dots + s_{n-1}. "ab" = 97·31 + 98 = 3105. "cat" = 98262, "act" = 96402: farklı. Karakterleri sadece toplasaydın ikisi de 312 olurdu; 31 ile çarpmak sırayı hesaba katar.

Soru 8zor

Using linear probing on the table 2:13 5:27 6:28 7:18 8:29 9:39 10:16, key 29 is deleted. Why must the cell be marked DELETED instead of emptied? How many cells does a search for 39 examine after a lazy delete?

Cevabı göster
Emptying breaks the probe chain; 4 cells

39'un yoklama yolu 6 → 7 → 8 → 9. Hücre 8 gerçekten boşaltılırsa arama 8'de "boş" görüp durur ve 39 yok sanılır (3 yoklama, yanlış sonuç).
Tembel silmede (lazy deletion) 8'e DELETED işareti konur; arama bunu geçer: 6, 7, 8, 9 → 4 hücre, 39 bulunur.
Ekleme DELETED hücreyi yeniden kullanabilir: 50 (h=6) eklenince 6, 7 dolu, 8 DELETED → 50 oraya yazılır.

Soru 9zor

A table of size 7 uses linear probing and is rehashed when λ>0.5\lambda > 0.5 after an insertion; the new size is the first prime ≥2M\ge 2M. Insert 10, 3, 17, 24. Give the final table (old table scanned from index 0 when reinserting).

Cevabı göster
size 17: 0:17 3:3 7:24 10:10

M=7: 10→3, 3→3 dolu→4, 17→3,4 dolu→5 (λ=3/7≈0.43), 24→3,4,5 dolu→6. λ = 4/7 ≈ 0.57 > 0.5 → rehash.
Yeni boyut: 2·7 = 14 → ilk asal 17.
Eski tablo 0'dan taranır: 10 (indeks 3) → 10 mod 17 = 10; 3 → 3; 17 → 0; 24 → 7.
Kümelenme kayboldu, hiçbir çarpışma yok.

Soru 10orta

Why should the table size be prime when using quadratic probing? (Hint: list the cells reachable from home cell 0 when M = 16.)

Cevabı göster
With M = 16 only cells 0, 1, 4, 9 are reachable; with prime M and λ ≤ 0.5 a free cell is guaranteed

M = 16 için i2 mod 16i^2 \bmod 16 yalnızca {0, 1, 4, 9} değerlerini alır: tablo yarı boşken bile yer bulunamayabilir. M asal ise ilk ⌈M/2⌉\lceil M/2 \rceil yoklama hep farklı hücrelere düşer (M=11 için {0,1,3,4,5,9}). Bu yüzden kural: M asal ve λ≤0.5\lambda \le 0.5 ise ekleme her zaman başarılı.

Soru 11orta

Write a method int insert(int[] table, int key) for a linear-probing table of non-negative ints where -1 marks an empty cell. Return the index where the key was stored, or -1 if the table is full.

Cevabı göster
Probe (h + i) % M for i = 0..M-1; store at first -1

Ev hücresinden başla, i'yi artırarak sarmalı ilerle.

static int insert(int[] table, int key) {
    int M = table.length;
    int h = key % M;
    for (int i = 0; i < M; i++) {
        int p = (h + i) % M;
        if (table[p] == -1) {
            table[p] = key;
            return p;
        }
    }
    return -1; // tablo dolu
}

Negatif anahtar ya da hashCode varsa key % M negatif çıkabilir; o zaman Math.floorMod(key, M) kullan.

Soru 12kolay

Give the average and worst-case time of search in a hash table with a good hash function and constant load factor. When does the worst case happen?

Cevabı göster
Average O(1), worst O(n); all keys collide

λ sabit tutulduğunda (rehash ile) beklenen yoklama sayısı sabittir: O(1)O(1). Kötü bir hash fonksiyonu bütün anahtarları aynı hücreye gönderirse zincirlemede tek liste, açık adreslemede tek küme oluşur ve arama O(n)O(n) olur. Hash tablosu sıralı gezinme ya da min/max için uygun değildir.

Doğrulama: Bütün tablolar, yoklama sayıları, rehash sonucu, String.hashCode değerleri ve beklenen yoklama formülleri Java ile hesaplandı (verify/SEN2212/W09.java, java W09.java).

Deftere not kartı

h(k) = k mod M. λ = n/M. Açık adreslemede λ ≤ 1, zincirlemede λ > 1 olabilir. Zincirleme: her hücre bir liste. Ortalama liste uzunluğu λ. Başa ekleme O(1). Doğrusal: h, h+1, h+2, ... (mod M). Birincil kümelenme. λ ≤ 0.5 hedefle. Karesel: h, h+1, h+4, h+9, ... M ASAL ve λ ≤ 0.5 ise yer garantili. İkincil kümelenme. Çift hashing: h + i·h2(k), h2(k) = R - (k mod R), R < M asal. h2 asla 0 olmamalı. Yoklama sayısını sorarsa: dolu hücreler + son bakılan (boş ya da bulunan) hücre. Silme açık adreslemede TEMBEL: DELETED işareti. Boşaltırsan arama zinciri kopar. Rehash: λ eşiği aşınca yeni M = 2M'den büyük ilk asal, tüm anahtarları YENİ M ile yeniden yerleştir. O(n), amortize O(1). String.hashCode = s0·31^(n-1) + ... + s(n-1). "ab" = 3105. Negatif olabilir: Math.floorMod. Math.abs(Integer.MIN_VALUE) negatiftir. -7 % 11 = -7, floorMod(-7, 11) = 4. Ortalama O(1), en kötü O(n). Sıralı çıktı, min/max, aralık sorgusu için hash DEĞİL, BST kullan. Beklenen yoklama (doğrusal, başarısız): ½(1 + 1/(1-λ)²). λ=0.5 → 2.5, λ=0.9 → 50.5.