Hash fonksiyonu nedir, hash tablosu oluşturma, anahtar-değer çifti ekleme, değer arama, eleman silme — çarpışma çözümleri ile adım adım görseller ve C kodu.
🗂️ Veri Yapıları📅 22 February 2026👁️ 3 görüntülenme
Hash Tablosu Nedir?
Hash tablosu, verileri anahtar-değer (key-value) çiftleri halinde saklayan ve ortalamada O(1) sürede ekleme, arama ve silme sağlayan veri yapısıdır. Bu hız, bir hash fonksiyonu sayesinde anahtarın doğrudan dizi indeksine dönüştürülmesiyle elde edilir.
Dizi ile arama
"Ali"nin notunu bul → tüm diziyi tara O(n) — 1 milyon öğrenci = 1M karşılaştırma
Hash tablosu ile arama
"Ali" → hash("Ali") → indeks 3 → direkt eriş O(1) — 1 milyon öğrenci = 1 erişim!
Temel fikir: Bir fonksiyon kullanarak anahtarı (string, sayı, nesne) bir dizi indeksine dönüştür. Bu indekse git ve değeri oku/yaz. Arada karşılaştırma yok, sıralama yok — doğrudan adres hesapla ve eriş.
Hash Fonksiyonu Nedir?
Hash fonksiyonu, herhangi bir boyuttaki girdiyi sabit boyutlu bir çıktıya (hash değeri / hash code) dönüştüren fonksiyondur. Bu çıktı, hash tablosundaki dizi indeksini belirler.
Hash Fonksiyonu — Anahtar → İndeks Dönüşümü
İyi Hash Fonksiyonunun 4 Özelliği
Özellik
Açıklama
Neden Önemli?
Belirleyicilik
Aynı girdi her zaman aynı çıktıyı verir
Arama tutarlı olmalı
Düzgün dağılım
Çıktılar tablo boyunca eşit yayılır
Çarpışmaları minimize eder
Hız
Hesaplama O(1) veya O(m) olmalı
Hash hesabı darboğaz olmamalı
Çığ etkisi
Küçük girdi farkı → çok farklı çıktı
Benzer anahtarlar aynı yere düşmemeli
Yaygın Hash Fonksiyonları
hash fonksiyonları
/* 1) Bölme metodu (Division) — en basit */unsigned inthashBolme(constchar* key, int tablo_boyutu) {
unsigned int hash = 0;
for (int i = 0; key[i]; i++)
hash += key[i];
return hash % tablo_boyutu;
}
/* 2) DJB2 — yaygın, basit, iyi dağılım */unsigned inthashDJB2(constchar* key, int tablo_boyutu) {
unsigned long hash = 5381;
int c;
while ((c = *key++))
hash = hash * 33 + c; // hash × 33 + karakterreturn hash % tablo_boyutu;
}
/* 3) Tam sayı anahtarlar için — Knuth çarpma */unsigned inthashKnuth(int key, int tablo_boyutu) {
return (key * 2654435761u) >> 16 % tablo_boyutu;
}
Tablo boyutu neden asal sayı olmalı?hash % tablo_boyutu işleminde tablo boyutu asal sayı olursa hash değerleri daha düzgün dağılır. Örneğin boyut 10 ise sadece son rakam etkili olur — boyut 11 ise tüm bitler dağılıma katkı sağlar.
Hash Tablosu Yapısı
Hash tablosu temelde bir dizi (array) ve bir hash fonksiyonundan oluşur. Dizi, anahtar-değer çiftlerini depolar. Hash fonksiyonu, anahtarı dizi indeksine çevirir.
hashtable.c — yapı
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<stdbool.h>#defineTABLO_BOYUTU11// asal sayı!/* Anahtar-değer çifti */typedefstructEntry {
char* key; // anahtar (string)int value; // değerstructEntry* next; // çarpışma zinciri (chaining)
} Entry;
/* Hash tablosu */typedefstruct {
Entry** buckets; // kova dizisiint size; // tablo boyutuint count; // eleman sayısı
} HashTable;
/* Hash fonksiyonu — DJB2 */unsigned inthash(constchar* key, int size) {
unsigned long h = 5381;
int c;
while ((c = *key++))
h = h * 33 + c;
return h % size;
}
/* Tablo oluştur */HashTable* tabloOlustur(int size) {
HashTable* ht = malloc(sizeof(HashTable));
ht->size = size;
ht->count = 0;
ht->buckets = calloc(size, sizeof(Entry*));
return ht;
}
Boş Hash Tablosu (boyut = 11)
0
NULL
1
NULL
2
NULL
3
NULL
4
NULL
5
NULL
6
NULL
7
NULL
8
NULL
9
NULL
10
NULL
Her kova (bucket) başlangıçta NULL. calloc ile sıfırlanmış.
Anahtar-Değer Ekleme (Insert)
Ekleme işlemi iki adımdan oluşur: hash fonksiyonu ile indeks hesapla, o indekse anahtar-değer çiftini yerleştir. Aynı anahtarla tekrar eklenirse değer güncellenir.
tabloEkle() — O(1) ortalama
voidtabloEkle(HashTable* ht, constchar* key, int value) {
unsigned int idx = hash(key, ht->size);
// Aynı anahtar varsa güncelleEntry* curr = ht->buckets[idx];
while (curr) {
if (strcmp(curr->key, key) == 0) {
curr->value = value; // güncellereturn;
}
curr = curr->next;
}
// Yeni entry oluştur — başa ekle (O(1))Entry* entry = malloc(sizeof(Entry));
entry->key = strdup(key);
entry->value = value;
entry->next = ht->buckets[idx]; // eski baş → next
ht->buckets[idx] = entry; // yeni baş
ht->count++;
}
① "Ali":85, "Ayşe":92, "Can":78, "Deniz":95 eklendi
0
NULL
1
Can : 78
2
NULL
3
Ali : 85
4
NULL
5
Deniz : 95
6
NULL
7
Ayşe : 92
8
NULL
9
NULL
10
NULL
4 ekleme, 0 çarpışma — her anahtar farklı indekse düştü ✓
Çarpışma (Collision) Problemi
Farklı anahtarlar aynı hash indeksine düşebilir — buna çarpışma denir. Yukarıdaki örnekte "Elif" de indeks 3'e düşüyor ama orada "Ali" zaten var!
② "Elif":88 ekleniyor → indeks 3 → ÇARPIŞMA!
0
NULL
1
Can : 78
2
NULL
3
Ali : 85 ← Elif de buraya düşmek istiyor!
4
NULL
5
Deniz : 95
6
NULL
7
Ayşe : 92
8
NULL
9
NULL
10
NULL
hash("Ali") == hash("Elif") == 3 → ÇARPIŞMA!
Çarpışma kaçınılmazdır! Pigeonhole prensibi: sonsuz olası anahtar → sonlu sayıda kova. Çarpışma olmayacak hash fonksiyonu yoktur. Önemli olan çarpışmayı nasıl çözdüğümüzdür. İki ana yöntem: Zincirleme (Chaining) ve Açık Adresleme (Open Addressing).
Çarpışma Çözümü 1: Zincirleme (Separate Chaining)
Her kova (bucket) bir bağlı liste tutar. Aynı indekse düşen tüm elemanlar o kovanın bağlı listesine eklenir. Arama sırasında doğru kovaya gidip listede doğrusal arama yapılır.
Zincirleme ile "Elif":88 eklendi — indeks 3'te zincir
İndeks 3'te zincir: Elif → Ali → NULL. Yeni eleman listenin başına eklendi (O(1)).
Değer Arama (Search / Lookup)
Arama, eklemenin simetriğidir: hash fonksiyonu ile indeksi hesapla, o kovadaki bağlı listeyi tara, anahtar eşleşirse değeri döndür.
③ search("Zeynep") → İndeks 9 → NULL → Bulunamadı ✗
hash("Zeynep") = 9 → buckets[9] → NULL → return not found
1 hash + 0 strcmp = O(1) — boş kova, hemen bitir
Eleman Silme (Delete)
Zincirleme (chaining) kullanan hash tablosunda silme, bağlı listeden düğüm silme ile aynıdır: indeksi hesapla, zincirde anahtarı bul, bağlı listeden çıkar.
Elif zincirin başında → buckets[3] = Elif.next (Ali) yapıldı. Elif free() ile serbest bırakıldı.
Çarpışma Çözümü 2: Açık Adresleme (Open Addressing)
Zincirlemeye alternatif olarak, tüm elemanlar doğrudan dizi içinde saklanır — bağlı liste yoktur. Çarpışma olduğunda bir sonraki boş kovayı aramak için sistematik bir yöntemle ilerlenir (probing).
Linear Probing (Doğrusal Araştırma)
En basit yöntem: çarpışma olursa sırayla bir sonraki kovaya bak. h(key, i) = (hash(key) + i) % boyut — i = 0, 1, 2, ...
Linear Probing ile "Elif":88 Ekleme (indeks 3 dolu)
0
NULL
1
Can : 78
2
NULL
3
Ali : 85 ← i=0: DOLU!
4
Elif : 88 ← i=1: BOŞ → yerleş!
5
Deniz : 95
6
NULL
7
Ayşe : 92
8
NULL
9
NULL
10
NULL
hash("Elif")=3 (dolu) → 4'e bak (boş) → yerleştir. Bağlı liste yok, hepsi dizide.
Açık Adresleme Yöntemleri Karşılaştırması
Yöntem
Formül
Avantaj
Dezavantaj
Linear Probing
(h + i) % n
Basit, cache-friendly
Kümeleme (clustering): dolu bölgeler büyür
Quadratic Probing
(h + i²) % n
Kümelemeyi azaltır
Tüm kovaları ziyaret etmeyebilir
Double Hashing
(h₁ + i×h₂) % n
En iyi dağılım
İki hash hesabı, daha karmaşık
Open Addressing'de silme problemi: Linear probing ile eklenen elemanı doğrudan silersek (NULL yaparsak) arama zinciri kopar! Çözüm: silinen hücreye DELETED (tombstone) işareti koyulur. Ekleme sırasında DELETED hücreye yazılabilir, arama sırasında DELETED üzerinden atlanır.
Chaining vs Open Addressing
Özellik
Chaining
Open Addressing
Depolama
Dizi + bağlı listeler
Sadece dizi
Çarpışma çözümü
Listeye ekle
Sonraki boş kovayı bul
Load factor > 1
Sorunsuz çalışır ✓
İmkansız ✗
Silme
Basit (listeden çıkar) ✓
Tombstone gerekli ✗
Cache performansı
Düşük (pointer takibi)
Yüksek (ardışık bellek) ✓
Bellek ek yükü
Pointer başına 8 byte
Sıfır ✓
En kötü arama
O(n) — tek zincir
O(n) — tüm dizi
Kullanım
Java HashMap, Go map
Python dict, Rust HashMap
Load Factor ve Rehashing
Load factor (yük faktörü), hash tablosunun ne kadar dolu olduğunu gösteren orandır. Performans doğrudan buna bağlıdır.
Load Factor = Eleman Sayısı / Tablo Boyutu
α = n / m = 5 / 11 ≈ 0.45
5 eleman, 11 kova → tablo %45 dolu
α (Load Factor)
Durum
Performans
α < 0.5
Bol yer var
Mükemmel — O(1)'e çok yakın
0.5 ≤ α < 0.75
Dengeli
İyi — hâlâ O(1) civarı
α ≥ 0.75
Kalabalık!
Kötüleşiyor — çarpışma artıyor
α > 1.0 (chaining)
Taşma
Zincirler uzar → O(α) arama
α → 1.0 (open addr.)
Neredeyse dolu
Felaket — arama O(n)'e yaklaşır
Rehashing (Yeniden Boyutlandırma)
Load factor eşik değerini aştığında (tipik olarak 0.75), tablo genişletilir ve tüm elemanlar yeni boyutlu tabloya yeniden hash'lenir. Çünkü hash indeksleri tablo boyutuna bağlıdır — boyut değişince tüm indeksler değişir.
Rehashing adımları: 1. Yeni, daha büyük dizi oluştur (tipik: 2 × eski boyut'a en yakın asal sayı) 2. Eski tablodaki her elemanı yeni hash fonksiyonu ile yeniden hesapla 3. Yeni tabloya ekle 4. Eski diziyi serbest bırak Maliyet: O(n) — ama nadiren yapılır (amortized O(1) ekleme)
rehash() — tabloyu genişlet
voidrehash(HashTable* ht) {
int eskiBoyut = ht->size;
Entry** eskiBuckets = ht->buckets;
// Yeni boyut: ~2× en yakın asal
ht->size = sonrakiAsal(eskiBoyut * 2);
ht->buckets = calloc(ht->size, sizeof(Entry*));
ht->count = 0;
// Tüm elemanları yeniden hash'lefor (int i = 0; i < eskiBoyut; i++) {
Entry* curr = eskiBuckets[i];
while (curr) {
Entry* next = curr->next;
tabloEkle(ht, curr->key, curr->value);
free(curr->key);
free(curr);
curr = next;
}
}
free(eskiBuckets);
}
/* tabloEkle içinde rehash tetikleme */voidtabloEkleAkilli(HashTable* ht, constchar* key, int val) {
if ((float)ht->count / ht->size >= 0.75f)
rehash(ht); // α ≥ 0.75 → genişlettabloEkle(ht, key, val);
}
=== Hash Table Demo ===
5 eleman eklendi. count=5, size=11
--- Arama ---
Ali → 85 ✓
Can → 78 ✓
Zeynep → bulunamadı ✗ ← tabloda yok
Elif → 88 ✓ ← çarpışma zincirinde bulundu
--- Güncelleme ---
Ali güncellendi: 90 ← aynı key ile tekrar ekle → değer güncellendi
--- Silme ---
delete("Elif"): silindi ✓ ← zincirden çıkarıldı
delete("Zeynep"): bulunamadı ✗ ← tabloda yok
search("Elif"): yok ✗ ← artık bulunamaz
Kalan eleman: 4
Karmaşıklık Analizi
Hash tablosunun en büyük gücü ortalama O(1) erişim süresidir. Ancak en kötü durum O(n)'dir — tüm anahtarlar aynı kovaya düşerse. İyi hash fonksiyonu ve düşük load factor ile en kötü durum pratikte oluşmaz.
Operasyon
Ortalama
En Kötü
Açıklama
Ekleme (insert)
O(1)
O(n)
Amortized O(1) — rehash dahil
Arama (search)
O(1)
O(n)
Tüm zincir taranırsa O(n)
Silme (delete)
O(1)
O(n)
Arama + sabit süre çıkarma
Rehashing
O(n)
Tüm elemanlar yeniden hash'lenir
Min / Max bulma
O(n)
Sıra yok → tüm tabloyu tara
Sıralı erişim
O(n log n)
Önce topla, sonra sırala — hash tablo sıralı değildir
Bellek
O(n)
Chaining: n entry + m kova. Open: m kova (m ≥ n)
Ortalama O(1) nasıl mümkün? İyi hash fonksiyonu elemanları düzgün dağıtır → her kovada ortalama α = n/m eleman → arama O(1 + α). Load factor α < 1 ise → O(1). Rehash ile α'yı düşük tutarız → amortized O(1) garanti.
Hash Tablosu vs Diğer Veri Yapıları
Özellik
Hash Table
Dengeli BST (AVL/RB)
Sıralı Dizi
Arama
O(1) ort. ✓
O(log n)
O(log n) (binary)
Ekleme
O(1) ort. ✓
O(log n)
O(n) (kaydırma)
Silme
O(1) ort. ✓
O(log n)
O(n) (kaydırma)
Sıralı erişim
O(n log n) ✗
O(n) inorder ✓
O(n) ✓
Min / Max
O(n) ✗
O(log n) ✓
O(1) ✓
Aralık sorgusu
Yapamaz ✗
O(log n + k) ✓
O(log n + k)
En kötü arama
O(n)
O(log n) garanti ✓
O(log n)
Bellek
O(n) — ek yük var
O(n) — pointer'lar
O(n) — minimum ✓
Doğru yapıyı seç: • Hızlı ekleme/arama/silme → Hash Table • Sıralı erişim / aralık sorgusu / min-max → BST (AVL/RB) • Salt okunur, sıralı veri → Sıralı dizi + binary search • Hash tablo = sırasız sözlük, BST = sıralı sözlük
Programlama Dillerinde Hash Tablosu
Dil
Yapı
Çarpışma Çözümü
Varsayılan Load Factor
C (standart)
Yok — kendin yaz!
—
—
C++
std::unordered_map
Chaining
1.0
Java
HashMap
Chaining → Tree (≥8)
0.75
Python
dict
Open addressing
~0.66
C#
Dictionary<K,V>
Chaining
1.0
Go
map
Chaining (bucket)
~0.65
JavaScript
Map / {}
Değişken
Motor bağımlı
Rust
HashMap
Robin Hood hashing
~0.875
Java HashMap ilginç detay: Java 8'den itibaren bir kovanın zinciri 8 elemandan uzunsa, bağlı liste kırmızı-siyah ağaca dönüştürülür. Böylece en kötü durum O(n) → O(log n)'e düşer. Tree → liste geri dönüşümü 6 elemanda gerçekleşir.
Gerçek Dünya Kullanımları
Alan
Kullanım
Veritabanı indeksleme
Hash index — eşitlik sorguları: WHERE id = 42
Caching (önbellek)
Redis, Memcached — key-value store temelinde hash
Derleyiciler
Sembol tablosu — değişken ismi → adres / tip eşleştirmesi
DNS çözümleme
Domain adı → IP adresi önbelleği
Dosya bütünlüğü
MD5, SHA-256 ile dosya hash'i → değişiklik kontrolü
Tekrar kontrolü
Görülen elemanları set'te tut — O(1) "daha önce gördüm mü?"
Hash tablosu, anahtar-değer çiftlerini saklayan ve hash fonksiyonu sayesinde ortalama O(1) sürede ekleme, arama ve silme sağlayan temel veri yapısıdır. Hash fonksiyonu anahtarı dizi indeksine dönüştürür — arada karşılaştırma veya sıralama yoktur.
Farklı anahtarlar aynı indekse düşebilir (çarpışma). İki ana çözüm vardır: zincirleme (her kovada bağlı liste) ve açık adresleme (sonraki boş kovayı ara). Her ikisinin de trade-off'ları vardır — zincirleme daha basit ve esnek, açık adresleme daha cache-friendly.
Performansın anahtarı load factor'dür. α = n/m oranı yükseldikçe çarpışmalar artar ve performans düşer. Rehashing ile tablo genişletilerek α düşük tutulur — bu da amortized O(1) ekleme garantisi sağlar.
Hash tablosunun en büyük dezavantajı sırasız olmasıdır. Sıralı erişim, aralık sorgusu ve min-max işlemleri için BST tercih edilmelidir. Hash tablo = hızlı sözlük, BST = sıralı sözlük.
Bölüm 7.1 özeti: Hash tablosu = dizi + hash fonksiyonu. Anahtar → hash(key) % boyut → indeks → O(1) erişim. Çarpışma kaçınılmaz → chaining (bağlı liste) veya open addressing (linear/quadratic probing, double hashing). Load factor α = n/m → α ≥ 0.75'te rehash (tablo genişlet + tüm elemanları yeniden hash'le) → amortized O(1). Silme: chaining'de basit (listeden çıkar), open addressing'de tombstone gerekli. Ortalama O(1) ekleme/arama/silme — ama sıralı erişim yok. Python dict, Java HashMap, C++ unordered_map, Redis — hepsi hash tablosu. Kullanım: cache, sembol tablosu, DNS, tekrar kontrolü, sayma, URL kısaltma.