İçeriğe geç
academia.sh

Ders 08 / 26

Çakışma Çözümleme

Zincirleme ve açık adresleme, yoklama stratejileri, kümelenme, silme sorunu ve iki ailenin karşılaştırması.

İçindekiler

Önceki ders çakışmanın kaçınılmaz olduğunu gösterdi: anahtar uzayı tablodan büyükse, en az iki anahtar aynı kovaya düşer. Bu ders, düştüklerinde ne yapılacağını ele alır.

İki çözüm ailesi vardır ve ikisi de yaygın kullanılır. Ayrım tek bir soruda toplanır: çakışan eleman kovanın içinde mi yoksa tablonun başka bir konumunda mı saklanır?

Zincirleme

Zincirlemede (chaining) her kova, o kovaya düşen elemanların listesini tutar. Tablo bir dizi listedir; ekleme, ilgili listeye eklemektir.

class ZincirliTablo:
    def __init__(self, kova_sayisi: int = 8) -> None:
        self._kovalar: list[list] = [[] for _ in range(kova_sayisi)]
        self.karsilastirma = 0                 # ölçüm için

    def _konum(self, anahtar) -> int:
        return hash(anahtar) % len(self._kovalar)

    def koy(self, anahtar, deger) -> None:
        kova = self._kovalar[self._konum(anahtar)]
        for i, (mevcut, _) in enumerate(kova):
            if mevcut == anahtar:
                kova[i] = (anahtar, deger)     # var olan anahtar güncellenir
                return
        kova.append((anahtar, deger))

    def al(self, anahtar):
        kova = self._kovalar[self._konum(anahtar)]
        for mevcut, deger in kova:
            self.karsilastirma += 1
            if mevcut == anahtar:
                return deger
        raise KeyError(anahtar)

    def sil(self, anahtar) -> None:
        kova = self._kovalar[self._konum(anahtar)]
        for i, (mevcut, _) in enumerate(kova):
            if mevcut == anahtar:
                kova.pop(i)                     # listeden çıkarmak yeterli
                return
        raise KeyError(anahtar)


tablo = ZincirliTablo()
for ad, olcum in [("kuzey", 12), ("guney", 18), ("dogu", 7), ("bati", 25)]:
    tablo.koy(ad, olcum)

print(tablo.al("dogu"))        # 7
tablo.sil("dogu")
print(tablo.al("bati"))        # 25

Silmenin sade olduğuna dikkat edin: eleman listeden çıkarılır, başka hiçbir şey etkilenmez.

Zincirlemenin maliyeti, kova başına düşen ortalama eleman sayısıdır — yani yük çarpanı. Arama, kovayı bulmak (sabit) artı kovadaki listeyi taramaktır (O(α)O(\alpha)). Yük çarpanı sabit tutulduğu sürece ortalama maliyet sabittir.

Yük çarpanı zincirlemede 11’i aşabilir: kovalar liste tuttuğu için tablo “dolmaz”. Bu esneklik, yeniden boyutlandırmayı geciktirmeye izin verir.

Açık Adresleme

Açık adreslemede (open addressing) tüm elemanlar tablonun kendi gözlerinde durur. Çakışma olduğunda, belirli bir kurala göre başka bir göz denenir; bu denemelere yoklama (probing) denir.

En yalın kural doğrusal yoklamadır: bir sonraki göze bak, dolu ise bir sonrakine, tablonun sonuna gelince başa dön.

BOS = object()        # hiç kullanılmamış göz
SILINDI = object()    # silinmiş göz (mezar taşı)

class AcikTablo:
    def __init__(self, kapasite: int = 8) -> None:
        self._gozler: list = [BOS] * kapasite
        self.yoklama = 0

    def _konum(self, anahtar) -> int:
        return hash(anahtar) % len(self._gozler)

    def koy(self, anahtar, deger) -> None:
        i = self._konum(anahtar)
        while self._gozler[i] is not BOS and self._gozler[i] is not SILINDI:
            if self._gozler[i][0] == anahtar:
                break                                   # güncelleme
            i = (i + 1) % len(self._gozler)             # doğrusal yoklama
            self.yoklama += 1
        self._gozler[i] = (anahtar, deger)

    def al(self, anahtar):
        i = self._konum(anahtar)
        while self._gozler[i] is not BOS:                # BOS görülünce arama biter
            self.yoklama += 1
            if self._gozler[i] is not SILINDI and self._gozler[i][0] == anahtar:
                return self._gozler[i][1]
            i = (i + 1) % len(self._gozler)
        raise KeyError(anahtar)

    def sil(self, anahtar) -> None:
        i = self._konum(anahtar)
        while self._gozler[i] is not BOS:
            if self._gozler[i] is not SILINDI and self._gozler[i][0] == anahtar:
                self._gozler[i] = SILINDI                # mezar taşı bırakılır
                return
            i = (i + 1) % len(self._gozler)
        raise KeyError(anahtar)


tablo = AcikTablo()
for ad, olcum in [("kuzey", 12), ("guney", 18), ("dogu", 7)]:
    tablo.koy(ad, olcum)
print(tablo.al("guney"))       # 18
tablo.sil("guney")
print(tablo.al("dogu"))        # 7

Silmenin Sorunu

Açık adreslemede silme, göze yalnızca “boş” yazarak yapılamaz. Arama, ilk boş gözü görünce durduğu için, silinen göz aramanın yolunu keser: aynı zincirin ilerisindeki elemanlar bulunamaz hâle gelir.

Çözüm, silinen gözü ayrı bir işaretle — mezar taşıyla — doldurmaktır. Arama mezar taşını görünce devam eder, ekleme ise oraya yazabilir.

Bedeli birikimdir: çok sayıda silme sonrası tablo mezar taşlarıyla dolar ve aramalar uzar. Gerçekleştirimler, mezar taşı oranı bir eşiği aştığında tabloyu yeniden düzenler.

Kümelenme

Doğrusal yoklamanın bilinen zayıflığı birincil kümelenmedir: dolu gözlerin ardışık blokları oluşur ve her yeni çakışma bloğu uzatır. Blok uzadıkça, o bölgeye düşen her anahtarın yoklama sayısı artar; büyüme kendi kendini besler.

İki alternatif kural bunu azaltır:

  • Karesel yoklama: Adım aralığı sabit değil, artan bir kare dizisidir. Ardışık bloklar oluşmaz; ancak aynı konumdan başlayan anahtarlar aynı sırayı izler (ikincil kümelenme).
  • Çift karma: Adım aralığı, ikinci bir karma fonksiyonundan hesaplanır. Her anahtar farklı bir sıra izler; kümelenme en aza iner, karşılığında ikinci karma hesabı gerekir.

Doğrusal yoklamanın bir üstünlüğü vardır ve önemsiz değildir: ardışık gözlere bakması, önbellek satırı içinde kalmak anlamına gelir. Bilgisayarlar Nasıl Çalışır kursundaki yerellik gözlemi burada doğrusal yoklamayı, kuramsal olarak daha iyi olan alternatiflere karşı rekabetçi kılar.

İki Ailenin Karşılaştırması

Ölçüt Zincirleme Açık adresleme
Yük çarpanı sınırı 11’i aşabilir 11’i aşamaz; eşik daha düşük tutulur
Bellek Düğüm/liste ek yükü Ek yapı yok, boş gözler var
Önbellek davranışı Zayıf (dağınık liste) İyi (bitişik gözler)
Silme Doğrudan Mezar taşı gerekir
En kötü durum Tek kovada uzun liste Tabloyu dolaşan uzun yoklama
Kovadaki yapı değiştirilebilir Evet (liste yerine ağaç) Hayır

Son satır, pratik bir güvenlik önlemine kapı açar: kova uzunluğu bir eşiği aşarsa liste yerine dengeli bir ağaç kullanılır ve en kötü durum O(n)O(n) yerine O(logn)O(\log n) olur. Bu tasarım, kasıtlı çakışma saldırılarına karşı savunma olarak yaygınlaşmıştır.

Maliyet Tablosu

Yapı Ortalama arama En kötü arama Silme
Zincirleme O(1+α)O(1 + \alpha) O(n)O(n) O(1+α)O(1 + \alpha)
Zincirleme (ağaçlı kova) O(1+α)O(1 + \alpha) O(logn)O(\log n) O(logn)O(\log n)
Açık adresleme O(11α)O\left(\frac{1}{1-\alpha}\right) O(n)O(n) Mezar taşıyla O(1)O(1)*

* Mezar taşı birikimi arttıkça bozulur.

Üçüncü satırdaki ifade, yük çarpanı 11’e yaklaştıkça maliyetin neden hızla arttığını gösterir: α=0,9\alpha = 0{,}9 için ortalama on yoklama gerekir. Açık adreslemede eşik bu nedenle daha düşük tutulur.

Özet

  • Zincirlemede çakışan elemanlar kovadaki bir listede, açık adreslemede tablonun başka gözlerinde saklanır.
  • Zincirlemede ortalama maliyet yük çarpanına bağlıdır ve yük çarpanı 11’i aşabilir.
  • Açık adreslemede silme, mezar taşı gerektirir; aksi hâlde arama yolu kesilir.
  • Doğrusal yoklama birincil kümelenme üretir; karesel yoklama ve çift karma bunu azaltır.
  • Doğrusal yoklamanın önbellek davranışı, kuramsal dezavantajını pratikte telafi eder.
  • Kova içi yapının ağaca dönüştürülmesi, en kötü durumu logaritmik yapar ve çakışma saldırılarına karşı savunma sağlar.

Sonraki Adım

Karma tablosu anahtar–değer eşlemesi kurar. Bazı problemlerde değer gerekmez; yalnızca “bu eleman var mı” sorusu sorulur. Sonraki ders, bu soruya odaklanan küme yapılarını ve küçük evrenlerde bit düzeyi işlemlerle kurulan çok verimli bir gerçekleştirimi ele alacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat