12  Problem Oturumu 4

Ders 9-10’un uygulaması: AVL ağaçlarının dört uygulaması — çift rotasyonlu silme, öncelik kuyruğu ve kayan pencere, çoklu yapı ve cross-linking, iç içe ağaçlar ve rank sorgusu

NotOturum bilgisi

12.1 Bu Problem Oturumu Ne Hakkında?

Dördüncü problem oturumu (Jason Ku) ikili ağaçların (AVL) uygulamalarına odaklanır; yanında bir de ikili yığın (binary heap) önizlemesi yapar. Yığın henüz işlenmedi — Problem 2’de bir kara kutu olarak kullanılır ve onu açan ders, kitap düzeninde bu oturumu izleyen Ders 12 (L8)’dir. Bu hafta Demaine’in set/sequence veri yapısı tablosu tamamlandı; AVL ağacı hemen her işlemde \(O(\log n)\) ile dengeli bir performans verir.

“Welcome to our fourth problem session. We’re going to be talking about binary trees mostly, today.” — Ku, 0:21

Tüm oturuma damga vuran genel ders şudur (Şekil 33.1): her veri yapısı probleminde önce ne sakladığını ve hangi değişmezleri (invariant) koruduğunu söyle; sonra dinamik işlemler bu değişmezleri korur, sorgular onlara dayanır. Dört problem birer “İfade → Yaklaşım → Çözüm → Karmaşıklık” akışıyla işlenir.

flowchart TD
    R["Problem Oturumu 4<br/>(Ders 9-10 uygulaması)"] --> P1["Problem 1<br/>Sequence AVL silme<br/>(çift rotasyon)"]
    R --> P2["Problem 2<br/>En güçlü görüşler<br/>(öncelik kuyruğu + kayan pencere)"]
    R --> P3["Problem 3<br/>Müzayede<br/>(çoklu yapı + cross-linking)"]
    R --> P4["Problem 4<br/>Receiver roster<br/>(iç içe ağaç + rank)"]
    CORE["Ortak ilke:<br/>önce NE saklıyorsun<br/>sonra hangi DEĞİŞMEZ korunuyor"]
    CORE -.-> P1
    CORE -.-> P2
    CORE -.-> P3
    CORE -.-> P4

    classDef root fill:#fef3c7,stroke:#b45309,stroke-width:3px,color:#1f2937
    classDef prob fill:#f3f4f6,stroke:#374151,stroke-width:2px,color:#1f2937
    classDef core fill:#fcd34d,stroke:#b45309,stroke-width:2px,color:#1f2937
    class R root
    class P1,P2,P3,P4 prob
    class CORE core
Şekil 12.1: Problem Oturumu 4’ün kavram haritası: kök (PS4) dört probleme dallanır ve ortadaki ortak ilke düğümü her dördünü birden yönlendirir. Problem 1 sequence AVL’de çift rotasyonlu silme yapar (height ve size birlikte); Problem 2 mutlak değerce en güçlü k görüşü öncelik kuyruğuyla, sonra log n bellekle kayan pencerede bulur; Problem 3 müzayedede çoklu yapıyı cross-linking ile bağlar ve toplamı zenginleştirme olarak tutar; Problem 4 oyuncu performanslarını iç içe ağaçlarda tutup k. en yükseği rank sorgusuyla çeker. Ortadaki ilke — önce ne sakladığını, sonra hangi değişmezleri koruduğunu söyle — Ku’nun her probleme aynı kapıdan girmesini sağlar.

12.2 Problem 1: Sequence AVL — delete_at ve Çift Rotasyon

İfade. Bir sequence AVL ağacında delete_at(8) (sıradaki 8. öğeyi sil) işlemini yürüt; sonucu yükseklik-dengeli tut.

İpucuYaklaşım — İki augmentation: height denge için, size sıra-indeksi için

Sequence AVL ağacı iki alanla zenginleştirilir: height (rotasyonlarda dengeyi kontrol için) ve size (sıra-indeksiyle arama için). 8. öğeyi subtree_at ile bul (her düğümde sol alt ağaç boyutuna bakarak in), sil, sonra ata yolunu yukarı çıkıp dengeyi rotasyonlarla onar. İki augmentation aynı düğümde yaşar ama farklı işe yarar: size nereye ineceğini, height nasıl dengeleyeceğini söyler.

“Sequence AVL trees… are augmented by two things… I need to store heights… and the sequence requires me to store their subtree numbers.” — Ku, 10:29

Çözüm. Silme sonrası bir düğüm dengesizleşir (skew ±2). Bu, AVL’nin zor durumudur (Ders 10 Durum 3): düğüm sağa ağır ama sağ çocuğu sola ağır. Tek rotasyon işi tersine bozar; çift rotasyon gerekir — önce sağ çocukta sağ-rotasyon, sonra düğümde sol-rotasyon.

“I am badly skewed to the right but my right subtree is skewed to the left — I have to do a right rotation here, and then do a rotation.” — Ku, 15:53

Her rotasyonda yalnızca etkilenen 2-3 düğümün zenginleştirmesi (height, size) çocuklardan yeniden hesaplanır. Verilen ağaç bir Fibonacci ağacıdır — belli düğüm sayısı için en yüksek (en az dengeli) AVL; bu yüzden silme logaritmik sayıda rotasyon gerektirebilir. İlginç fark: insert her zaman tek bir denge işlemiyle (≤ 2 rotasyon içeren bir olay) düzelir; delete ata yolu boyunca log n ayrı olay isteyebilir.

“in a delete operation, it’s possible that you have to do a logarithmic number of these rotations… An insertion operation will always re-balance after one re-balance operation.” — Ku, 24:34

Şekil 12.2 bu farkı motordan gerçek verilerle gösterir: 400 rastgele ekleme boyunca her ekleme en çok 1 olayla kapanır (sol panel); buna karşılık bir Fibonacci ağacında (h = 7, n = 54) en soldaki yaprağı silmek ata yolu boyunca tam 3 ardışık olay doğurur (sağ panel) — silmenin kademeli (cascade) onarım yapabildiğinin somut tanığı. İkisi de toplam \(O(\log n)\) zamandır, çünkü her olay \(O(1)\)’dir; fark, olay sayısındadır.

Karmaşıklık. delete_at \(O(\log n)\) (subtree_at ile bul + ata yolunda \(O(\log n)\) rotasyon, her rotasyon \(O(1)\)).

Şekil 12.2: AVL onarımı — insert TEK olay vs delete LOG N olay (Ku 24:34) — Problem 1 İMZA. SOL panel INSERT: küçük bir AVL’ye ekleme kökte skew=+2 (slate çerçeve, İHLAL) doğurur; TEK right_rotate olayıyla (amber yıldız) dengelenir — ekleme her zaman O(1) olayla kapanır. SAĞ panel DELETE: Fibonacci ağacı (h=7, n=54; mümkün en az dengeli AVL, her düğümde sağ alt-ağaç soldan tam 1 yüksek) sembolik kademe şeması; en soldaki yaprak silinince (kesik amber daire) ata yolu boyunca motordan GERÇEK 3 ardışık seviyede skew=±2 doğar → 3 amber yıldız (3 olay). Alt şerit: insert O(1) olay, delete O(log n) olay — ama her olay O(1) olduğundan ikisi de toplam O(log n) zaman; fark olay SAYISINDADIR. Sol paneldeki 400 rastgele eklemenin hiçbiri 1 olayı aşmaz (motor tanığı).

12.3 Problem 2: En Güçlü Görüşler

İfade. “Fick Nury” (Nick Fury), n kahramanın görüşlerini (güçlü-pozitif veya güçlü-negatif) içeren salt-okunur bir diziden, mutlak değerce en güçlü log n kahramanı bulmak ister. (a) doğrusal zamanda. (b) en fazla log n ek bellek kısıtıyla.

İpucuYaklaşım — (a) build O(n) yığın; (b) sabit-boyutlu küme + kayan pencere
  1. Öncelik kuyruğu (priority queue) — ikili yığının çözdüğü arayüz; build ve delete_max sunar. Yığın bu oturumda kara kutudur (Ders 12’de açılır). (b) bellek kısıtı çıktı boyutuna (log n) bağlı bir yapıyı zorlar: en fazla log n öğelik bir set AVL ağacı + kayan pencere ile akışı bir kez gez, her an top-k’yı tut.

“It’s implementing what we call a priority queue interface… delete_max.” — Ku, 29:14

Çözüm.

(a) İkili yığınla: Tüm görüşlerin mutlak değerini bir diziye kopyala, ikili yığın olarak build et — \(O(n)\). Sonra delete_max’i log n kez çağır — en güçlü log n görüşü çeker.

“a binary heap does build in linear time and delete_max in log n time.” — Ku, 35:13

İkili yığının zarafeti: build \(O(n)\) (set AVL’nin \(O(n \log n)\)’inden iyi), delete_max \(O(\log n)\). Toplam \(O(n + \log n \cdot \log n)\) = \(O(n + \log^2 n)\) = \(O(n)\) (\(\log^2 n \ll n\)).

def strongest_opinions_heap(opinions, k):       # (a) yığınla — O(n)
    h = MaxHeap([abs(x) for x in opinions])     # build: O(n) (heapify)
    return [h.delete_max() for _ in range(k)]   # k = log n çekiş — O(log²n)

(b) log n bellek + kayan pencere: En fazla log n öğelik bir set AVL ağacı tut (yüksekliği log log n). İlk log n öğeyle doldur; sonra kalan her öğe için: ekle, sonra en küçüğü sil. Değişmez: ağaç her an o ana kadar görülen k = log n en güçlü görüşü tutar.

“the invariant I’m maintaining here is that my thing always has the k largest opinions of the ones that I’ve processed so far.” — Ku, 48:14

class TopKTree:                                  # (b) kayan pencere — O(log n) bellek
    def process(self, value):                    # akıştan tek öğe
        self.root = avl_insert(self.root, value)
        self.n += 1
        if self.n > self.k:                      # taştıysa
            smallest = subtree_first(self.root).item
            self.root = avl_delete(self.root, smallest)   # en küçüğü düşür
            self.n -= 1

Şekil 12.3 bu izi akış \([5, 1, 8, 3, 9, 2, 7]\) ve k = 3 üzerinde motordan gerçek çalıştırarak gösterir: ağaç içeriği adım adım \([5] \to [1,5] \to [1,5,8] \to [3,5,8] \to [5,8,9] \to [5,8,9] \to [7,8,9]\) olur. Her adımda değişmez tutar — ağaç, o ana kadar görülenlerin tam olarak top-3’üdür (akışın 6. öğesi olan 2, mevcut minimum 5’ten küçük olduğundan girip hemen çıkar; içerik değişmez).

Karmaşıklık. (a) \(O(n)\) (ikili yığın). (b) \(O(n \log \log n)\) zaman (her öğe için log log n yükseklikli ağaca insert/delete), yalnızca \(O(\log n)\) bellek.

Şekil 12.3: k-en-iyi kayan set-AVL + DEĞİŞMEZ (Ku 48:14) — Problem 2b İMZA. Akış \([5,1,8,3,9,2,7]\), k=3 motordan GERÇEK çalıştırılır. Üstte akış şeridi; altında 7 adım satırı: her satır o adımdan sonra ağaç içeriğini (sıralı mini-set), düşen öğeyi (gri daire + üzeri çizgi) ve kalıcı giren öğeyi (amber daire) gösterir. Sağdaki amber rozet DEĞİŞMEZİ doğrular: ağaç her an o ana kadar görülenlerin top-3’üdür (hepsi ✓). Akışın 6. öğesi olan 2, mevcut minimum 5’ten küçük olduğundan girip hemen çıkar → içerik [5,8,9] değişmez. Alt amber kutu: bellek O(k)=O(log n), öğe başına maliyet O(log k)=O(log log n).

12.4 Problem 3: Müzayede — Çoklu Yapı ve Cross-Linking

İfade. Bir müzayedede teklif verenler (benzersiz bidder ID + tamsayı teklif). new_bid ve update_bid \(O(\log n)\); ihale kapanınca en yüksek k teklifin toplamı (get_revenue) \(O(1)\). (k önceden sabittir.)

İpucuYaklaşım — İki anahtar → çoklu yapı; önce ne saklanır ve hangi değişmez korunur

İki “anahtar” var: bidder ID (arama için) ve teklif (sıralama için) → tek bir ağaç yetmez, birden çok veri yapısı gerekir. Her zamanki gibi önce ne saklandığını ve hangi değişmezin korunduğunu netleştir: en yüksek k teklif bir kümede, geri kalanlar başka bir kümede, toplam ayrı bir sayıda. Yapıları bağlayan şey cross-linking’tir.

Çözüm. Dört bileşen:

  1. Sözlük (set AVL), bidder ID anahtarlı: bir teklifçiyi bul; her düğüm, teklifçinin diğer yapılardaki yerine bir işaretçi tutar (cross-linking).
  2. k-en-yüksek set AVL (teklif anahtarlı): şu anki en yüksek k teklif.
  3. (n−k)-kalan set AVL (teklif anahtarlı): geri kalanlar.
  4. total (t): k-en-yüksek kümesinin toplamı — bir sayı zenginleştirmesi.

new_bid: yeni teklif, k-en-yüksek’in minimumundan büyükse, o minimumu (n−k)’ye taşı ve yeniyi k-en-yüksek’e koy; değilse doğrudan (n−k)’ye. Her hareket total’ı \(O(1)\) günceller. get_revenue sadece total’ı döndürür — \(O(1)\).

“This is called cross-linking.” — Ku, 1:07:08

def new_bid(self, bidder, amount):               # O(log n)
    if bidder in self.bids:                      # cross-link: sözlük O(1) bulur
        old = (self.bids[bidder], bidder)        # eski anahtarı sil (O(log n))
        ...
    self.bids[bidder] = amount
    self._topk_insert((amount, bidder))          # yeni anahtar (O(log n))
    self._rebalance_sets()                        # topk ↔ rest sınırını koru

def get_revenue(self):
    return self.total                            # zenginleştirme → O(1)

Şekil 12.4 dört bileşeni ve cross-link’leri motordan gerçek bir senaryoyla gösterir: k = 2 ile A:100, B:300, C:200 teklifleri girilince topk = {300, 200}, rest = {100}, revenue = 500. Sonra A’nın teklifi 400’e güncellenince A’nın eski kaydı (100) rest’ten silinir, (400, A) topk’ye girer, taşan minimum (200, C) rest’e iner ve revenue 700 olur — her adım total’ı \(O(1)\) günceller.

Karmaşıklık. new_bid/update_bid \(O(\log n)\) (sabit sayıda AVL işlemi); get_revenue \(O(1)\) (değişmez total).

İpucuBuilder Notu — cross-linking = veritabanı ikincil indeksi / foreign key

Cross-linking, veritabanı dünyasındaki ikincil indeks (secondary index) ve foreign key ile birebir örtüşür. Bir tablo birincil anahtarıyla (bidder ID) saklanırken, sık sorgulanan başka bir alan (teklif) üzerine ayrı bir indeks kurulur; ikincil indeks aynı kaydı farklı bir anahtarla bulmanı sağlar — tıpkı buradaki teklif-anahtarlı topk/rest ağaçlarının sözlükteki aynı teklifçiye bir işaretçiyle bağlanması gibi. Veri tek yerde tutulur, ona iki ayrı yoldan erişilir; güncellemede her iki yapı da senkron tutulmalıdır (foreign key bütünlüğü). Bu, “aynı veriyi iki anahtarla aranabilir kılmanın” maliyeti ve gücüdür.

12.5 Problem 4: Receiver Roster — İç İçe Ağaçlar ve Rank Sorgusu

İfade. Bir futbol koçu, oyuncuların k. en yüksek performansını (= ortalama puan = puan/oyun, bir rasyonel) \(O(\log n)\)’de sorgulamak ister. Veriler dinamik güncellenir (oyun ekle/sil).

İpucuYaklaşım — Rasyonel için çapraz çarpım; rank için size augmentation

Performans rasyoneldir → bölme (kayan nokta hatası) yerine çapraz çarpım ile karşılaştır: \(w_1/f_1\) ile \(w_2/f_2\)’yi kıyaslarken bölmek yerine \(w_1 \cdot f_2\) ile \(w_2 \cdot f_1\)’i karşılaştır (paydalar pozitif olduğundan işaret korunur; motorda Fraction bunun exact eşdeğeridir). k. en büyüğü (rank sorgusu) bulmak için ise size zenginleştirmesi gerekir — sequence ağacının subtree_at’iyle aynı mekanizma.

Çözüm. İç içe bir yapı kurulur:

  1. Set AVL (receiver ID anahtarlı): oyuncuları bul.
  2. Her oyuncuyla iç içe bir set AVL (game ID anahtarlı): o oyuncunun oyunları (bir oyunu \(O(\log n)\)’de silmek için liste değil ağaç).
  3. Her oyuncuda iki sayı zenginleştirmesi: toplam puan ve oyun sayısı → performans (puan/oyun) çapraz-çarpımla karşılaştırılır.
  4. Performansa göre set AVL (çapraz-çarpım komparatörü) + size zenginleştirmesi: bu, sequence ağacının subtree_at’iyle aynı rank-find işlemini verir — k. en büyük performansı \(O(\log n)\)’de bulur. (Ders 10’daki subtree_at ile aynı mekanizma: sondan k. öğe = in-order’da indeks n−k.)
  5. Cross-linking işaretçileri (bir oyuncunun performans ağacındaki yeri).

“you can think of the size augmentation finding-rank as a one-sided range query.” — Ku, 1:29:19

def kth_best(self, k):                           # k. en yüksek performans — O(log n)
    # size-rank: in-order'da sondan k. = indeks n−k (Ders 10'daki subtree_at)
    return subtree_at(self.perf, self.perf_n - k).item

def _perf_key(self, pid):                         # rasyonel anahtar: çapraz çarpım = Fraction
    p = self.players[pid]
    return (Fraction(p["points"], len(p["games"])), pid)   # exact, bölme YOK

Karmaşıklık. Tüm güncellemeler ve k. en yüksek sorgusu \(O(\log n)\) (iç içe aramalar log n × log n değil, çünkü oyun sayısı toplamı n).

12.6 Ne Öğrendik?

ÖnemliAltı Taşınabilir Araç

Bu oturum, Ders 9-10’un AVL teorisini dört somut problemde uyguladı ve altı taşınabilir araç kazandırdı:

  1. Sequence AVL = height + size: indeksle arama (subtree_at) ve denge birlikte; çift rotasyon (Durum 3) ezberlenir.
  2. Priority queue / binary heap: build \(O(n)\) + delete_max \(O(\log n)\) — set AVL’nin \(O(n \log n)\) build’inden üstün (önizleme).
  3. Kayan pencere + değişmez: sabit boyutlu (log n) yapıyla “o ana kadarki en iyi k” tutmak; az bellek.
  4. Çoklu yapı + cross-linking: aynı veriyi farklı anahtarlarla iki/üç ağaçta tutup işaretçilerle bağlamak.
  5. Augmentation ile \(O(1)\) sorgu: bir toplamı (total) zenginleştirmeyle tutup sorguyu sabit zamana indirmek.
  6. size augmentation = rank sorgusu: k. en büyük / “kaç tanesi büyük” (one-sided range query) — sequence subtree_at’in seti.
İpucuBuilder Notu — kayan pencere = streaming leaderboard

Problem 2’nin (b) şıkkı, modern sistemlerin streaming leaderboard (akış sıralaması) deseninin tam kalbidir: sonsuz bir olay akışında (oyun skorları, satışlar, beğeniler) “şu ana kadarki en iyi k” sabit boyutlu bir yapıda tutulur ve her yeni olayda sıfırdan değil artımlı güncellenir — yeni öğe girer, eşik altına düşen en küçük çıkar. Tüm geçmişi bellekte tutmak yerine yalnızca \(O(\log n)\) öğe saklamak, milyarlarca olaylık akışları sabit bellekle işlenebilir kılar; değişmez (“yapı her an o ana kadarki top-k”) da sonucun doğruluğunu garanti eder.

12.7 Sonraki

UyarıDers 12 (L8) — İkili Yığınlar (Binary Heaps)

Sırada Ders 12 (L8): İkili Yığınlar (Binary Heaps) var — Erik Demaine ile, bu oturumda Problem 2’de kara kutu olarak kullandığımız öncelik kuyruğunu açıyoruz: ikili yığın, diziye gömülü bir tam-ağaçla build \(O(n)\) ve delete_max \(O(\log n)\) verir. (Not: Notion kaynağı bu dersi “Ders 8 (L8)” diye anar, ama kitap düzeninde L8 = Ders 12’dir.) Bu oturumdaki augmentation ve invariant sezgileri, yığının “tahtadan-aşağı” (heapify) mantığını anlamana yarayacak.