Hash tablolarında çarpışma çözümleme: Separate Chaining, Linear Probing, Quadratic Probing ve Double Hashing — kümeleme problemi, tombstone, performans karşılaştırması ile adım adım görseller ve C kodu.
🗂️ Veri Yapıları📅 22 February 2026👁️ 17 görüntülenme
Çarpışma Neden Kaçınılmazdır?
Hash fonksiyonu sonsuz büyüklükteki anahtar uzayını sonlu boyutlu bir diziye sıkıştırır. Pigeonhole (güvercin yuvası) prensibi gereği, eğer anahtar sayısı dizi boyutundan fazlaysa en az iki anahtar aynı indekse düşecektir. Ama pratikte anahtar sayısı dizi boyutundan az olsa bile farklı anahtarlar aynı hash değerini üretebilir — bu durum çarpışma (collision) olarak adlandırılır.
Birthday Paradox: 23 kişilik bir grupta aynı doğum gününe sahip iki kişi bulunma olasılığı %50'den fazladır. Benzer şekilde, 1000 kovalık bir hash tablosuna 38 eleman eklendiğinde çarpışma olasılığı %50'yi aşar. Çarpışma "olabilir" değil, "kesinlikle olacak" bir durumdur.
Yaklaşım 1: Zincirleme
Her kovada bir bağlı liste tut. Çarpışan elemanlar aynı kovanın listesine eklenir. Tablo dışında ek bellek kullanır.
Yaklaşım 2: Açık Adresleme
Tüm elemanlar dizi içinde kalır. Çarpışmada sistematik bir yöntemle sonraki boş kovayı ara (probing). Ek bellek yok.
Zincirleme (Separate Chaining)
En basit ve en yaygın çarpışma çözümü: her kova bir bağlı listenin başını tutar. Aynı indekse düşen tüm elemanlar bu listede saklanır. Ekleme O(1) (başa ekle), arama O(k) (k = zincir uzunluğu), silme O(k).
Tam Chaining Implementasyonu
chaining.c — tam implementasyon
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<stdbool.h>#defineINIT_SIZE11#defineMAX_LOAD0.75typedefstructEntry {
char* key;
int value;
structEntry* next;
} Entry;
typedefstruct {
Entry** buckets;
int size; // kova sayısıint count; // eleman sayısı
} ChainTable;
unsigned inthashDJB2(constchar* k, int sz) {
unsigned long h = 5381;
while (*k) h = h * 33 + *k++;
return h % sz;
}
ChainTable* chainOlustur(int sz) {
ChainTable* t = malloc(sizeof(ChainTable));
t->size = sz; t->count = 0;
t->buckets = calloc(sz, sizeof(Entry*));
return t;
}
/* ── Ekleme ── */voidchainEkle(ChainTable* t, constchar* key, int val) {
unsigned int idx = hashDJB2(key, t->size);
// Var mı? Güncellefor (Entry* e = t->buckets[idx]; e; e = e->next)
if (strcmp(e->key, key) == 0) { e->value = val; return; }
// Yeni — başa ekleEntry* e = malloc(sizeof(Entry));
e->key = strdup(key); e->value = val;
e->next = t->buckets[idx];
t->buckets[idx] = e;
t->count++;
}
/* ── Arama ── */intchainAra(ChainTable* t, constchar* key, bool* ok) {
unsigned int idx = hashDJB2(key, t->size);
for (Entry* e = t->buckets[idx]; e; e = e->next)
if (strcmp(e->key, key) == 0) { *ok=true; return e->value; }
*ok = false; return -1;
}
/* ── Silme ── */boolchainSil(ChainTable* t, constchar* key) {
unsigned int idx = hashDJB2(key, t->size);
Entry* curr = t->buckets[idx], *prev = NULL;
while (curr) {
if (strcmp(curr->key, key) == 0) {
if (prev) prev->next = curr->next;
else t->buckets[idx] = curr->next;
free(curr->key); free(curr);
t->count--;
returntrue;
}
prev = curr; curr = curr->next;
}
returnfalse;
}
İndeks 2'de F→C→A zinciri (3 eleman). Arama sırasında en kötü durum: zincirin sonundaki A için 3 strcmp gerekir.
Zincir Uzunluğu ve Performans
İyi hash fonksiyonu ile elemanlar kovalara düzgün dağılır. n eleman ve m kova varsa ortalama zincir uzunluğu α = n/m'dir (load factor). Arama maliyeti O(1 + α): 1 hash hesabı + α karşılaştırma.
α (Load Factor)
Ort. Zincir
Arama Maliyeti
Yorum
0.5
0.5 eleman
~1.5 işlem
Çok iyi
0.75
0.75 eleman
~1.75 işlem
İyi (Java HashMap eşiği)
1.0
1.0 eleman
~2.0 işlem
Kabul edilebilir
2.0
2.0 eleman
~3.0 işlem
Rehash zamanı
10.0
10 eleman
~11 işlem
Çok kötü — listeye dönüştü
Chaining'in gizli maliyeti — cache miss: Bağlı liste düğümleri bellekte dağınıktır. Her next pointer takibi potansiyel bir cache miss'tir. Modern CPU'larda cache miss ~100 ns sürer. 5 elemanlı bir zincir bile ardışık bellekteki 50 elemandan yavaş olabilir. Bu yüzden bazı implementasyonlar zincir yerine küçük dizi kullanır.
Açık Adresleme (Open Addressing)
Açık adreslemede bağlı liste yoktur — tüm elemanlar doğrudan dizi içinde saklanır. Bir kova dolu ise probing (araştırma) dizisi ile bir sonraki boş kova aranır. Probing dizisi şu formülle tanımlanır:
Genel probing formülü:h(key, i) = (h₁(key) + f(i)) % m i = deneme sayısı (0, 1, 2, ...). f(i) fonksiyonu probing stratejisini belirler: • Linear: f(i) = i • Quadratic: f(i) = c₁·i + c₂·i² • Double Hashing: f(i) = i · h₂(key)
Linear Probing (Doğrusal Araştırma)
En basit probing yöntemi: çarpışma olursa bir sonraki kovaya bak. Boş kova bulunana kadar sırayla ilerle. Formül: h(key, i) = (hash(key) + i) % m
Tam Linear Probing Implementasyonu
linear_probing.c
#defineTABLE_SIZE11#defineEMPTY -1#defineDELETED -2// tombstonetypedefstruct {
char* key;
int value;
int state; // EMPTY, DELETED, veya 0 (dolu)
} Slot;
typedefstruct {
Slot* slots;
int size;
int count;
} LinearTable;
/* ── Ekleme ── */voidlinearEkle(LinearTable* t, constchar* key, int val) {
unsigned int idx = hashDJB2(key, t->size);
for (int i = 0; i < t->size; i++) {
int pos = (idx + i) % t->size; // linear probeif (t->slots[pos].state == EMPTY ||
t->slots[pos].state == DELETED) {
// Boş veya silinmiş → yerleştir
t->slots[pos].key = strdup(key);
t->slots[pos].value = val;
t->slots[pos].state = 0;
t->count++;
return;
}
if (strcmp(t->slots[pos].key, key) == 0) {
t->slots[pos].value = val; // güncellereturn;
}
}
}
/* ── Arama ── */intlinearAra(LinearTable* t, constchar* key, bool* ok) {
unsigned int idx = hashDJB2(key, t->size);
for (int i = 0; i < t->size; i++) {
int pos = (idx + i) % t->size;
if (t->slots[pos].state == EMPTY) {
*ok = false; return -1; // boş → yok
}
if (t->slots[pos].state != DELETED &&
strcmp(t->slots[pos].key, key) == 0) {
*ok = true; return t->slots[pos].value;
}
// DELETED → atla, aramaya devam
}
*ok = false; return -1;
}
/* ── Silme (Tombstone) ── */boollinearSil(LinearTable* t, constchar* key) {
unsigned int idx = hashDJB2(key, t->size);
for (int i = 0; i < t->size; i++) {
int pos = (idx + i) % t->size;
if (t->slots[pos].state == EMPTY) returnfalse;
if (t->slots[pos].state != DELETED &&
strcmp(t->slots[pos].key, key) == 0) {
free(t->slots[pos].key);
t->slots[pos].state = DELETED; // ★ tombstone!
t->count--;
returntrue;
}
}
returnfalse;
}
6 Eleman Sonrası — Birincil Kümeleme (Primary Clustering) Görünür
0
—
1
—
2
—
3
A
4
C
5
D
6
E
7
B
8
F
9
—
10
—
3-4-5-6 = küme! 7-8 = küme! Kümeler büyüdükçe yeni çarpışmalar kümeye düşer → küme daha da büyür!
Birincil Kümeleme Problemi (Primary Clustering)
Linear probing'in en büyük sorunu birincil kümeleme'dir: ardışık dolu hücreler bir "küme" oluşturur. Yeni bir eleman bu kümenin herhangi bir yerine hash'lenirse, kümenin sonuna eklenir → küme büyür → daha fazla eleman kümeye düşer → kısır döngü.
Kümeleme Etkisi — Yoğun Bölge Büyüyor
α = 0.5: Ortalama 1.5 probe (başarılı arama) α = 0.75: Ortalama 2.5 probe α = 0.9: Ortalama 5.5 probe α = 0.95: Ortalama 10.5 probe — neredeyse doğrusal arama!
Formül (başarılı arama): ½(1 + 1/(1-α)). α yükseldikçe performans hızla çöker.
Linear Probing'in avantajı: Kümeleme sorununa rağmen, ardışık bellek erişimi sayesinde cache performansı çok iyidir. Modern CPU'larda cache line 64 byte'tır — bir probe'da birden fazla kova aynı anda cache'e yüklenir. Bu yüzden düşük α'da linear probing tüm yöntemlerden hızlıdır. Python dict ve Rust HashMap bu avantajı kullanır.
Tombstone (Mezar Taşı) Mekanizması
Open addressing'de silinen eleman doğrudan NULL yapılamaz — çünkü probe zinciri kopar. Çözüm: silinen hücreye özel DELETED (tombstone) işareti konur.
Tombstone Olmadan Silme → Arama Bozulur!
Başlangıç: A(3), C(3→4), E(3→4→5→6)
3
A
4
C
5
—
6
E
C'yi sil → NULL yap
NULL yapıldı → search(E) BOZULDU!
3
A
4
∅
5
—
6
E
search(E): 3(A)→4(∅) DUR! → "E yok" ← YANLIŞ!
Tombstone ile doğru çözüm
3
A
4
DEL
5
—
6
E
search(E): 3(A)→4(DEL:atla)→5(∅)... Bekle — E aslında 6'da!
DELETED üzerinden atlanır ama arama durdurulmaz. Sadece EMPTY gördüğünde "yok" denir.
Quadratic Probing (Karesel Araştırma)
Linear probing'in birincil kümeleme sorununu çözmek için adım boyutunu sabit 1 yerine karesel artıran yöntemdir. Formül:
Quadratic Probing formülü:h(key, i) = (hash(key) + c₁·i + c₂·i²) % m Yaygın basit form: h(key, i) = (hash(key) + i²) % m Probe dizisi: +0, +1, +4, +9, +16, +25, ... → dolu bölgeleri atlayarak ilerler.
i=0: 3 dolu → i=1: 4 dolu → i=2: (3+4)%11 = 7 dolu → i=3: (3+9)%11 = 1 boş
→ 1
E
3
3,4,7,1 dolu → i=4: (3+16)%11 = 8 boş
→ 8
Quadratic vs Linear — Aynı Hash Değerleri, Farklı Dağılım
Linear Probing → Küme!
—
—
—
A
C
D
E
B
—
—
—
3-4-5-6-7: 5 ardışık dolu → küme
Quadratic Probing → Dağılmış
—
D
—
A
C
—
—
B
E
—
—
1, 3, 4, 7, 8: dağınık → küme yok!
quadratic probing — ekleme
voidquadEkle(LinearTable* t, constchar* key, int val) {
unsigned int h = hashDJB2(key, t->size);
for (int i = 0; i < t->size; i++) {
int pos = (h + i * i) % t->size; // ★ i² adımif (t->slots[pos].state == EMPTY ||
t->slots[pos].state == DELETED) {
t->slots[pos].key = strdup(key);
t->slots[pos].value = val;
t->slots[pos].state = 0;
t->count++;
return;
}
if (strcmp(t->slots[pos].key, key) == 0) {
t->slots[pos].value = val;
return;
}
}
}
Quadratic Probing'in tuzağı — İkincil Kümeleme (Secondary Clustering): Aynı hash değerine sahip tüm anahtarlar aynı probe dizisini takip eder: h, h+1, h+4, h+9, ... Bu, birincil kümelemeden hafiftir ama yine verimsizlik yaratır. Ayrıca tablo boyutu asal ve α < 0.5 değilse tüm kovalar ziyaret edilemeyebilir → yer olsa bile eleman eklenemez!
Quadratic Probing'in çalışma garantisi: Tablo boyutu asal sayı ve α < 0.5 ise, ilk ⌈m/2⌉ probe farklı kovalara düşer → boş kova bulunması garantidir. Bu yüzden quadratic probing kullanan tablolarda rehash eşiği genellikle 0.5'tir.
Double Hashing (Çift Hash)
Hem birincil hem ikincil kümelemeyi çözen en güçlü open addressing yöntemidir. Adım boyutu sabit veya karesel değil, ikinci bir hash fonksiyonu tarafından belirlenir — böylece her anahtar kendine özgü bir probe dizisi üretir.
Double Hashing formülü: h(key, i) = (h₁(key) + i × h₂(key)) % m
h₁ = birincil hash (indeks belirler) h₂ = ikincil hash (adım boyutunu belirler) Kural: h₂(key) asla 0 olmamalı (sonsuz döngü!) ve m ile aralarında asal olmalı (tüm kovaları ziyaret garanti). Yaygın formül:h₂(key) = ASAL - (key % ASAL) — ASAL < m olan bir asal sayı.
Double Hashing — Aynı h₁ ama Farklı Adım Boyutları → Mükemmel Dağılım
0
1
2
47
69
5
6
80
58
9
10
4 anahtar aynı h₁=3 → ama h₂ farklı (2, 5, 1, 4) → hiç kümeleme yok! 47: adım 2 → 3,5,7,9... 58: adım 5 → 3,8,2,7... 69: adım 1 → 3,4,5,6... 80: adım 4 → 3,7,0,4...
double hashing — ekleme + arama
#definePRIME27// m'den küçük asalunsigned inthash2(int key) {
returnPRIME2 - (key % PRIME2); // asla 0 olmaz ✓
}
voiddoubleEkle(LinearTable* t, int key, int val) {
unsigned int h1 = key % t->size;
unsigned int h2 = hash2(key);
for (int i = 0; i < t->size; i++) {
int pos = (h1 + i * h2) % t->size; // ★ double hashif (t->slots[pos].state == EMPTY ||
t->slots[pos].state == DELETED) {
t->slots[pos].value = val;
t->slots[pos].state = 0;
t->count++;
return;
}
}
}
intdoubleAra(LinearTable* t, int key, bool* ok) {
unsigned int h1 = key % t->size;
unsigned int h2 = hash2(key);
for (int i = 0; i < t->size; i++) {
int pos = (h1 + i * h2) % t->size;
if (t->slots[pos].state == EMPTY)
{ *ok = false; return -1; }
if (t->slots[pos].state != DELETED &&
t->slots[pos].value == key) // int key örneği
{ *ok = true; return t->slots[pos].value; }
}
*ok = false; return -1;
}
3 Probing Yönteminin Probe Dizisi Karşılaştırması
Aynı 4 anahtar (hepsi h₁ = 3), tablo boyutu 11:
Anahtar
Linear (+1)
Quadratic (+i²)
Double (×h₂)
1. (h=3)
3
3
3
2. (h=3)
4
4 (3+1)
8 (3+5)
3. (h=3)
5
7 (3+4)
4 (3+1)
4. (h=3)
6
1 (3+9)
7 (3+4)
Dağılım
3,4,5,6 — küme!
3,4,7,1 — daha iyi
3,8,4,7 — en iyi!
Double Hashing neden en iyisi? Her anahtarın kendine özgü adım boyutu (h₂) vardır. Linear'da herkes +1, Quadratic'te herkes +i² adım atar — ama Double Hashing'de aynı h₁'e sahip anahtarlar bile farklı yollar izler. Bu, elemanların tabloya mümkün olan en düzgün şekilde dağılmasını sağlar.
4 Yöntemin Karşılaştırması
Performans — Ortalama Probe Sayısı
Başarılı arama için ortalama probe sayısı (teorik):
α
Chaining
Linear
Quadratic
Double Hash
0.25
1.13
1.17
1.15
1.15
0.50
1.25
1.50
1.44
1.39
0.75
1.38
2.50
2.01
1.85
0.90
1.45
5.50
3.52
2.56
0.95
1.48
10.50
5.81
3.15
0.99
1.50
50.50
—
4.65
Chaining: 1 + α/2. Linear: ½(1 + 1/(1-α)). Double: -ln(1-α)/α. α yükseldikçe linear feci şekilde kötüleşir.
α = 0.90'da Performans Farkı — Aynı Arama, 4 Farklı Sonuç
Chaining
1.45
probe
Double
2.56
probe
Quadratic
3.52
probe
Linear
5.50
probe
Genel Karşılaştırma Tablosu
Özellik
Chaining
Linear
Quadratic
Double
Kümeleme
Yok ✓
Birincil ✗
İkincil
Yok ✓
Cache perf.
Kötü ✗
En iyi ✓
İyi
Orta
Silme
Basit ✓
Tombstone
Tombstone
Tombstone
α > 1.0
Çalışır ✓
İmkansız ✗
İmkansız ✗
İmkansız ✗
Bellek
Pointer ek yükü
Min ✓
Min ✓
Min ✓
Rehash eşiği
0.75 – 1.0
0.5 – 0.75
0.5
0.6 – 0.75
Hash sayısı
1
1
1
2
Koda karmaşıklık
Orta
Basit ✓
Orta
Karmaşık
Kullanım
Java HashMap Go map
Python dict Rust HashMap
Ders kitapları
Teorik ideal
Kümeleme Türleri — Görsel Karşılaştırma
Birincil Kümeleme (Linear)
Ardışık dolu hücreler büyüyen bir küme oluşturur. Herhangi bir hash değeri kümenin içine veya hemen bitişiğine düşerse, kümenin sonuna eklenir → küme büyür. Etki: Büyük kümeler diğer kümelerle birleşir → felaket.
İkincil Kümeleme (Quadratic)
Aynı hash değerine sahip anahtarlar aynı probe dizisini izler (h, h+1, h+4, h+9...). Farklı hash değerleri farklı diziler üretir. Etki: Birincilden hafif ama yine verimsiz.
Kümeleme Yok (Double Hash)
Her anahtar kendine özgü adım boyutuna sahip. Aynı h₁ bile farklı h₂ üretir → probe dizileri tamamen bağımsız. Etki: Uniform probing'e en yakın gerçek yöntem.
Modern Çarpışma Çözüm Teknikleri
Teknik
Açıklama
Kullanan
Robin Hood Hashing
"Zenginden al, fakire ver" — ekleme sırasında probe sayısı fazla olan elemanlar, az olan elemanlarla yer değiştirir. Probe sayıları dengelenir → en kötü durum iyileşir.
Rust HashMap
Cuckoo Hashing
İki ayrı hash fonksiyonu, iki ayrı tablo. Çarpışmada mevcut elemanı diğer tabloya taşı. Arama garantili O(1) — en fazla 2 bakış.
Hardware hash, ağ paketleri
Hopscotch Hashing
Her kova bir "komşuluk" (neighborhood) bölgesi tutar. Elemanlar kendi bölgelerinden uzaklaşamaz → cache-friendly + iyi dağılım.
Eşzamanlı (concurrent) tablolar
Swiss Table
SIMD komutları ile 16 kovayı aynı anda kontrol eder. Metadata (hash'in üst 7 biti) ayrı dizide tutulur → çok hızlı probe.
Google Abseil, C++ absl::flat_hash_map
Chaining → Tree
Zincir 8 elemandan uzunsa bağlı listeyi kırmızı-siyah ağaca dönüştür → en kötü O(n) → O(log n).
Java 8+ HashMap
Demo — 3 Yöntem Yan Yana
Aynı 6 anahtarı 3 farklı yöntemle ekleyip probe sayılarını karşılaştıralım. Tablo boyutu = 11, hash(key) = key % 11.
• Load factor yüksek olabilir (α > 0.75) • Silme işlemi sık yapılacak • Anahtar boyutu büyük (pointer daha ucuz) • Basit implementasyon istiyorsan • Eşzamanlı erişim (concurrent) gerekiyorsa
Open Addressing Seç — Eğer:
• Bellek kısıtlı (pointer ek yükü istemiyorsun) • Cache performansı kritik • α düşük tutulabilir (< 0.7) • Silme nadir yapılıyor • Küçük, sabit boyutlu veriler
Karar Ağacı — Çarpışma Çözüm Yöntemi Seçimi
① Bellek kısıtlı mı? → Evet → Open Addressing → Hayır → ②
② Silme sık mı? → Evet → Chaining (tombstone yok) → Hayır → ③
③ Cache performansı kritik mi? → Evet, α düşük → Linear Probing → Evet, α orta → Robin Hood / Swiss Table → Hayır → ④
④ Garantili O(1) arama gerekli mi? → Evet → Cuckoo Hashing → Hayır → Double Hashing (en iyi genel amaçlı)
Sonuç
Hash tablolarında çarpışma kaçınılmazdır — Birthday Paradox bile küçük tablolarda yüksek çarpışma olasılığı gösterir. Çarpışma çözümünün kalitesi doğrudan hash tablosunun performansını belirler.
Zincirleme (Chaining) en basit yöntemdir: her kovada bağlı liste tutulur, α > 1.0'da bile çalışır, silme doğaldır. Ancak pointer takibi cache-unfriendly'dir ve ek bellek gerektirir.
Open Addressing ailesi tüm elemanları dizi içinde saklar — bağlı liste yoktur. Linear Probing cache-friendly ama birincil kümeleme sorunu yaşar; Quadratic Probing kümelemeyi azaltır ama tüm kovaları ziyaret edemeyebilir; Double Hashing her anahtara özgü adım boyutuyla hem birincil hem ikincil kümelemeyi ortadan kaldırır.
Modern implementasyonlar (Robin Hood, Cuckoo, Swiss Table) bu temel yöntemlerin zayıflıklarını CPU mimarisi bilgisiyle aşar. Rust'ın Robin Hood'u dengeleme yapar, Java 8 zincir uzarsa ağaca geçer, Google'ın Swiss Table'ı SIMD ile 16 kovayı paralel kontrol eder.
Bölüm 7.2 özeti: Çarpışma = farklı anahtarlar → aynı indeks. Chaining: her kovada bağlı liste, α > 1 destekler, silme kolay, cache-unfriendly. Open Addressing: tüm elemanlar dizide, bağlı liste yok, bellek verimli, silmede tombstone gerekli. Linear Probing: h+i, cache-friendly ama birincil kümeleme; α=0.9'da ortalama 5.5 probe. Quadratic Probing: h+i², ikincil kümeleme; tablo boyutu asal ve α<0.5 gerekli. Double Hashing: h₁+i×h₂, kümeleme yok, en iyi dağılım; α=0.9'da 2.56 probe. Modern teknikler: Robin Hood (probe dengeleme), Cuckoo (O(1) garanti arama), Swiss Table (SIMD paralel probe), Java TreeBin (zincir→RB-Tree α=0.75).