Hash Tabloları Hash Temelleri

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.

🗂️ Data Structures 📅 22 February 2026 👁️ 6 views

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ü
"Ali""Ayşe""Can"hash(key)% tablo_boyutu→ 3→ 7→ 1

İyi Hash Fonksiyonunun 4 Özelliği

ÖzellikAçıklamaNeden Önemli?
BelirleyicilikAynı girdi her zaman aynı çıktıyı verirArama tutarlı olmalı
Düzgün dağılımÇıktılar tablo boyunca eşit yayılırÇarpışmaları minimize eder
HızHesaplama O(1) veya O(m) olmalıHash hesabı darboğaz olmamalı
Çığ etkisiKüçü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 int hashBolme(const char* 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 int hashDJB2(const char* key, int tablo_boyutu) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++))
        hash = hash * 33 + c;  // hash × 33 + karakter
    return hash % tablo_boyutu;
}

/* 3) Tam sayı anahtarlar için — Knuth çarpma */
unsigned int hashKnuth(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>

#define TABLO_BOYUTU  11  // asal sayı!

/* Anahtar-değer çifti */
typedef struct Entry {
    char*  key;            // anahtar (string)
    int    value;           // değer
    struct Entry* next;     // çarpışma zinciri (chaining)
} Entry;

/* Hash tablosu */
typedef struct {
    Entry** buckets;       // kova dizisi
    int     size;           // tablo boyutu
    int     count;          // eleman sayısı
} HashTable;

/* Hash fonksiyonu — DJB2 */
unsigned int hash(const char* 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
void tabloEkle(HashTable* ht, const char* key, int value) {
    unsigned int idx = hash(key, ht->size);

    // Aynı anahtar varsa güncelle
    Entry* curr = ht->buckets[idx];
    while (curr) {
        if (strcmp(curr->key, key) == 0) {
            curr->value = value;  // güncelle
            return;
        }
        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++;
}

Adım Adım — 5 Öğrenci Notu Ekleme

Hash fonksiyonu sonuçları (DJB2, tablo boyutu = 11):

AnahtarHash Hesabıİndeks
"Ali"hash("Ali") % 113
"Ayşe"hash("Ayşe") % 117
"Can"hash("Can") % 111
"Deniz"hash("Deniz") % 115
"Elif"hash("Elif") % 113 ← çarpışma!
① "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
0NULL1Can : 78→ NULL2NULL3Elif : 88Ali : 85→ NULL4NULL5Deniz : 956NULL7Ayşe : 928-10NULL ...
İ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.

tabloAra() — O(1) ortalama
int tabloAra(HashTable* ht, const char* key, bool* found) {
    unsigned int idx = hash(key, ht->size);
    Entry* curr = ht->buckets[idx];

    while (curr) {
        if (strcmp(curr->key, key) == 0) {
            *found = true;
            return curr->value;  // bulundu!
        }
        curr = curr->next;
    }
    *found = false;
    return -1;  // bulunamadı
}

Arama Adımları — 3 Farklı Senaryo

3 Arama Senaryosu
① search("Ayşe") → İndeks 7 → Direkt bulundu ✓
hash("Ayşe") = 7 → buckets[7] → "Ayşe":92 → strcmp eşleşti → return 92
1 hash hesabı + 1 strcmp = O(1)
② search("Ali") → İndeks 3 → Zincirde 2. sırada ✓
hash("Ali") = 3 → buckets[3] → "Elif":88 → strcmp ✗ → next → "Ali":85 → strcmp ✓ → return 85
1 hash + 2 strcmp = O(k), k = zincir uzunluğu
③ 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.

tabloSil() — O(1) ortalama
bool tabloSil(HashTable* ht, const char* key) {
    unsigned int idx = hash(key, ht->size);
    Entry* curr = ht->buckets[idx];
    Entry* prev = NULL;

    while (curr) {
        if (strcmp(curr->key, key) == 0) {
            // Bağlı listeden çıkar
            if (prev)
                prev->next = curr->next;
            else
                ht->buckets[idx] = curr->next;

            free(curr->key);
            free(curr);
            ht->count--;
            return true;   // silindi
        }
        prev = curr;
        curr = curr->next;
    }
    return false;  // bulunamadı
}

Adım Adım — "Elif" Silme

delete("Elif") → İndeks 3 → Zincir başından çıkar
Önce (indeks 3)
Elif:88 → Ali:85 → NULL
Sonra (indeks 3)
Ali:85 → NULL
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öntemFormülAvantajDezavantaj
Linear Probing(h + i) % nBasit, cache-friendlyKümeleme (clustering): dolu bölgeler büyür
Quadratic Probing(h + i²) % nKümelemeyi azaltırTüm kovaları ziyaret etmeyebilir
Double Hashing(h₁ + i×h₂) % nEn 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

ÖzellikChainingOpen Addressing
DepolamaDizi + bağlı listelerSadece dizi
Çarpışma çözümüListeye ekleSonraki boş kovayı bul
Load factor > 1Sorunsuz çalışır ✓İmkansız ✗
SilmeBasit (listeden çıkar) ✓Tombstone gerekli ✗
Cache performansıDüşük (pointer takibi)Yüksek (ardışık bellek) ✓
Bellek ek yüküPointer başına 8 byteSıfır ✓
En kötü aramaO(n) — tek zincirO(n) — tüm dizi
KullanımJava HashMap, Go mapPython 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)DurumPerformans
α < 0.5Bol yer varMükemmel — O(1)'e çok yakın
0.5 ≤ α < 0.75Dengeliİyi — hâlâ O(1) civarı
α ≥ 0.75Kalabalık!Kötüleşiyor — çarpışma artıyor
α > 1.0 (chaining)TaşmaZincirler uzar → O(α) arama
α → 1.0 (open addr.)Neredeyse doluFelaket — 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
void rehash(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'le
    for (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 */
void tabloEkleAkilli(HashTable* ht, const char* key, int val) {
    if ((float)ht->count / ht->size >= 0.75f)
        rehash(ht);   // α ≥ 0.75 → genişlet
    tabloEkle(ht, key, val);
}
Rehashing: Boyut 7 → 17 (α = 6/7 ≈ 0.86 → tetiklendi)
Eski (boyut=7, α=0.86)[0] Ali:85[1] NULL[2] Can:78 → Elif:88[3] Deniz:95[4-6] ...→ rehash →Yeni (boyut=17, α=0.35)[3] Ali:85[7] Elif:88[9] Can:78[12] Deniz:95... (çarpışma yok!)
Boyut büyüdü → tüm elemanlar yeniden dağıtıldı → çarpışmalar çözüldü → α düştü.

Tam Demo Program

main() — tüm operasyonlar
int main() {
    HashTable* ht = tabloOlustur(11);

    // ── Ekleme ──
    printf("=== Hash Table Demo ===\n\n");
    tabloEkle(ht, "Ali",   85);
    tabloEkle(ht, "Ayşe",  92);
    tabloEkle(ht, "Can",   78);
    tabloEkle(ht, "Deniz", 95);
    tabloEkle(ht, "Elif",  88);
    printf("5 eleman eklendi. count=%d, size=%d\n",
           ht->count, ht->size);

    // ── Arama ──
    printf("\n--- Arama ---\n");
    bool found;
    int val;
    const char* aramalar[] = {"Ali","Can","Zeynep","Elif"};
    for (int i = 0; i < 4; i++) {
        val = tabloAra(ht, aramalar[i], &found);
        if (found)
            printf("  %s → %d ✓\n", aramalar[i], val);
        else
            printf("  %s → bulunamadı ✗\n", aramalar[i]);
    }

    // ── Güncelleme ──
    printf("\n--- Güncelleme ---\n");
    tabloEkle(ht, "Ali", 90);  // 85 → 90
    val = tabloAra(ht, "Ali", &found);
    printf("  Ali güncellendi: %d\n", val);

    // ── Silme ──
    printf("\n--- Silme ---\n");
    printf("  delete(\"Elif\"): %s\n",
           tabloSil(ht,"Elif") ? "silindi ✓" : "yok");
    printf("  delete(\"Zeynep\"): %s\n",
           tabloSil(ht,"Zeynep") ? "silindi" : "bulunamadı ✗");
    printf("  search(\"Elif\"): %s\n",
           tabloAra(ht,"Elif",&found)?"var":"yok ✗");
    printf("  Kalan eleman: %d\n", ht->count);

    return 0;
}

Program Çıktısı

çıktı
=== 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.

OperasyonOrtalamaEn 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
RehashingO(n)Tüm elemanlar yeniden hash'lenir
Min / Max bulmaO(n)Sıra yok → tüm tabloyu tara
Sıralı erişimO(n log n)Önce topla, sonra sırala — hash tablo sıralı değildir
BellekO(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ı

ÖzellikHash TableDengeli BST (AVL/RB)Sıralı Dizi
AramaO(1) ort. ✓O(log n)O(log n) (binary)
EklemeO(1) ort. ✓O(log n)O(n) (kaydırma)
SilmeO(1) ort. ✓O(log n)O(n) (kaydırma)
Sıralı erişimO(n log n) ✗O(n) inorder ✓O(n) ✓
Min / MaxO(n) ✗O(log n) ✓O(1) ✓
Aralık sorgusuYapamaz ✗O(log n + k) ✓O(log n + k)
En kötü aramaO(n)O(log n) garanti ✓O(log n)
BellekO(n) — ek yük varO(n) — pointer'larO(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

DilYapıÇarpışma ÇözümüVarsayılan Load Factor
C (standart)Yok — kendin yaz!
C++std::unordered_mapChaining1.0
JavaHashMapChaining → Tree (≥8)0.75
PythondictOpen addressing~0.66
C#Dictionary<K,V>Chaining1.0
GomapChaining (bucket)~0.65
JavaScriptMap / {}DeğişkenMotor bağımlı
RustHashMapRobin 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ı

AlanKullanım
Veritabanı indekslemeHash index — eşitlik sorguları: WHERE id = 42
Caching (önbellek)Redis, Memcached — key-value store temelinde hash
DerleyicilerSembol tablosu — değişken ismi → adres / tip eşleştirmesi
DNS çözümlemeDomain 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ü?"
Sayma (counting)Kelime frekansı, karakter sayımı, histogram
YönlendirmeURL kısaltma (bit.ly), load balancer (consistent hashing)

Sonuç

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.
← Data Structures