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ü.
Hashing, “bu anahtar var mı?” sorusunu ortalama sabit zamanda cevaplamanın yoludur. BST bunu ‘de yapar, hash tablosu ‘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:
- Ev hücresi . Önce bunu her anahtar için hesapla, kenara yaz.
- Ç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.
- Her şey mod M. Tablonun sonundan başa sararsın.
- Açık adreslemede silme tembel yapılır.
- λ 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 , 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):
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 % Mde negatif olur ve dizi indeksi patlar.Math.abs(h) % Mbile güvenli değil:Math.abs(Integer.MIN_VALUE)yine negatiftir. DoğrusuMath.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): . 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 ().
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 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: Burada zorunlu, pratikte hedeflenir.
Doğrusal yoklama (linear probing)
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 | Başarısız |
|---|---|---|
| 0.50 | 1.50 | 2.50 |
| 0.75 | 2.50 | 8.50 |
| 0.90 | 5.50 | 50.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)
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 , bir önceki denemeye eklenmez.
Neden M asal ve λ ≤ 0.5? M asal değilse 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 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)
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. asla 0 olmamalı (yoksa yerinde sayar); 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ü 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 sürer ama nadiren olur. Her seferinde boyut ikiye katlandığı için ekleme başına amortize maliyet kalır. Java’nın HashMap’i varsayılan olarak λ > 0.75 olunca kapasiteyi ikiye katlar.
Hangi yöntem ne zaman?
| Yöntem | Kümelenme | λ sınırı | Silme | Not |
|---|---|---|---|---|
| Ayrı zincirleme | yok | > 1 olabilir | kolay | ek bellek: liste düğümleri |
| Doğrusal | birincil | ≤ 0.5 önerilir | tembel | önbellek dostu, basit |
| Karesel | ikincil | ≤ 0.5, M asal | tembel | yer garantisi şartlı |
| Çift hashing | yok denecek kadar az | ≤ 0.5 önerilir | tembel | iki 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üşünce | Doğrusu |
|---|---|
| Karesel yoklamada her adımda öncekine ekle | Her deneme ev hücresine eklenir: |
| Çift hashing’de | 0 çıkabilir; kullan |
| Açık adreslemede silme = hücreyi boşalt | DELETED işareti; yoksa aramalar kopar |
| Rehash’te anahtarlar eski indekslerine kopyalanır | Yeni M ile hepsi yeniden hash’lenir |
| Zincirlemede yeni anahtar sona eklenir | Soruda ne dendiyse; genelde başa, |
| Tablonun sonunda yoklama durur | mod M ile başa sarar |
Math.abs(h) % M her zaman güvenli | Integer.MIN_VALUE’da negatif; floorMod |
| Hash tablosu ile min bulmak | ; hash sıra tutmaz |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
A hash table of size 11 holds 7 keys. What is its load factor ? For which collision strategy may exceed 1?
Cevabı göster
. Açık adreslemede her hücrede bir anahtar olduğu için zorunlu. Ayrı zincirlemede listeler uzayabilir, olabilir (ama performans düşer).
Insert 27, 18, 29, 28, 39, 13, 16 into a table of size 11 with using separate chaining (new keys go to the front of the list). Give all non-empty chains.
Cevabı göster
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].
Insert 27, 18, 29, 28, 39, 13, 16 into an empty table of size 11 using linear probing with . Give the final table (index:key).
Cevabı göster
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).
Same keys and table size, but with quadratic probing (). Give the final table.
Cevabı göster
İ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.
Same keys, size 11, double hashing with and probe sequence . Give the final table.
Cevabı göster
27 → 5. 18 → 7.
29: h=7 dolu, , (7+6) mod 11 = 2 → 2.
28 → 6.
39: h=6 dolu, , 6+3 = 9 → 9.
13: h=2 dolu (29), , 3 → 3.
16: h=5 dolu, , 10 → 10.
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
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.
What is printed?
System.out.println("ab".hashCode());
System.out.println("cat".hashCode() == "act".hashCode());Cevabı göster
String.hashCode: . "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.
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
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.
A table of size 7 uses linear probing and is rehashed when after an insertion; the new size is the first prime . Insert 10, 3, 17, 24. Give the final table (old table scanned from index 0 when reinserting).
Cevabı göster
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.
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
M = 16 için yalnızca {0, 1, 4, 9} değerlerini alır: tablo yarı boşken bile yer bulunamayabilir. M asal ise ilk yoklama hep farklı hücrelere düşer (M=11 için {0,1,3,4,5,9}). Bu yüzden kural: M asal ve ise ekleme her zaman başarılı.
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
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.
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
λ sabit tutulduğunda (rehash ile) beklenen yoklama sayısı sabittir: . 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 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).