Hash Tablosu Uygulamaları

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.251.131.17Çok boş — bellek israfı
0.501.251.50İdeal
0.751.382.50Rehash eşiği (Java, Python)
0.901.455.50Kötüleşiyor
1.001.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 */
bool asalMi(int n) {
    if (n < 2) return false;
    for (int i = 2; i * i <= n; i++)
        if (n % i == 0) return false;
    return true;
}
int sonrakiAsal(int n) {
    while (!asalMi(n)) n++;
    return n;
}

/* Rehashing */
void rehash(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'le
    for (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ırak
    printf("Rehash: %d → %d (α: %.2f)\n",
           eskiBoyut, ht->size,
           (float)ht->count / ht->size);
}

/* Akıllı ekleme — α kontrollü */
void tabloEkleAkilli(HashTable* ht, const char* key, int val) {
    if ((float)ht->count / ht->size >= 0.75f)
        rehash(ht);
    tabloEkle(ht, key, val);
}

Adım Adım — Rehashing Süreci

Boyut 7 → 17'ye Rehash (6. eleman eklenince α = 6/7 = 0.86 → tetiklendi)
Eski tablo (boyut=7, α=0.86)[0] Ali:85[1] Can:78[2] Elif:88→Deniz:95[3] ∅[4] Ayşe:92[5] Fatma:70[6] ∅çarpışma: [2]'de 2 elemanrehash()O(n) — 6 elemanyeniden hash'leYeni tablo (boyut=17, α=0.35)[0] ∅ [1] ∅ [2] ∅[3] Ali:85[4] ∅ [5] ∅ [6] ∅[7] Elif:88 [8] ∅[9] Can:78 [10] Fatma:70[11] ∅[12] Deniz:95 [13] Ayşe:92[14-16] ∅çarpışma: 0 — mükemmel dağılım!

Amortized O(1) Analizi

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 boyutuRehash?Ekleme maliyeti
1–711HayırO(1) × 7 = 7
811 → 23Evet!O(1) + O(8) = 9
9–1723HayırO(1) × 9 = 9
1823 → 47Evet!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).

Örnek: Kelime Frekansı

Metin: "elma armut elma kiraz elma armut"

Adım Adım — Kelime Sayma
elma → tablo["elma"] = 1 ← yeni
armut → tablo["armut"] = 1 ← yeni
elma → tablo["elma"] = 2 ← güncelle
kiraz → tablo["kiraz"] = 1 ← yeni
elma → tablo["elma"] = 3 ← güncelle
armut → tablo["armut"] = 2 ← güncelle
Sonuç Tablosu — Frekans Dağılımı
elma
3
armut
2
kiraz
1
frekans_sayma.c — kelime frekansı
/* Dizi elemanlarının frekansını say */
void frekansSay(const char* 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);  // +1
        else
            tabloEkle(ht, kelimeler[i], 1);          // ilk kez
    }

    // Tabloyu yazdır
    for (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 */
const char* 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 */
void karakterFrekans(const char* 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 */
int ilkTekrar(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 */
void tumTekrarlar(int arr[], int n) {
    HashTable* freq = tabloOlustur(sonrakiAsal(n * 2));

    // Adım 1: Frekansları say
    for (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ır
    printf("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öntemZamanBellekDiziyi değiştirir?
Brute force (iki döngü)O(n²)O(1)Hayır
Sıralama + taramaO(n log n)O(1) veya O(n)Evet
Hash SetO(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 ekle
    HashTable* 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 tara
    int* 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:

İşlemAlgoritmaZaman
Kesişim (A ∩ B)A'yı hash'le, B'yi tara: sette varsa ekleO(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 ekleO(n + m)
Simetrik fark (A △ B)Her ikisini hash'le, sadece birinde olanları alO(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 */
bool twoSum(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);
            return true;
        }

        // arr[i]'yi tabloya ekle (değer = indeks)
        sprintf(key, "%d", arr[i]);
        tabloEkle(ht, key, i);
    }
    tabloYikici(ht);
    return false;
}
Two Sum Örneği: arr = [2, 7, 11, 15], hedef = 9
i=0: arr[0]=2, tamamlayıcı=7. Tabloda 7 var mı? Hayır. → tablo[2]=0
i=1: arr[1]=7, tamamlayıcı=2. Tabloda 2 var mı? EVET! (indeks 0)
→ return (0, 1) → arr[0] + arr[1] = 2 + 7 = 9 ✓
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)
bool anagramMi(const char* s1, const char* s2) {
    if (strlen(s1) != strlen(s2)) return false;

    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) return false;

    return true;
}

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

UygulamaAnahtarDeğerZamanAlternatif
Frekans saymaElemanSayaçO(n)O(n²) brute, O(n log n) sort
Tekrar tespitiEleman— (set)O(n)O(n²) brute, O(n log n) sort
Dizi kesişimiEleman— (set)O(n+m)O(nm) brute, O(n log n) sort
Two SumElemanİndeksO(n)O(n²) brute, O(n log n) sort
AnagramKarakterSayaçO(n)O(n log n) sort+karşılaştır
Caching/MemoGirdiÇıktıO(1) lookupYeniden hesapla
Sembol tablosuİsimAdres/TipO(1) lookupBST O(log n)
LRU CacheKeyNode ptrO(1) get/putDizi 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."
← Veri Yapıları