6  Hashing

Karşılaştırma modeli alt sınırı, doğrudan erişim dizisi, hash fonksiyonu, çakışma + zincirleme ve evrensel hashing ile beklenen \(O(1)\)

NotBölüm bilgisi

6.1 Bu Derste Ne Var?

Ders 3, kümeyi (set) sıralı diziyle gerçekleştirip aramayı \(O(\log n)\)’e indirdi. Bu ders tek bir soruyla başlar: daha hızlı olur mu? Jason Ku önce olamaz der — karşılaştırma modelinde \(\log n\) bir alt sınırdır — sonra modeli genişletip olur der: hashing ile find/insert/delete beklenen \(O(1)\).

Üç temel kavram bu derste yan yana gelir:

  1. Karşılaştırma modeli alt sınırı — yalnızca anahtarları karşılaştıran her algoritma için arama \(\Omega(\log n)\)’dir (karar ağacı argümanı).
  2. Direct access array → hash tablosu — anahtarı doğrudan adres yap; evren çok büyükse bir hash fonksiyonuyla küçült.
  3. Evrensel hashing — rastgele seçilen bir hash ailesi, beklenen zincir uzunluğunu sabit yapar.

“Today we are going to be talking about hashing.” — Ku, 0:20

flowchart TD
    A["Ders 5 (L4): Hashing"] --> B["Karşılaştırma modeli<br/>alt sınır Ω(log n)"]
    A --> C["Direct access array"]
    A --> D["Hash fonksiyonu"]
    B --> B1["Karar ağacı<br/>n+1 yaprak → yükseklik log n"]
    B --> B2["Daha hızlısı için<br/>rastgele erişim gerek"]
    C --> C1["anahtar = indeks<br/>O(1) en kötü durum"]
    C --> C2["evren u kadar yer<br/>n ≪ u → israf"]
    D --> D1["h: 0..u−1 → 0..m−1<br/>(m ≈ n)"]
    D --> D2["çakışma (pigeonhole)<br/>→ chaining"]
    D2 --> E["Evrensel hashing<br/>P(çakışma) ≤ 1/m"]
    E --> F["Beklenen zincir<br/>1 + (n−1)/m → O(1)"]

    classDef root fill:#fef3c7,stroke:#b45309,stroke-width:3px,color:#1f2937
    classDef branch fill:#f3f4f6,stroke:#374151,stroke-width:2px,color:#1f2937
    classDef leaf fill:#ffffff,stroke:#9ca3af,stroke-width:1px,color:#1f2937
    classDef win fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1f2937
    class A root
    class B,C,D branch
    class B1,B2,C1,C2,D1,D2 leaf
    class E,F win
Şekil 6.1: Ders 5’in (L4) kavram haritası: karşılaştırma modelinin Ω(log n) sınırından, doğrudan erişim dizisine (O(1) ama u kadar yer), oradan hash fonksiyonuna (m ≈ n), çakışmaya ve zincirleme + evrensel hashing yoluyla beklenen O(1)’e.
İpucuBuilder Notu — Hash Tablosu = Python dict ve Randomized Algoritma

Bu ders ML değil, ama ML’in ve her ciddi yazılımın altındaki erişim mekanizmasını ve olasılıksal analizi kurar:

  • Geriye → Python (Phase 1): Python dict/set bir hash tablosudur (open addressing şeması). Bu ders onun iç mantığını ve “neden bazen yavaşladığını” açıklar — bedava görünen d[key] çağrısının arkasındaki \(O(1)\) buradan gelir.
  • Geriye → Stat 110 (Phase 1): dersin kalbi olasılıktır — gösterge rastgele değişkeni (indicator RV), beklenen değer, beklentinin doğrusallığı (linearity of expectation). Evrensel hashing, randomized algoritmanın bu kursta gördüğün ilk ciddi örneğidir: garanti “her girdide” değil, “beklenen” anlamda verilir.
  • İleriye → ML ve sistem tasarımı: feature hashing, embedding indeksleme, cache, veritabanı indeksi, dağıtık sistemlerde consistent hashing — hepsi anahtar → kova \(O(1)\) erişimine dayanır.

Tek cümle: Sıralamayı bırakıp anahtarı doğrudan adrese “serpiştirirsek” arama \(O(1)\) olur — yeter ki hash fonksiyonunu rastgele seçip çakışmayı beklenen anlamda küçük tutalım.

6.2 Set’i \(O(\log n)\)’den Hızlı Yapabilir miyiz?

Sıralı dizi, find(k)’yi ikili aramayla \(O(\log n)\) yapar. \(\log n\) zaten küçüktür (gerçek problemlerde ~30’u geçmez), ama Ku daha hızlısını ister. Soru iki parçalıdır: (1) belirli bir modelde \(O(\log n)\)’den hızlı mümkün mü, (2) değilse modeli nasıl değiştiririz?

Cevap: karşılaştırma modelinde mümkün değildir (Bölüm 6.3). Ama word RAM modelinin rastgele erişim gücünü kullanırsak mümkündür (Bölüm 6.4 ve sonrası).

6.3 Karşılaştırma Modeli ve Alt Sınır

Karşılaştırma modeli: sakladığımız nesneleri “kara kutu” gibi düşünürüz; onları ayırt etmenin tek yolu iki anahtarı karşılaştırmaktır (eşit mi, büyük mü, küçük mü). Ders 3’teki üç sıralama da bu modeldeydi.

“the only way that I can distinguish between them is to say… I can do a comparison on those keys.” — Ku, 5:15

Bir karşılaştırma algoritmasını karar ağacı (decision tree) olarak çiz (Şekil 6.2): iç düğümler karşılaştırmalardır (iki dallı: doğru/yanlış), yapraklar çıktılardır. En kötü durumdaki karşılaştırma sayısı = en uzun kök-yaprak yolu = ağacın yüksekliği.

Çalışılan Örnek — \(\log n\) alt sınırı. Doğru bir arama algoritması, sakladığı \(n\) öğeden herhangi birini veya “yok”u döndürebilmeli → en az \(n + 1\) yaprak gerekir. İkili bir ağacın \(n+1\) yaprağı varsa, minimum yüksekliği \(\log(n+1)\)’dir (en iyi durum: dengeli ağaç).

“the minimum height of any binary tree that has at least n plus 1 leaves… it’s log n.” — Ku, 12:44

Demek ki yalnızca karşılaştırmayla, arama \(\Omega(\log n)\) sürer. Daha hızlısı için karşılaştırmadan güçlü bir işlem gerekir.

Şekil 6.2: Karşılaştırma modeli ve aramanın \(\Omega(\log n)\) alt sınırı. Yalnızca karşılaştırma yapabilen herhangi bir arama algoritması bir ikili karar ağacı olarak çizilebilir: her iç düğüm bir anahtar karşılaştırmasıdır (slate daire, iki dallı — örn. \(A[mid] < k\)), ve algoritmanın verebileceği her olası çıktı bir yapraktır (amber kutu). \(n\) öğelik bir kümede arama \(n\) farklı öğeden birini ya da “yok”u döndürebilmeli → en az \(n+1\) yaprak gerekir. Burada \(n = 7\), yani \(8\) yaprak. İkili bir ağacın yüksekliği \(h\) ise en fazla \(2^h\) yaprağı olur → \(2^h \ge n+1 \Rightarrow h \ge \log_2(n+1)\), yani minimum yükseklik \(\lceil \log_2(n+1) \rceil = 3\). En kötü durumdaki karşılaştırma sayısı = en uzun kök-yaprak yolu = ağacın yüksekliği → karşılaştırmaya dayalı arama \(\Omega(\log n)\)’dir. İkili arama (\(O(\log n)\), Şekil 5.3) bu alt sınıra ulaşır; hash tabloları ise karşılaştırma modelinden çıkıp anahtarı doğrudan indekse çevirerek beklenen \(O(1)\)’e iner.

6.4 Direct Access Array (Doğrudan Erişim Dizisi)

Karşılaştırma sabit dallanma (2 yol) verir; bize sabit-olmayan dallanma lazım. word RAM’in rastgele erişimi tam bunu sağlar: bir sayıyla belleğin herhangi bir yerine sabit zamanda gidebiliriz.

Fikir: anahtarı \(k\) olan öğeyi doğrudan \(k\). indekse koy. Anahtarı 10 olan öğe, indeks 10’da durur (Şekil 6.3).

“This is what I call a direct access array… It takes constant time, worst case.” — Ku, 16:24

find, insert, delete: hepsi \(O(1)\) en kötü durum. Mükemmel görünüyor — ama bir sorun var.

6.5 Problem: Anahtar Evreni Çok Büyük

Direct access array, anahtar evreni kadar yer ister. \(u\) = saklanabilecek en büyük anahtarın boyutu (anahtar evreninin büyüklüğü). MIT ID’leri 9 haneli → \(u \approx 10^9\), oysa sınıfta yalnızca \(n \approx 300\) kişi var. \(u\), \(n\)’den çok büyükse, çoğu hücre boş kalır — büyük bellek israfı (Şekil 6.3).

“u is the maximum size of any number that I’m storing. It’s the size of the universe of space of keys” — Ku, 19:09

İki varsayım gerekir: (1) anahtarlar tam sayı olmalı (adres olarak kullanılacak); (2) \(u < 2^w\) (\(w\) = word boyutu), ki anahtar bir word’e sığıp sabit zamanda erişilebilsin.

Şekil 6.3: Doğrudan erişim dizisi (direct access array): bir öğenin anahtarı doğrudan dizi indeksi olarak kullanılır — A[k]’ye ofset aritmetiğiyle erişilir, dolayısıyla insert/find/delete en kötü durumda \(O(1)\) (karşılaştırma yok). Bedeli bellektir: dizinin uzunluğu, anahtar evreni \(u\) kadar olmak zorundadır. Burada temsilî olarak \(u = 40\) hücre ayrılmış ama yalnızca \(n = 4\) anahtar (5, 12, 23, 31) saklanıyor; amber hücreler kendi indekslerinde dolu, kalan 36 hücre (slate) boş duruyor. Gerçek ölçekte fark uçurumdur: MIT ID anahtarları için \(u \approx 10^9\) iken tipik \(n \approx 300\) kayıt tutulur → hücrelerin ~%99.99997’si boş kalır, yani \(10^9\) word’lük dizi neredeyse tamamen israftır. Bu israf, bir sonraki adımı zorlar: büyük evreni küçük bir aralığa (\(m \approx n\)) sıkıştıran bir hash fonksiyonu (Şekil 6.4).
İpucuBuilder Notu — O(1) Rastgele Erişim ve Hash’in ML’de Kullanımı

Doğrudan erişimin “anahtar = indeks → \(O(1)\)” sezgisi, ML’in en sıcak yollarında (hot path) tekrar tekrar karşına çıkar — ama her zaman tam evreni ayırmadan, hash’le sıkıştırarak:

  • İleriye → feature hashing (hashing trick): Yüksek-kardinaliteli kategorik öznitelikleri (\(u\) devasa: tüm olası kelimeler/URL’ler) sabit boyutlu (\(m\)) bir vektöre \(h(k)\) ile indirger. Direct access array’in israfını çözmenin ML’deki birebir karşılığı — çakışmaları bilerek tolere eder.
  • İleriye → embedding indeksleme: Token → indeks → embedding satırı erişimi tam bir direct access array’dir; sözlük boyutu \(m\) (\(u\) değil) kadar satır ayrılır, \(h\) token’ı bu aralığa eşler. Karpathy’nin makemore serisindeki embedding tablosu budur.
  • İleriye → bloom filter sezgisi: “Bu anahtarı daha önce gördük mü?” sorusunu birkaç hash fonksiyonu + bit dizisiyle çok az bellekte cevaplar — yine \(u\) değil \(m\) kadar yer, çakışma karşılığında.

6.6 Hash Fonksiyonu

Çözüm: büyük anahtar evrenini (\(0 \ldots u-1\)) küçük bir aralığa (\(0 \ldots m-1\), \(m \approx n\)) bir fonksiyonla sıkıştır. Bu fonksiyona \(h\) (hash fonksiyonu) denir. Artık öğeyi \(k\). indekste değil, \(h(k)\). indekste sakla.

Boyutu \(m \approx n\) olan bu yapıya hash tablosu denir. Alan artık \(O(n)\)’dir — sorun çözüldü gibi. Ama sıkıştırma yeni bir sorun doğurur: çakışma (Şekil 6.4).

6.7 Çakışma (Collision) ve Çözümleri

Büyük evreni küçük aralığa eşleyince, güvercin yuvası ilkesi (pigeonhole) gereği bazı farklı anahtarlar aynı indekse düşer. Kötü bir \(h\) seçilirse, \(u > n^2\) iken \(n\) anahtarın hepsi tek bir indekse gidebilir → hiçbir kazanç yok.

“I really want a hash function that will evenly distribute keys over this space.” — Ku, 27:27

Çakışmayla başa çıkmanın iki yolu:

  • Açık adresleme (open addressing): çakışan öğeyi dizide başka boş bir yere koy. Python bunu kullanır; analizi zordur (bu derste işlenmez).
  • Zincirleme (chaining): o indekste tek öğe yerine, bir zincir (chain — linked list veya dinamik dizi) tut. Çakışanları zincire ekle; ararken zinciri lineer tara (Şekil 6.4).

“this is an idea called chaining.” — Ku, 32:11

Hedef: zincirleri kısa tutmak. Zincirde sabit sayıda öğe varsa, hepsini taramak yine \(O(1)\)’dir.

Şekil 6.4: Hash fonksiyonu + zincirleme (chaining). Sol: anahtarlar büyük bir evrenden (\(0 \ldots u-1\)) gelir. Ortadaki hash fonksiyonu \(h\) bu evreni \(m \approx n\) küçük kovaya sıkıştırır (\(h(k) = k \bmod m\), burada \(m = 5\)). Sıkıştırma kaçınılmaz olarak çakışma doğurur (güvercin yuvası ilkesi: \(u > m\) olduğundan en az iki anahtar aynı kovaya düşer). Çözüm zincirleme: aynı kovaya düşen anahtarlar o kovanın zincirine (bağlı listesine, amber kutular) eklenir. Burada deterministik olarak kova \(0\) üç anahtarlık bir zincir tutar (\(5, 15, 30\) — hepsi \(\bmod 5 = 0\)) ve kova \(3\) iki anahtarlık bir zincir tutar (\(8, 23\)). Arama, anahtarın düştüğü kovanın zincirini lineer tarar; zincirler kısa tutulursa (\(m = \Theta(n)\)) işlemler beklenen \(O(1)\)’dir.

6.8 Hash Fonksiyonu Seçimi: Bölme Yöntemi

En basit hash: bölme yöntemi (division method), \(h(k) = k \bmod m\). Büyük anahtarı \(m\)’ye böler, kalanı indeks yapar.

“Modulus– great. This is called the division method.” — Ku, 34:38

Anahtarlar düzgün (uniform) dağılmışsa iyidir — ama bu, girdiye bir dağılım şartı koyar. Python bunu biraz bit karıştırmayla kullanır; deterministik olduğu için, performansı bozan kötü girdi dizileri vardır. 6.006 ise girdiden bağımsız bir garanti ister: hangi anahtarlar saklanırsa saklansın iyi çalışmalı. Sabit (deterministik) bir hash bunu sağlayamaz.

İpucuBuilder Notu — Hash-Flooding DoS ve Güvenlik

“Deterministik hash’in kötü girdisi vardır” cümlesi soyut bir uyarı değil; gerçek bir güvenlik açığının (\(CWE\)-407 algorithmic complexity attack) tam tanımıdır:

  • Saldırı: Hash fonksiyonu sabit ve bilinen olduğunda (eski PHP, Python <3.3, birçok web framework), saldırgan hepsi aynı kovaya düşen anahtarlar üretir. Zincir \(O(n)\) uzar; \(O(1)\) sandığın her insert/find \(O(n)\) olur ve \(n\) istek \(O(n^2)\) işe patlar → sunucu tek bir HTTP form gönderimiyle çöker (hash-flooding DoS).
  • Çözüm = bu dersin konusu: hash fonksiyonunu rastgele tohumla (random seed) seç. Saldırgan tohumu bilmediği için kötü girdiyi önceden tasarlayamaz — bu, Bölüm 6.9’deki evrensel hashing’in pratik gerekçesidir.
  • Standart oldu: Python 3.3+ PYTHONHASHSEED’i varsayılan olarak rastgeleler; tam da bu saldırıyı engellemek için.

6.9 Evrensel Hashing (Universal Hashing)

Çözüm: hash fonksiyonunu önceden değil, sonradan rastgele seç. Kullanıcı girdisini belirler; sonra biz rastgele bir hash seçeriz — hangisini seçtiğimizi bilmediği için kötü girdi vermesi zorlaşır.

“choose one randomly later… I’m going to choose a random hash function.” — Ku, 38:31

Tüm hash fonksiyonları arasından seçmek imkânsızdır (çok fazla). Bunun yerine bir aileden seçeriz. Evrensel hash ailesi örneği:

\[h(k) = ((a \cdot k + b) \bmod p) \bmod m\]

Burada \(p\), \(u\)’dan büyük sabit bir asal; \(a \ne 0\) ve \(b\) ise hash tablosu kurulurken rastgele seçilir. Evrensellik özelliği: rastgele seçilen bir \(h\) için, herhangi iki farklı anahtarın çakışma olasılığı \(\le 1/m\)’dir (Şekil 6.5).

Şekil 6.5: Evrensel hashing (Ku \(\S 8\)): tek bir hash fonksiyonu sabitlenmez — bunun yerine \(h(k) = ((a \cdot k + b) \bmod p) \bmod m\) ailesinden, girdi belirlendikten sonra \(a, b\) rastgele seçilir (\(1 \le a \le p-1\), \(0 \le b \le p-1\); \(p\) anahtarlardan büyük bir asal). Solda aile üyeleri (\(h_1, \dots, h_5\)), her biri farklı \((a, b)\) ile; amber olan rastgele seçilen \(h\)’tir (burada \(a=5, b=18\)). Sağ panel evrensellik garantisini ampirik doğrular: iki sabit, farklı anahtar \(k_1=7, k_2=18\) için \(2000\) rastgele \(h\) denenir; çakışma oranı \(0.166\), teorik sınır \(1/m = 0.200\)’ün altında kalır — yani rastgele bir \(h\) için herhangi iki farklı anahtarın çakışma olasılığı \(\le 1/m\)’dir. Bu garanti girdiden bağımsızdır (saldırgan anahtarları \(h\)’ten önce seçemez), \(p=23\), \(m=5\).
İpucuBuilder Notu — Universal Hashing ve Stat 110 (Indicator RV)

Bu, kursta gördüğün randomized algoritmanın ilk ciddi örneğidir — ve doğrudan Phase 1 Stat 110’a bağlanır:

  • Gösterge rastgele değişkeni (indicator RV):\(i\) ve \(j\) çakıştı mı?” sorusunu \(0/1\) bir değişkenle (\(X_{ij}\)) modellersin. Bir indicator RV’nin beklenen değeri, o olayın olasılığına eşittir: \(E[X_{ij}] = P(X_{ij}=1) \le 1/m\). Doğum günü problemiyle (Ders 1) aynı olasılık aletidir.
  • Beklentinin doğrusallığı (linearity of expectation): Zincir uzunluğu birçok indicator RV’nin toplamıdır; toplamın beklentisi, değişkenler bağımlı olsa bile beklentilerin toplamıdır. Bölüm 6.10’deki \(E[X_i] = 1 + (n-1)/m\) türetimi bunun üzerine kuruludur.
  • Köprü: “garanti her girdide değil, beklenen anlamda” disiplini, ML’deki Monte Carlo tahmini, dropout, stokastik gradyan inişi gibi rastgeleliği bilerek kullanan yöntemlerin temel düşünme tarzıdır.

6.10 Beklenen Zincir Uzunluğu

Evrensellik, zincirlerin beklenen uzunluğunu sabit yapar. İspat olasılıkla yapılır (Stat 110 köprüsü, Şekil 6.6).

Çalışılan Örnek — beklenen zincir uzunluğu. Bir gösterge rastgele değişkeni tanımla: \(X_{ij} = 1\) eğer anahtar \(i\) ve \(j\) çakışırsa (\(h(k_i) = h(k_j)\)), aksi halde \(0\). \(i\)’nin düştüğü indeksteki zincir uzunluğu \(X_i = \sum_j X_{ij}\) (\(j = 0 \ldots n-1\)). Beklentinin doğrusallığı ile:

\[E[X_i] = \sum_j E[X_{ij}]\]

“Linearity of expectation” — Ku, 49:05

Toplamı iki parçaya ayır: \(j = i\) terimi (anahtar kendisiyle kesin çakışır, \(E = 1\)) ve \(j \ne i\) terimleri (evrensellik gereği her biri \(E[X_{ij}] \le 1/m\)). \(n-1\) tane \(j \ne i\) terimi var:

\[E[X_i] = 1 + (n - 1)/m\]

“this should equal 1 plus n minus 1 over m… we’re expected to have our chain lengths be constant” — Ku, 51:26

\(m\)’yi \(n\)’de lineer seçersek (\(m = \Theta(n)\)), \((n-1)/m\) sabit olur → beklenen zincir uzunluğu \(O(1)\). Dolayısıyla find/insert/delete beklenen \(O(1)\).

Şekil 6.6: Beklenen zincir uzunluğu (İMZA, Stat 110 köprüsü). Bir anahtarın düştüğü kovanın zincir uzunluğu \(X_i = 1 + \sum_{j \ne i} X_{ij}\), burada \(X_{ij}\) anahtar \(j\)’nin \(i\) ile çakışıp çakışmadığını gösteren gösterge rastgele değişkeni (indicator RV). Evrensel hashing’de \(P(X_{ij}=1) = E[X_{ij}] \le 1/m\); beklentinin doğrusallığı ile \(E[X_i] = 1 + \sum_{j \ne i} E[X_{ij}] = 1 + (n-1)/m\) — toplamın beklentisi, çakışmalar bağımsız olmasa bile beklentilerin toplamıdır. Amber çizgi: \(m = 20\) sabit\((n-1)/m\) lineer büyür, zincirler uzar. Slate çizgi: \(m = \Theta(n)\) (burada \(m = n\)) → \((n-1)/m \approx 1\) sabit kalır, \(E[X_i] \approx 2\) yani beklenen \(O(1)\). Noktalar deterministik (sabit seed) ampirik simülasyon: \(n\) farklı anahtar rastgele evrensel hash ile yerleştirilir, her anahtarın zincir uzunluğu ortalanır — ampirik değerler teorik çizgilerin tam üstünde oturur (ör. \(n=200\), \(m=20\): teori \(10.95\), ampirik \(10.935\)). Hash tablosunu \(m = \Theta(n)\) tuttuğun sürece arama/ekleme/silme beklenen sabit zamandır.
İpucuBuilder Notu — Beklenen vs En Kötü Durum

Bu dersin en ince fikri, garantinin tipini değiştirmektir: en kötü durum \(O(n)\) olan bir yapının beklenen \(O(1)\) olduğunu kanıtlamak.

  • Randomized garanti: Evrensel hashing’le hâlâ kötü bir durum vardır (tüm anahtarlar tek kovaya düşebilir), ama olasılığı küçüktür ve girdiye değil, bizim rastgele seçimimize bağlıdır. Saldırgan kötü girdi tasarlayamaz çünkü hangi \(h\)’yi seçtiğimizi bilmez.
  • İleriye → ML’de beklenen-değer analizi: Monte Carlo tahmini, minibatch SGD, dropout — hepsi “her örnekte değil, beklentide doğru/iyi” mantığıyla çalışır. Bir tahminin yansızlığını (unbiasedness) göstermek, tam olarak bu \(E[\cdot]\) türetimini yapmaktır.
  • Disiplin: “En kötü durum mu, beklenen mi, amortize mi?” sorusu, bir veri yapısı seçerken sorman gereken ilk sorudur — üçü farklı garantilerdir, üçü farklı risklerdir.

6.11 Dinamik Hashing

\(n\) büyüdükçe, sabit \(m\) artık \(n\)’de lineer kalmayabilir; o zaman zincirler uzar. Çözüm: tıpkı dinamik dizide olduğu gibi, \(n\) çok büyüdüğünde tüm hash tablosunu yeni (daha büyük) \(m\) ile yeniden kur. Doğru oranda yeniden boyutlandırırsan, bu pahalı işlem nadiren olur — amortize edilmiş sabit zaman.

Bu dersin tüm yolculuğu — \(\Omega(\log n)\) karşılaştırma sınırından beklenen \(O(1)\) hash’e — Şekil 6.7’da tek bir grafikte toplanır.

Şekil 6.7: Arama maliyetinin tüm kurs boyunca evrimi — bu dersin sentezi. ① Sırasız dizi: \(O(n)\) (slate) — find baştan sona lineer tarar; en kötü durumda \(n\) öğenin hepsine bakılır (L1/L2). ② Sıralı dizi: \(O(\log n)\) (koyu amber) — ikili arama her adımda aralığı yarıya böler, yalnız ortaları inceler; ama bu karşılaştırma modelinin alt sınırıdır (\(\Omega(\log n)\), L3) — karşılaştırma yaparak \(O(\log n)\)’den hızlı arama imkânsızdır. ③ Hash tablosu: beklenen \(O(1)\) (amber, vurgulu) — anahtarı doğrudan bir kovaya hash’leyerek karşılaştırma modelinin alt sınırını aşar; \(m = \Theta(n)\) kova ve evrensel hash ile beklenen zincir uzunluğu \(1 + (n-1)/m = O(1)\) olur (L4). \(n\) büyüdükçe \(O(n)\) doğrusal patlar, \(O(\log n)\) yavaşça tırmanır, ama hash’in düz \(E[O(1)]\) çizgisi \(n\)’den bağımsız sabit kalır — kursun ulaştığı kazanç budur.

6.12 Bu Dersin Özeti

  1. Karşılaştırma modelinde arama \(\Omega(\log n)\)’dir: karar ağacının \(n+1\) yaprağı için minimum yükseklik \(\log n\).
  2. Direct access array: anahtar = indeks → \(O(1)\), ama anahtar evreni \(u\) kadar yer ister.
  3. Hash fonksiyonu \(h\): \(0 \ldots u-1 \to 0 \ldots m-1\), \(m \approx n\); alanı \(O(n)\)’e indirir.
  4. Çakışma kaçınılmazdır (pigeonhole); çözüm: open addressing veya chaining.
  5. Bölme yöntemi (\(k \bmod m\)) deterministiktir → kötü girdi vardır.
  6. Evrensel hashing: rastgele \(h\) ailesi, çakışma olasılığı \(\le 1/m\).
  7. Beklenen zincir uzunluğu = \(1 + (n-1)/m\); \(m = \Theta(n)\) ile \(O(1)\) (gösterge RV + beklentinin doğrusallığı).
ÖnemliTek Bir Cümle

Hashing, \(O(\log n)\) karşılaştırma sınırını rastgele erişimle aşar; ve hash fonksiyonunu rastgele seçmek, en kötü durum garantisini beklenen \(O(1)\)’e dönüştürür.

6.13 Kontrol Soruları

Cevap: Doğru bir arama, \(n\) öğeden herhangi birini ya da “yok”u döndürebilmeli → karar ağacında en az \(n+1\) yaprak gerekir. İkili bir ağaçta \(n+1\) yaprağa ulaşmak için minimum yükseklik \(\log(n+1)\)’dir, ve en kötü durumdaki karşılaştırma sayısı = en uzun kök-yaprak yolu = yükseklik. Dolayısıyla en az \(\log n\) karşılaştırma şarttır. (Hashing bu sınırı aşar çünkü karşılaştırma değil, rastgele erişim — sabit-olmayan dallanma — kullanır.)

Cevap: Direct access array, anahtar evreni \(u\) kadar bellek ister. Anahtarlar büyükse (örneğin 9 haneli MIT ID → \(u \approx 10^9\)) ama öğe sayısı küçükse (\(n \approx 300\)), dizinin neredeyse tamamı boş kalır — kabul edilemez bellek israfı. Hash fonksiyonu tam bu sorunu çözer: evreni \(m \approx n\) boyutuna sıkıştırır (çakışma pahasına).

Cevap: \(u\) büyükse, herhangi sabit bir \(h\) için (pigeonhole), tüm \(n\) anahtarı aynı indekse eşleyen bir girdi vardır → o girdide zincir uzunluğu \(n\), arama \(O(n)\). Saldırgan/kötü şanslı girdi bunu tetikleyebilir. Universal hashing, \(h\)’yi girdiden sonra rastgele seçer; kullanıcı hangi \(h\)’nin seçileceğini bilmediğinden kötü girdi tasarlayamaz. Garanti “her girdide” değil, “beklenen” anlamda sağlanır: çakışma olasılığı \(\le 1/m\).

Cevap: \(X_i\), anahtar \(i\)’nin düştüğü zincirin uzunluğu = \(\sum_j X_{ij}\). Beklentinin doğrusallığıyla \(E[X_i] = \sum_j E[X_{ij}]\). “1”: \(j = i\) terimi — anahtar kendisiyle kesin “çakışır” (aynı indeks), yani zincirde her zaman en az kendisi vardır. \((n-1)/m\): kalan \(n-1\) anahtarın her biri, evrensellik gereği \(\le 1/m\) olasılıkla \(i\) ile çakışır; toplamları \((n-1) \cdot (1/m)\). \(m = \Theta(n)\) seçilirse bu terim sabit kalır → \(E[X_i] = O(1)\).

6.14 Egzersizler

Egzersiz 1. \(n+1\) yaprağı olan ikili bir ağacın minimum yüksekliğinin \(\log(n+1)\) olduğunu bir yineleme (recurrence) kurarak göster. (İpucu: yükseklik \(h\) olan bir ağaç en fazla \(2^h\) yaprak tutar.)

Egzersiz 2. Bölme yöntemi \(h(k) = k \bmod m\) için, \(m = 2^b\) (ikinin kuvveti) seçmek neden kötüdür? (İpucu: yalnızca son \(b\) bit kullanılır.)

Egzersiz 3. Universal hash \(h(k) = ((a \cdot k + b) \bmod p) \bmod m\)’de \(a = 0\) neden yasaktır? Anahtar bilgisi nasıl kaybolur?

Egzersiz 4. Python’da deterministik hash’in çakışmasını gözlemle: aynı hash(k) % m değerine düşen anahtarlar üret ve zincir uzunluğunu ölç.

m = 1024
buckets = [0] * m
for k in range(100_000):
    buckets[hash(k) % m] += 1
print("max zincir:", max(buckets), "ort:", sum(buckets) / m)

Egzersiz 5. Beklenen zincir uzunluğu \(1 + (n-1)/m\)’yi kullanarak, \(m = n\) seçildiğinde find işleminin beklenen süresini hesapla. \(m = n/2\) olursa ne değişir?

6.15 Sonraki Ders İçin Hazırlık

Ders 5 (L5): Doğrusal Zamanlı Sıralama

Jason Ku ile, karşılaştırma modelinin \(\Omega(n \log n)\) sıralama sınırını aşan sıralamalara geçiyoruz: counting sort ve radix sort, küçük tam sayı anahtarlarda \(O(n)\). Hashing’deki “anahtarı doğrudan indeks yap” fikri burada da işe yarayacak. (Not: ders akışında araya Problem Oturumu 2 girer.)

UyarıDers 5 Öncesi Yapılacak
  • Bu dersin egzersizlerini, özellikle Egzersiz 4’ü (çakışma ölçümü) çöz.
  • \(E[X_i] = 1 + (n-1)/m\) türetimini gösterge RV + doğrusallıkla yeniden yaz.
  • Ana cümleyi tekrar oku: “Hash fonksiyonunu rastgele seçmek, en kötü durumu beklenen \(O(1)\)’e dönüştürür.”

6.16 Anahtar Kavramlar (Cheat Sheet)

Kavram Tanım Sayfada
Karşılaştırma modeli Anahtarları yalnızca kıyaslayan model; arama \(\Omega(\log n)\) Böl. 3
Karar ağacı (decision tree) Karşılaştırma algoritması; \(n+1\) yaprak → yükseklik \(\log n\) Böl. 3
Direct access array Anahtar = indeks; \(O(1)\) ama \(u\) kadar yer Böl. 4
Anahtar evreni \(u\) En büyük anahtar boyutu; \(u < 2^w\) varsayımı Böl. 5
Hash fonksiyonu \(h\): \(0 \ldots u-1 \to 0 \ldots m-1\), \(m \approx n\) Böl. 6
Çakışma / zincirleme İki anahtar aynı indekse; chain ile çöz Böl. 7
Bölme yöntemi \(h(k) = k \bmod m\); deterministik (kötü girdi var) Böl. 8
Evrensel hashing Rastgele \(h\) ailesi; çakışma olasılığı \(\le 1/m\) Böl. 9
Beklenen zincir \(E = 1 + (n-1)/m\); \(m = \Theta(n)\) ile \(O(1)\) Böl. 10

6.17 Builder ve OMSCS Bağlantıları

İpucu6 köprü

Hashing, ML ve sistem mühendisliğinin altındaki erişim ve olasılık dilini somutlaştırır — köprülerin özeti:

  1. Universal hashing → Stat 110: gösterge RV, beklentinin doğrusallığı, randomized algoritma analizi — hepsi burada somutlaşır.
  2. Hash tablosu → Python dict/set, her dilin map/HashMap’i: \(O(1)\) erişimin iç mekanizması.
  3. Consistent hashing → dağıtık sistemler (cache, sharding, yük dengeleme): anahtarları sunuculara dağıtma.
  4. Hash-flooding (DoS) → güvenlik: deterministik hash saldırıya açık; rastgele seed bu yüzden standart oldu.
  5. Beklenen vs en kötü durum → randomized algoritma disiplini: garantiyi “her girdi” yerine “ortalama girdi” üzerinden vermek (ML’de Monte Carlo / beklenen-değer analizi).
  6. \(u < 2^w\) varsayımı → düşük seviye: anahtarın bir word’e sığması, gerçek donanımda sabit-zaman erişimin koşulu.

ÖnemliTek bir şey alıp gideceksen

Karşılaştırma seni \(O(\log n)\)’de tutar; rastgele erişim + hash bunu \(O(1)\)’e indirir. Ama bedava değildir — hash fonksiyonunu rastgele seçmezsen, en kötü durum seni \(O(n)\) zincire mahkûm eder. Universal hashing, olasılığı senin lehine çevirir.