Hash tablosu uygulamaları: yük faktörü ve rehashing, frekans sayma, tekrar eden eleman tespiti, iki dizinin kesişimi — adım adım görseller, karmaşıklık analizi ve C kodu.
🗂️ Veri Yapıları📅 22 February 2026👁️ 15 görüntülenme
Yük Faktörü ve Yeniden Boyutlandırma (Rehashing)
Hash tablosunun performansı doğrudan yük faktörüne (load factor) bağlıdır. Yük faktörü, tablonun ne kadar dolu olduğunu gösteren orandır:
Yük Faktörü Formülü
α = n / m
n = eleman sayısı | m = kova (bucket) sayısı
α yükseldikçe çarpışma olasılığı ve ortalama arama süresi artar. Belirli bir eşik aşıldığında tablo genişletilmeli ve tüm elemanlar yeniden hash'lenmelidir — bu işleme rehashing denir.
Load Factor'ün Performansa Etkisi
α
Chaining (ort. arama)
Linear Probing (ort. arama)
Durum
0.25
1.13
1.17
Çok boş — bellek israfı
0.50
1.25
1.50
İdeal
0.75
1.38
2.50
Rehash eşiği (Java, Python)
0.90
1.45
5.50
Kötüleşiyor
1.00
1.50
∞ (dolu!)
Open addr. çalışamaz!
Rehashing Mekanizması — Detaylı
Rehashing ne zaman tetiklenir? Her insert() sonrası α kontrol edilir. α ≥ eşik (tipik: 0.75) ise: 1. Yeni boyut hesapla: yeniBoyut = sonrakiAsal(eskiBoyut × 2) 2. Yeni dizi oluştur (calloc) 3. Eski tablodaki her elemanı yeni hash fonksiyonu ile yeniden hesapla ve ekle 4. Eski diziyi free() Maliyet: O(n) — ama nadiren yapılır → amortized O(1) garanti.
rehash.c — tam implementasyon
/* Bir sonraki asal sayıyı bul */boolasalMi(int n) {
if (n < 2) returnfalse;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) returnfalse;
returntrue;
}
intsonrakiAsal(int n) {
while (!asalMi(n)) n++;
return n;
}
/* Rehashing */voidrehash(HashTable* ht) {
int eskiBoyut = ht->size;
Entry** eskiBuckets = ht->buckets;
// 1) Yeni boyut: ~2× eski, en yakın asal
ht->size = sonrakiAsal(eskiBoyut * 2);
ht->buckets = calloc(ht->size, sizeof(Entry*));
ht->count = 0;
// 2) Tüm elemanları yeniden hash'lefor (int i = 0; i < eskiBoyut; i++) {
Entry* curr = eskiBuckets[i];
while (curr) {
Entry* next = curr->next;
// Yeni indeks hesapla (boyut değişti!)unsigned int yeniIdx = hash(curr->key, ht->size);
curr->next = ht->buckets[yeniIdx];
ht->buckets[yeniIdx] = curr;
ht->count++;
curr = next;
}
}
free(eskiBuckets); // 3) Eski diziyi serbest bırakprintf("Rehash: %d → %d (α: %.2f)\n",
eskiBoyut, ht->size,
(float)ht->count / ht->size);
}
/* Akıllı ekleme — α kontrollü */voidtabloEkleAkilli(HashTable* ht, constchar* key, int val) {
if ((float)ht->count / ht->size >= 0.75f)
rehash(ht);
tabloEkle(ht, key, val);
}
Rehashing O(n) maliyetindedir — ama her ekleme sonrası yapılmaz. Tablo her seferinde 2× büyüdüğü için rehash sıklığı giderek azalır:
İşlem #
Tablo boyutu
Rehash?
Ekleme maliyeti
1–7
11
Hayır
O(1) × 7 = 7
8
11 → 23
Evet!
O(1) + O(8) = 9
9–17
23
Hayır
O(1) × 9 = 9
18
23 → 47
Evet!
O(1) + O(18) = 19
Toplam 18 ekleme:
44 işlem
Ekleme başına ortalama:
44/18 ≈ 2.4 = O(1)
Amortized analiz: n ekleme boyunca toplam maliyet = n (normal eklemeler) + n/2 + n/4 + n/8 + ... (rehash'ler) ≤ 3n. Ekleme başına ortalama maliyet = 3n/n = O(1). Her ekleme arada sırada pahalı olsa da, uzun vadede ortalama sabit kalır.
Küçültme (shrinking): Çoğu implementasyon silme sonrası α çok düşerse (tipik: α < 0.25) tabloyu küçültür. Böylece bellek israfı önlenir. Ancak bu opsiyoneldir — bazı sistemler sadece büyütme yapar.
Uygulama 1: Frekans Sayma (Frequency Counting)
Hash tablosunun en yaygın uygulaması frekans sayma'dır: bir koleksiyondaki her elemanın kaç kez geçtiğini saymak. Anahtar = eleman, değer = sayaç. Karşılaşılan her eleman için tabloda varsa sayaç +1, yoksa sayaç = 1 olarak ekle.
Hash olmadan — O(n²)
Her eleman için tüm diziyi tara, sayacı tut. İç içe iki döngü → yavaş.
Hash tablosu ile — O(n)
Tek geçiş: her eleman → hash → sayaç++. Toplam n ekleme, her biri O(1).
/* Dizi elemanlarının frekansını say */voidfrekansSay(constchar* kelimeler[], int n) {
HashTable* ht = tabloOlustur(17);
for (int i = 0; i < n; i++) {
bool found;
int count = tabloAra(ht, kelimeler[i], &found);
if (found)
tabloEkle(ht, kelimeler[i], count + 1); // +1elsetabloEkle(ht, kelimeler[i], 1); // ilk kez
}
// Tabloyu yazdırfor (int i = 0; i < ht->size; i++) {
Entry* e = ht->buckets[i];
while (e) {
printf(" %-10s : %d\n", e->key, e->value);
e = e->next;
}
}
}
/* Kullanım */constchar* kelimeler[] = {
"elma", "armut", "elma",
"kiraz", "elma", "armut"
};
frekansSay(kelimeler, 6);
// Çıktı:
// elma : 3
// armut : 2
// kiraz : 1
Karakter Frekansı — Tam Sayı Hash
String yerine tam sayı anahtarlar kullanarak karakter frekansı saymak daha da basittir. ASCII karakterlerin kendisi doğrudan indeks olarak kullanılabilir (boyut 256 dizi = mükemmel hash).
karakter_frekans.c — O(n)
/* Karakter frekansı — basit dizi tabanlı hash */voidkarakterFrekans(constchar* str) {
int freq[256] = {0}; // ASCII "hash tablosu"for (int i = 0; str[i]; i++)
freq[(unsigned char)str[i]]++;
printf("Karakter frekansları:\n");
for (int i = 0; i < 256; i++)
if (freq[i] > 0)
printf(" '%c' : %d\n", i, freq[i]);
}
karakterFrekans("merhaba dünya");
// 'a' : 2, 'b' : 1, 'd' : 1, 'e' : 1, ...
Frekans sayma kullanım alanları: kelime bulutu (word cloud), log analizi, metin sınıflandırma (TF-IDF), veri analizi (histogram), anagram kontrolü, en sık k eleman (top-k), sıkıştırma (Huffman kodlama).
Uygulama 2: Tekrar Eden Eleman Tespiti (Duplicate Detection)
Bir dizide tekrar eden eleman var mı? Hash tablosu bunu O(n)'de çözer — her elemanı tabloya eklerken zaten var mı kontrol et. Sıralama gerektirmez, ek dizi gerektirmez.
Brute Force — O(n²)
İç içe iki döngü: her çifti karşılaştır. 1M eleman → 1012 karşılaştırma 💀
Sıralama — O(n log n)
Diziyi sırala, ardışık eşitlik kontrol et. Daha iyi ama diziyi değiştiriyor.
Hash Set — O(n)
Her elemanı hash'e ekle, zaten varsa "tekrar!" 1M eleman → 1M lookup. En hızlı.
Hash Set Kavramı
Hash Set, hash tablosunun sadece anahtar saklayan versiyonudur — değer yoktur. Tek amaç: "bu eleman sette var mı?" sorusuna O(1)'de cevap vermek. Tekrar tespitinde anahtar = eleman, değer gerekmez.
tekrar_tespit.c — Hash Set ile O(n)
/* Dizide tekrar eden ilk elemanı bul */intilkTekrar(int arr[], int n) {
HashTable* set = tabloOlustur(sonrakiAsal(n * 2));
for (int i = 0; i < n; i++) {
char key[20];
sprintf(key, "%d", arr[i]);
bool found;
tabloAra(set, key, &found);
if (found) {
tabloYikici(set);
return arr[i]; // tekrar bulundu!
}
tabloEkle(set, key, 1); // sete ekle
}
tabloYikici(set);
return -1; // tekrar yok
}
/* Tüm tekrar eden elemanları bul */voidtumTekrarlar(int arr[], int n) {
HashTable* freq = tabloOlustur(sonrakiAsal(n * 2));
// Adım 1: Frekansları sayfor (int i = 0; i < n; i++) {
char key[20];
sprintf(key, "%d", arr[i]);
bool found;
int cnt = tabloAra(freq, key, &found);
tabloEkle(freq, key, found ? cnt + 1 : 1);
}
// Adım 2: Frekansı > 1 olanları yazdırprintf("Tekrar eden elemanlar:\n");
for (int i = 0; i < freq->size; i++) {
Entry* e = freq->buckets[i];
while (e) {
if (e->value > 1)
printf(" %s (%d kez)\n", e->key, e->value);
e = e->next;
}
}
}
Adım Adım — Tekrar Tespiti
Dizi: [5, 3, 8, 5, 2, 8, 1, 3]
Hash Set ile Tekrar Tespiti — 8 Eleman
i=0:5 → sette yok → ekle → set: {5} i=1:3 → sette yok → ekle → set: {5, 3} i=2:8 → sette yok → ekle → set: {5, 3, 8} i=3:5 → sette VAR! → TEKRAR! return 5 Kalan elemanlar kontrol edilmedi — ilk tekrar bulundu, erken çıkış.
Tekrar Tespiti — Karmaşıklık
Yöntem
Zaman
Bellek
Diziyi değiştirir?
Brute force (iki döngü)
O(n²)
O(1)
Hayır
Sıralama + tarama
O(n log n)
O(1) veya O(n)
Evet
Hash Set
O(n)
O(n)
Hayır ✓
Uygulama 3: İki Dizinin Kesişimi (Array Intersection)
İki dizinin ortak elemanlarını bulmak: veritabanı JOIN, küme işlemleri, öneri sistemleri ve filtreleme gibi pek çok alanda kullanılır. Hash tablosu bu işlemi O(n + m)'de çözer.
Brute Force — O(n × m)
Her eleman çifti kontrol edilir. 1000 × 1000 = 1M karşılaştırma.
Sıralama — O(n log n + m log m)
İki diziyi sırala, iki pointer ile tara (merge-like).
Hash Set — O(n + m)
Küçük diziyi hash'le, büyük diziyi tara. En hızlı — doğrusal!
Algoritma
Hash ile kesişim — 2 adım: 1. Birinci diziyi hash set'e ekle → O(n) 2. İkinci diziyi tara: her eleman sette varsa → kesişimde → O(m) Toplam: O(n + m) zaman, O(min(n, m)) bellek. İpucu: Küçük diziyi hash'lemek bellek kullanımını minimize eder.
Adım Adım — Kesişim Bulma
A = [3, 7, 1, 5, 9, 4] B = [2, 5, 8, 3, 6, 4, 10]
Adım 1: A'yı Hash Set'e Ekle
0
—
1
1
2
—
3
3
4
4
5
5
6
—
7
7
8
—
9
9
10
—
A'nın 6 elemanı set'e eklendi — O(6)
Adım 2: B'yi Tara — Sette Var mı?
B[0] = 2 → sette yok → atla B[1] = 5 → sette VAR → kesişim += 5 ✓ B[2] = 8 → sette yok → atla B[3] = 3 → sette VAR → kesişim += 3 ✓ B[4] = 6 → sette yok → atla B[5] = 4 → sette VAR → kesişim += 4 ✓ B[6] = 10 → sette yok → atla
Kesişim = {3, 4, 5} — 3 ortak eleman bulundu!
kesisim.c — O(n + m)
/* İki dizinin kesişimi — hash set ile */int* diziKesisim(int A[], int n,
int B[], int m,
int* sonucBoyut) {
// Küçük diziyi hash'le (bellek tasarrufu)int* kucuk = n <= m ? A : B;
int kBoy = n <= m ? n : m;
int* buyuk = n <= m ? B : A;
int bBoy = n <= m ? m : n;
// Adım 1: Küçük diziyi set'e ekleHashTable* set = tabloOlustur(sonrakiAsal(kBoy * 2));
for (int i = 0; i < kBoy; i++) {
char key[20];
sprintf(key, "%d", kucuk[i]);
tabloEkle(set, key, 1);
}
// Adım 2: Büyük diziyi taraint* sonuc = malloc(kBoy * sizeof(int));
int idx = 0;
for (int i = 0; i < bBoy; i++) {
char key[20];
sprintf(key, "%d", buyuk[i]);
bool found;
tabloAra(set, key, &found);
if (found) {
sonuc[idx++] = buyuk[i];
tabloSil(set, key); // tekrar eklemeyi önle
}
}
*sonucBoyut = idx;
tabloYikici(set);
return sonuc;
}
/* Kullanım */int A[] = {3, 7, 1, 5, 9, 4};
int B[] = {2, 5, 8, 3, 6, 4, 10};
int boyut;
int* sonuc = diziKesisim(A, 6, B, 7, &boyut);
// sonuc = [5, 3, 4], boyut = 3
Küme İşlemleri — Hash ile O(n + m)
Kesişim aynı teknikle birleşim (union) ve fark (difference) hesaplamak da mümkündür:
İşlem
Algoritma
Zaman
Kesişim (A ∩ B)
A'yı hash'le, B'yi tara: sette varsa ekle
O(n + m)
Birleşim (A ∪ B)
A'yı hash'le, B'yi hash'le (var olanı atla)
O(n + m)
Fark (A \ B)
B'yi hash'le, A'yı tara: sette yoksa ekle
O(n + m)
Simetrik fark (A △ B)
Her ikisini hash'le, sadece birinde olanları al
O(n + m)
Ek Uygulamalar — Hash Tablosu Klasikleri
Two Sum Problemi
Klasik mülakat sorusu: bir dizide toplamı hedef sayıya eşit olan iki elemanın indekslerini bul. Hash tablosu ile tek geçişte O(n)'de çözülür.
two_sum.c — O(n)
/* İki elemanın toplamı = hedef → indeksleri döndür */booltwoSum(int arr[], int n, int hedef,
int* idx1, int* idx2) {
HashTable* ht = tabloOlustur(sonrakiAsal(n * 2));
for (int i = 0; i < n; i++) {
int tamamlayici = hedef - arr[i];
char key[20];
sprintf(key, "%d", tamamlayici);
bool found;
int j = tabloAra(ht, key, &found);
if (found) {
*idx1 = j; *idx2 = i;
tabloYikici(ht);
returntrue;
}
// arr[i]'yi tabloya ekle (değer = indeks)sprintf(key, "%d", arr[i]);
tabloEkle(ht, key, i);
}
tabloYikici(ht);
returnfalse;
}
Mantık: "Ben 7'yim, 2'ye ihtiyacım var. Daha önce 2 gördün mü?" → Hash tablosuna sor → O(1).
Anagram Kontrolü
İki string anagram mı? (aynı karakterlerden mi oluşuyor?) → Her ikisinin karakter frekanslarını hash tablosu ile say ve karşılaştır.
anagram.c — O(n)
boolanagramMi(constchar* s1, constchar* s2) {
if (strlen(s1) != strlen(s2)) returnfalse;
int freq[256] = {0}; // karakter hash tablosu// s1'in karakterlerini say (+)for (int i = 0; s1[i]; i++)
freq[(unsigned char)s1[i]]++;
// s2'nin karakterlerini çıkar (-)for (int i = 0; s2[i]; i++)
freq[(unsigned char)s2[i]]--;
// Tüm sayaçlar 0 olmalıfor (int i = 0; i < 256; i++)
if (freq[i] != 0) returnfalse;
returntrue;
}
printf("%d\n", anagramMi("listen", "silent")); // 1 (true)printf("%d\n", anagramMi("hello", "world")); // 0 (false)
"listen" vs "silent" — Anagram Kontrolü
"listen" (+)
e:1 i:1 l:1 n:1 s:1 t:1
−
"silent" (−)
e:1 i:1 l:1 n:1 s:1 t:1
=
Fark
tümü 0 → Anagram ✓
Hash Tablosu Uygulamaları — Genel Tablo
Uygulama
Anahtar
Değer
Zaman
Alternatif
Frekans sayma
Eleman
Sayaç
O(n)
O(n²) brute, O(n log n) sort
Tekrar tespiti
Eleman
— (set)
O(n)
O(n²) brute, O(n log n) sort
Dizi kesişimi
Eleman
— (set)
O(n+m)
O(nm) brute, O(n log n) sort
Two Sum
Eleman
İndeks
O(n)
O(n²) brute, O(n log n) sort
Anagram
Karakter
Sayaç
O(n)
O(n log n) sort+karşılaştır
Caching/Memo
Girdi
Çıktı
O(1) lookup
Yeniden hesapla
Sembol tablosu
İsim
Adres/Tip
O(1) lookup
BST O(log n)
LRU Cache
Key
Node ptr
O(1) get/put
Dizi O(n)
Sonuç
Hash tablosu O(1) ortalama erişim süresiyle birçok problemi dramatik şekilde hızlandırır. Ancak bu gücü doğru yük faktörü yönetimi ile birlikte gelir: α yükseldikçe performans düşer, rehashing ile tablo genişletilerek α kontrol altında tutulur — bu da amortized O(1) garanti sağlar.
Hash tablosunun pratik uygulamaları çok geniştir: frekans sayma tek geçişte eleman sayımı sağlar, tekrar tespiti Hash Set ile O(n)'de yapılır, dizi kesişimi küçük diziyi hash'leyip büyüğü tarayarak O(n+m)'de çözülür. Two Sum, anagram kontrolü, caching ve sembol tablosu gibi klasik problemler de hash tablosunun doğal uygulama alanlarıdır.
Her uygulamada ortak desen: bir şeyi O(1)'de sormak istiyorsanız, onu önce hash tablosuna koyun. "Bu elemanı daha önce gördüm mü?", "Bu anahtarın değeri nedir?", "Bu elemanın tamamlayıcısı var mı?" — tüm bu soruların cevabı hash tablosundadır.
Bölüm 7.3 özeti:Load factor α = n/m → α ≥ 0.75'te rehash: boyut 2× büyüt, tüm elemanları yeniden hash'le → amortized O(1). n ekleme toplam maliyeti ≤ 3n. Frekans sayma: anahtar=eleman, değer=sayaç, tek geçiş O(n) — kelime bulutu, histogram, Huffman. Tekrar tespiti: Hash Set (sadece anahtar), her ekleme öncesi "var mı?" sor → O(n), bellek O(n). Dizi kesişimi: küçük diziyi hash'le, büyüğü tara → O(n+m). Aynı teknikle birleşim, fark, simetrik fark. Two Sum: tamamlayıcıyı hash'te ara → O(n). Anagram: karakter frekansları eşit mi → O(n). Ortak desen: "O(1)'de sormak istiyorsan, önce hash'e koy."