Bölüm 11: Arama Ağaçları

C ile Veri Yapıları ve Algoritmalar
İkili arama ağacıEklemeAramaDerinlikSilmeAVL döndürmeleriKırmızı-siyah, B-ağacı
Kaynak: Horowitz–Sahni 5.7, 10.2 · CLRS 12–13

Giriş

Öğrenme Çıktıları

Tanımı uygulamak

İkili arama ağacının dört özelliğini bir ağaçta denetlemek.

Ekleme ve arama

arama_agacina_ekle ve arama_agacinda_ara yordamlarını döngüsel ve özyinelemeli yazmak.

Derinliği hesaplamak

Derinliği özyinelemeli yazmak; ek parametre ve global değişkenden kaçınmak.

Silmek

Çocuksuz, tek çocuklu ve iki çocuklu düğüm silmenin dört yöntemini karşılaştırmak.

Dengeyi korumak

Dengesizliğin maliyetini hesaplamak; AVL'de LL, RR, LR, RL döndürmelerini uygulamak.

Yapı seçmek

BST, AVL, kırmızı-siyah ve B-ağacını kullanım yerine göre ayırmak.

İkili Arama Ağacı

İkili arama ağacı (binary search tree)

  1. Her öğenin değeri farklıdır; değerler tektir (unique).
  2. Boş olmayan sol alt ağaçtaki değerler, alt ağacın kökündekinden küçüktür.
  3. Boş olmayan sağ alt ağaçtaki değerler kökündekinden büyüktür.
  4. Sol ve sağ alt ağaçlar da ikili arama ağacıdır.
İkili arama ağacı, ikili ağacın bir alt kümesidir. Kök ortada gezinme değerleri sıralı verir.
1815922201921ikili arama ağacı DEĞİL958050297ikili arama ağacı3540383953ikili arama ağacı

İkili Arama Ağacı

Hangisi ikili arama ağacı?

1815922201921ikili arama ağacı DEĞİL958050297ikili arama ağacı3540383953ikili arama ağacı

22, 15'in sağında olduğu için 15'ten büyük (doğru) ama 18'in sol alt ağacında olduğu hâlde 18'den büyük → tanıma uymaz.

1815922201921ikili arama ağacı DEĞİL958050297ikili arama ağacı3540383953ikili arama ağacı

95'in solundaki her değer (80, 50, 2) 95'ten küçük; 80'in solu 50, 50'nin solu 2: sıralama bozulmamış.

1815922201921ikili arama ağacı DEĞİL958050297ikili arama ağacı3540383953ikili arama ağacı

35'in sağındaki her değer büyük; 40'ın solunda 38 ve 39 (40'tan küçük, 35'ten büyük): geçerli.

Ekleme

Ekleme: yeri ağaç belirler

Hocadan “Bir değer söylensin, ben ekleyeyim. “Hocam 15’in sağına ekleyin.” Sağa eklerim, sola eklerim; nereye ekleyeceğim benim bileceğim iş! ☺”

SORU: 3, 5, 7, 9, 11 kümesiyle derinliği 3, 4 ve 5 olan ikili arama ağaçları çiziniz. 2 derinliğinde çizilebilir mi?

Çözüm

Lemma: k derinlikte en çok 2k−1 düğüm; 2 derinlikte en çok 3 → 5 düğüm sığmaz. Yanda 3, 4, 5 derinlikli örnekler.

953711537911357911derinlik 3derinlik 4derinlik 5

Ekleme

Verilerin ağaca eklenme sırası önemli midir?

4747derinlik 12626derinlik 1

47 (sol) ve 26 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

475547 55derinlik 2261726 17derinlik 2

55 (sol) ve 17 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

47554947 55 49derinlik 326174726 17 47derinlik 2

49 (sol) ve 47 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

4726554947 55 49 26derinlik 32617479326 17 47 93derinlik 3

26 (sol) ve 93 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

472634554947 55 49 26 34derinlik 3261747938426 17 47 93 84derinlik 4

34 (sol) ve 84 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

47261734554947 55 49 26 34 17derinlik 326174793845226 17 47 93 84 52derinlik 5

17 (sol) ve 52 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

4726173455495247 55 49 26 34 17 52derinlik 42617473493845226 17 47 93 84 52 34derinlik 5

52 (sol) ve 34 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

472617345549528447 55 49 26 34 17 52 84derinlik 4261747349384525526 17 47 93 84 52 34 55derinlik 6

84 (sol) ve 55 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.

47261734554952849347 55 49 26 34 17 52 84 93derinlik 426174734938452495526 17 47 93 84 52 34 55 49derinlik 6

Sonuç: derinlik 4 ve 6. Kök ortada gezinme ikisinde de aynı: 17 26 34 47 49 52 55 84 93.

Ekleme

Çarpıklık, dengeli ağaçlar ve sık kullanılan düğümler

  • Veriler sıralı gelirse ağaç sağa ya da sola çarpık olur, derinliği n olur: liste gibi.
  • Derinlik ≈ lg n ise dengeli ikili arama ağacı: ekleme, silme, arama O(lg n).
  • Dengeli ağaçlar (self-balancing): veri geldikçe ağacı yeniden dengeleyen algoritmalar — kırmızı-siyah, AVL.
  • Sık kullanılan düğümler: arama sıklığı yüksek düğümleri köke yaklaştıran algoritmalar (“zig-zag”; literatürdeki adıyla splay tree, Sleator–Tarjan).
Hocadan “Ağacın dengeli olması çok önemliyse ağacı dengeli kuran algoritmalar kullanılmalıdır. Çok zor değil, kitaplarda da var, yalnızca biraz daha uzun. Ödev olarak sorabilirim.”

Ekleme

??? — düşündüren iki soru

??? Ağaç görüntüsünden verilerin eklenme sırası elde edilebilir mi? Gelen veriye göre oluşan ağaç unique mi?

Cevap

Aynı sıra her zaman aynı ağacı verir (ekleme deterministiktir). Ama tersi tek değildir: 2,1,3 ve 2,3,1 aynı ağacı kurar. Her atanın torunlarından önce gelmesi yeter; ağacın kök önde sırası daima geçerli bir ekleme sırasıdır.

??? Kök önde, kök ortada, kök sonda gezinme sırasından ağaç görüntüsü elde edilebilir mi?

Cevap

Genel ikili ağaçta tek gezinme yetmez; kök ortada + (kök önde ya da kök sonda) tek ağaç verir. İkili arama ağacında kök ortada zaten sıralı hâldir; bu yüzden yalnızca kök önde (ya da kök sonda) sırası yeterlidir.

Hocadan “Şimdiye kadar arka planda bir ağaç olmadığı için kod çalıştırılarak test edilemiyordu.”

Artık ekleme yordamıyla örnek ağaçlar kurup ikili ağaç yordamlarını (gezinme, kopyalama, eşitlik) gerçekten çalıştırabiliriz.

Ekleme

Ekleme nasıl yapılır?

  • Her iki çocuğu da olan düğüme eklenemez; en az bir çocuğu olmayan düğüme eklenir.
  • Gelen veri kökten itibaren aranır: daha büyük değerli düğümde sola, daha küçük değerlide sağa ilerlenir.
  • Aynı değer bulunursa tanıma aykırı olacağından ekleme yapılmaz.
  • Arama işaretçisinin ilk rastladığı NULL'un yerine yeni öğe eklenir: bunun için bir adım arkadan gelen işaretçi (q) gerekir.
typedef enum { FALSE, TRUE } boolean;
typedef struct dugum *agac_gost;
struct dugum {
    agac_gost solcocuk;
    int       veri;
    agac_gost sagcocuk;
};
/* ogeal(): malloc + NULL denetimi,
   çocukları NULL yapar */

Ekleme · Kodu adım adım çalıştır

arama_agacina_ekle(50, &L) adım adım

472617345549528493px = 50

p kökü gösteriyor (47), q (arkadaki işaretçi) NULL.

472617345549528493p,qx = 50

q = p = 47: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.

472617345549528493p,qx = 50

50 > 47 → yeri sağ alt ağaçta: p = 47->sagcocuk = 55.

472617345549528493p,qx = 50

q = p = 55: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.

472617345549528493p,qx = 50

50 < 55 → yeni öğenin yeri sol alt ağaçta: p = 55->solcocuk = 49.

472617345549528493p,qx = 50

q = p = 49: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.

472617345549528493p,qx = 50

50 > 49 → yeri sağ alt ağaçta: p = 49->sagcocuk = 52.

472617345549528493p,qx = 50

q = p = 52: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.

472617345549528493p,qx = 50

50 < 52 → yeni öğenin yeri sol alt ağaçta: p = 52->solcocuk = NULL.

472617345549528493qNULLx = 50

p NULL oldu: döngü biter. Yeni öğe q = 52'nin boş çocuğunun yerine gelecek.

472617345549528493q50rx = 50

Yeni düğüm ayrılır (çocukları NULL) ve verisi 50 yapılır.

47261734554952508493qx = 50

Ağaç boş değil; 50 < 52 → q->solcocuk = r. Bağ kuruldu.

47261734554952508493qx = 50

TRUE döner. Yalnızca bir işaretçi değişti; ağacın geri kalanına dokunulmadı.

boolean arama_agacina_ekle(int x, agac_gost *L) {
  agac_gost q = NULL, p = *L, r; // q arka, p ön
  while (p) {                    // NULL'a dek in
    q = p;                       // arkayı günle
    if (x < p->veri) p = p->solcocuk;
    else if (x > p->veri) p = p->sagcocuk;
    else { printf("Benzer deger var.\n");
           return FALSE; }
  }
  r = ogeal();                   // çocuklar NULL
  r->veri = x;
  if (*L == NULL) *L = r;        // boş ağaç: kök
  else if (x < q->veri) q->solcocuk = r;
  else q->sagcocuk = r;
  return TRUE;
}

Ekleme

Neden agac_gost *L? Maliyet?

  • Ağaç boşsa (*L == NULL) eklenen öğe kök olur: çağıranın kök işaretçisi değişmelidir.
  • İşaretçinin kendisini değiştirmek için adresi gönderilir (call by reference): arama_agacina_ekle(x, &kok).
  • Maliyet: derinlik kadar karşılaştırma → Ω(lg n) – O(n).
  • C99 düzeltmesi: q = NULL ilk değeri.
/* Özyinelemeli alternatif: yeni kökü döndürür */
agac_gost ekle(agac_gost L, int x) {
  if (!L) {
    L = ogeal();
    L->veri = x;
  } else if (x < L->veri)
    L->solcocuk = ekle(L->solcocuk, x);
  else if (x > L->veri)
    L->sagcocuk = ekle(L->sagcocuk, x);
  return L;        /* kok = ekle(kok, x); */
}

Arama · Kodu adım adım çalıştır

arama_agacinda_ara(52, L) — döngüsel

472617345549528493Laranan x = 52karşılaştırma: 1

52 > 47 → sağa. Sol alt ağaç elenir.

472617345549528493Laranan x = 52karşılaştırma: 2

52 < 55 → sola. Sağ alt ağaç (soluk) hiç incelenmez: aramanın gücü buradan gelir.

472617345549528493Laranan x = 52karşılaştırma: 3

52 > 49 → sağa. Sol alt ağaç elenir.

472617345549528493Laranan x = 52karşılaştırma: 4

52 == 52: bulundu, düğümün adresi döndürülür. Toplam 4 karşılaştırma.

agac_gost arama_agacinda_ara(int x, agac_gost L) {
  while (L) {           // uygun alt ağaçta in
    if (x == L->veri) return L;
    if (x < L->veri) L = L->solcocuk;
    else             L = L->sagcocuk;
  }
  return NULL;          // bulunamadı
}

“Memura görevi tanımlarken müdür çok doğru tanımlamalı. Adam bir daha gelip kapıyı çalmamalı.”

Arama · Kodu adım adım çalıştır

Özyinelemeli arama: ara(34, L)

472617345549528493x = 34

34 < 47: return ara(x, L->solcocuk) — sonucu ne olursa olsun aynen yukarı iletilecek.

472617345549528493x = 34

34 > 26: return ara(x, L->sagcocuk).

472617345549528493x = 34dönen: &düğüm(34)

34 == 34: bulundu, bu çerçeve L'yi döndürür.

472617345549528493x = 34dönen: &düğüm(34)

Alt çağrı &düğüm(34) döndürdü; ara(34, 26) da onu döndürür.

472617345549528493x = 34dönen: &düğüm(34)

Alt çağrı &düğüm(34) döndürdü; ara(34, 47) da onu döndürür.

agac_gost ara(int x, agac_gost L) {
  if (!L) return NULL;          // yok
  if (x == L->veri) return L;   // bulundu
  if (x < L->veri)
    return ara(x, L->solcocuk);
  return ara(x, L->sagcocuk);
}

Arama

Örnek: “Bu örnek çalışır mı?”

if (L) {
  if (x == L->veri) return L;
  if (x <  L->veri) return arama(x, L->sol);
  return arama(x, L->sag);
}
return NULL;   /* “Buraya hiç girmez, ama
                  olsun, biz yazalım da… ☺” */

3. satırdaki if bloğuna girilirse 4. satırdaki sağ alt ağaç araması asla yapılmaz: alt çağrı ne döndürürse o döner.

  • İkili arama ağacında bu davranış tam istenendir: x küçükse sağda olamaz; kod doğrudur ve O(h)'dir.
  • Sıralı olmayan genel ikili ağaçta ise x sağ alt ağaçta olabilir; bu kod onu kaçırır.
  • Genel ağaç için çözüm iki tarafı da arar → O(n).
Hocadan “Özyinelemeli kod yazılırken yandaki örnekteki gibi return satırından sonra işlem yapılıyor.”

Arama

Örnek çözüm: her ikili ağaçta arama

agac_gost ara_ozyineli(int veri, agac_gost L) {
  if (!L) return NULL;              /* ağaç boş mu   */
  if (L->veri == veri) return L;    /* aranan bulundu */
  agac_gost T = ara_ozyineli(veri, L->solcocuk);
  return T ? T : ara_ozyineli(veri, L->sagcocuk);
}
  • Önce solda arar; bulamazsa (T == NULL) sağda arar.
  • Sıralama bilgisi kullanılmaz → en kötü O(n); BST'de gereksiz yere yavaş.
  • Ağırlık bulma sınav sorusunun çözümünde bu yordam kullanılır.
Hocadan “Bu yazdığımız yordamları buradaki herkesin gözü kapalı yazması lazım! Hadi bir gözü açık olsun ☺”

Derinlik

SORU: derinliği döngüsel yazınız

Hocadan “İteratif kod yazmak acemi işi! … Sınavda bu şekilde yazılırsa, çalışsa bile puan alamaz. Neden? Çünkü global değişkenler kullanılıyor. — “Ama doğru çalışıyorsa 1 puan da mı vermezsiniz?” — Hayır, 0.25 bile vermem!”
/* global değişkensiz döngüsel çözüm:
   düzey düzey gezin, düzeyleri say */
int derinlik_d(agac_gost L) {
  agac_gost k[MAX_SIZE];
  int on = 0, arka = 0, d = 0;
  if (L) k[arka++] = L;
  while (on < arka) {          /* bir düzey */
    int son = arka;
    d++;
    for (; on < son; on++) {
      if (k[on]->solcocuk) k[arka++] = k[on]->solcocuk;
      if (k[on]->sagcocuk) k[arka++] = k[on]->sagcocuk;
    }
  }
  return d;       /* MAX_SIZE >= n olmalı */
}

Derinlik

Neden global değişken yok?

Hocadan “Büyük firmalarda büyük işlerin yapılabilmesi için entegre çalışmak gerekir. Birinin yazdığı yordamı ta öbür uçtaki biri kullanıyor. Birbirlerini tanımıyorlar, o ta Hindistan’da! Telefonu açıp “Senin bir max değişkenin var, bu ne oluyor?” diye soramaz ki!”
Yordam kendi içinde tamam olmalı

Girdi parametreden, çıktı dönüş değerinden; gizli durum yok.

Yerel prototip olur

Yordamın içinde başka bir yordamın prototipi yerel olarak bildirilip kullanılabilir; tanım dışarıda kalır (standart C iç içe yordam tanımına izin vermez).

Özyinelemeli çözüm

Her çağrının kendi yerel sol/sağ değişkeni vardır; global sayaç gerekmez.

Derinlik · Kodu adım adım çalıştır

Özyinelemeli derinlik: {sol, sağ} çerçeveleri

A{?,?}BDCEFşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = A: önce sol alt ağacın derinliği: derinlik(B) çağrılır.

A{?,?}B{?,?}DCEFşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = B: önce sol alt ağacın derinliği: derinlik(D) çağrılır.

A{?,?}B{?,?}D{0,0}CEFşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = D: sol = 0, sağ = 0 (NULL → 0) → MAX + 1 = 1 döner; yerel sol/sağ silinir.

A{?,?}B{1,0}D=1CEFşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = B: sol = 1, sağ = 0 (NULL → 0) → MAX + 1 = 2 döner; yerel sol/sağ silinir.

A{2,?}B=2D=1CEFşimdi çalışanyığında {sol,sağ}=döndürdüğü

sol = 2; şimdi derinlik(C).

A{2,?}B=2D=1C{?,?}EFşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = C: önce sol alt ağacın derinliği: derinlik(E) çağrılır.

A{2,?}B=2D=1C{?,?}E{?,?}Fşimdi çalışanyığında {sol,sağ}=döndürdüğü

L = E: önce sol alt ağacın derinliği: derinlik(F) çağrılır.

A{2,?}B=2D=1C{?,?}E{?,?}F{0,0}şimdi çalışanyığında {sol,sağ}=döndürdüğü

L = F: sol = 0, sağ = 0 (NULL → 0) → MAX + 1 = 1 döner; yerel sol/sağ silinir.

A{2,?}B=2D=1C{?,?}E{1,0}F=1şimdi çalışanyığında {sol,sağ}=döndürdüğü

L = E: sol = 1, sağ = 0 (NULL → 0) → MAX + 1 = 2 döner; yerel sol/sağ silinir.

A{2,?}B=2D=1C{2,0}E=2F=1şimdi çalışanyığında {sol,sağ}=döndürdüğü

L = C: sol = 2, sağ = 0 (NULL → 0) → MAX + 1 = 3 döner; yerel sol/sağ silinir.

A{2,3}B=2D=1C=3E=2F=1şimdi çalışanyığında {sol,sağ}=döndürdüğü

L = A: sol = 2, sağ = 3 → MAX + 1 = 4 döner; yerel sol/sağ silinir.

int derinlik(agac_gost L) {
  int sol, sag;
  if (L) {                   // işaretçi NULL mı
    sol = derinlik(L->solcocuk);
    sag = derinlik(L->sagcocuk);
    return MAX(sol, sag) + 1;
  }
  else return 0;             // uç yaprağa ulaşıldı
}

Derinlik

Derinlik: problem yeterince küçüldü mü?

  • Ağacın derinliği, sol ve sağ alt ağaçlarının en derin olanının 1 fazlasıdır: MAX(sol, sağ) + 1.
  • Her düğümün kendi 2 yerel değişkeni vardır: sol ve sağ derinlik. NULL ile yapılan çağrıların yerelleri kullanılmadan silinir.
  • Zaman O(n), bellek O(h). #define MAX(a,b) ((a) > (b) ? (a) : (b))
int derinlik(agac_gost L) {    /* tek satır */
  return !L ? 0 : MAX(derinlik(L->solcocuk),
                      derinlik(L->sagcocuk)) + 1;
}

MAX makrosu argümanını iki kez değerlendirir; bu tek satırlıkta özyinelemeli çağrılar iki kez yapılır → üstel süre! Bu yüzden makro yerine max() işlevi kullanılır (sınav çözümündeki gibi).

Hocadan “Problem yeterince küçüldü mü? Başta da NULL gelebilir!”
Hocadan “Bu iki yaklaşımı (özyinelemeli düşünme, özyinelemeli bağıntıları çözme) bizim mühendis arkadaşlara kabul ettirmek zordur. Adamın bir düşünce tarzı var, ne güzel kod yazıyor!”

Derinlik

Ek parametreler koyma mevzusu

“If you have a procedure with 10 parameters, you probably missed some.”

— Alan J. Perlis

Soru: “Hocam, int derinlik(agac_gost L, int a) diye fazladan bir parametre ekleyip derinlik(L, 0) diye çağırsak olur mu?”

Hocadan “Neden fazladan parametre tanımlanıp ilk çağrıda sıfır aktarılıyor? Yordamı kullanana ne denecek? —“a’ya sıfır aktarmayı sakın ha unutma!” Sıralama yordamı yazılmış; 0 aktarınca sıralıyor da, 1 aktarınca sıralayamıyor… ☺ Olur mu?”
Ama şu olur: küçükten büyüğe mi büyükten küçüğe mi sıralanacağını belirten (−1/+1, '<'/'>') ya da verinin türünü bildiren bir parametre. Parametre davranışı seçmeli, gizli bir başlangıç değeri taşımamalı.

Silme

İkili arama ağacından öğe silme

  • Silerken ağacın ikili arama ağacı özelliği bozulmamalı.
  • Çocuksuz (yaprak): babanın ilgili işaretçisi NULL yapılır.
  • Tek çocuklu: babanın işaretçisi silinenin tek çocuğunu gösterir.
  • İki çocuklu: farklı yöntemler var; örnek ağaçtan 60'ı silelim.
402010306050425552575670656673silinecek öğe: 60

Örnek ağaç: 40, 20, 60, 10, 30, 50, 70, 42, 55, 65, 73, 52, 57, 66, 56 sırasıyla eklenmiştir (derinlik 6).

Silme

1. yöntem: sol alt ağacın en büyüğü

402010306050425552575670656673sol alt ağaçta sürekli sağa: 50 → 55 → 57

Sol alt ağaçtaki en büyük değer: sol alt ağaçta sağ işaret alanları üzerinden ilerlenir; sağ çocuğu olmayan öğe (57) en büyüktür.

40201030605042555257567065667357'nin sol çocuğu 56 var!

Bu öğenin bir sol alt ağacı olabilir (57'nin 56'sı). Bu sol çocuk, taşınacak öğenin atasının sağ bağına aktarılmalı (55 → 56).

4020103057504255525670656673veri kopyalama: 1 bağ günlenir · derinlik 6 → 5

İkinci yol: 60 free edilmez; veri alanına 57 yazılır, yalnızca 55'in sağ bağı 56'ya günlenir → 1 bağ. Birinci yol (düğümü taşımak) 4 bağ ister: 40→57, 55→56, 57.sol→50, 57.sağ→70.

Silme · Kodu adım adım çalıştır

sil(60, &L): iki çocuklu düğüm

402010306050425552575670656673p

p kökü, bag kök işaretçisinin adresini tutar: silinecek düğüme giden bağı değiştirebilmek için.

402010306050425552575670656673p

60 > 40 → bag = &40->sagcocuk, p = 60.

402010306050425552575670656673p

60 bulundu ve iki çocuğu var (50, 70) → sol alt ağacın en büyüğü aranır.

402010306050425552575670656673p

b = &60->solcocuk → *b = 50.

402010306050425552575670656673p

*b'nin sağı var → b = &50->sagcocuk, *b = 55.

402010306050425552575670656673p

*b'nin sağı var → b = &55->sagcocuk, *b = 57.

402010306050425552575670656673p

57'nin sağ çocuğu yok: sol alt ağacın en büyüğü 57.

402010305750425552575670656673p

60'ın veri alanına 57 yazılır (düğüm taşınmaz, 1. yöntemin 2. yolu).

402010305750425552575670656673p

Artık silinecek olan eski 57 düğümü; bag = &55->sagcocuk.

4020103057504255525670656673

*bag = 57'nin tek çocuğu (56): 55'in sağ bağı artık 56'yı gösterir.

4020103057504255525670656673

Eski düğüm free edilir. Ağaç yine ikili arama ağacı; derinlik 6 → 5.

void sil(int x, agac_gost *L) {
  agac_gost p = *L, *bag = L;  // bag: p'ye giden
  while (p && p->veri != x) {  // x'i ara
    bag = (x < p->veri) ? &p->solcocuk
                        : &p->sagcocuk;
    p = *bag;
  }
  if (!p) return;              // x yok
  if (p->solcocuk && p->sagcocuk) {  // 2 çocuk
    agac_gost *b = &p->solcocuk;     // sol alt
    while ((*b)->sagcocuk)           // ağacın en
      b = &(*b)->sagcocuk;           // büyüğü
    p->veri = (*b)->veri;      // veriyi taşı
    bag = b;  p = *b;          // onu sileceğiz
  }
  *bag = p->solcocuk ? p->solcocuk : p->sagcocuk;
  free(p);
}

Silme

Dört yöntem ve derinliğe etkisi

40201030575042555256706566731. yöntem (sol alt ağacın en büyüğü): derinlik 6 → 5

1. yöntem: 57 yukarı, 56 55'in sağına. Derinlik 5'e iner.

40201030655042555257567066732. yöntem (sağ alt ağacın en küçüğü): derinlik 6 → 6

2. yöntem: sağ alt ağacın en küçüğü 65 taşınır; 65'in sağ çocuğu 66, 70'in soluna bağlanır. Derinlik 6.

40201030706550425552575666733. yöntem (sağ çocuk yukarı kayar): derinlik 6 → 7

3. yöntem: silinenin yerine sağ çocuğu (70) kayar; 50 alt ağacı, 70 alt ağacının en küçüğünün (65) soluna asılır. Derinlik 7.

40201030504255525756706566734. yöntem (sol çocuk yukarı kayar): derinlik 6 → 7

4. yöntem: yerine sol çocuğu (50) kayar; 70 alt ağacı, 50 alt ağacının en büyüğünün (57) sağına asılır. Derinlik 7 (40-50-55-57-70-65-66 yolu 7 düğümdür).

Hocadan “Sildiğimiz öğenin kökü olduğu ağacın olabildiğince dengeli olmasını sağlayacak şekilde silmeliyiz.”

Dengeli Ağaçlar

Dengesizliğin maliyeti

ndengeli: ⌈lg(n+1)⌉çarpık: n
1.000101.000
1.000.000201.000.000
2.000.000212.000.000
10930109
  • Rastgele sırada eklenen n anahtarlı BST'nin ortalama derinliği Θ(lg n)'dir; ortalama arama ≈ 1,39 lg n karşılaştırma (Sedgewick; CLRS 12.4).
  • Ama en kötü durum (sıralı veya neredeyse sıralı veri, ki gerçekte sık görülür) Θ(n)'dir.
  • Çözüm: her ekleme/silmeden sonra yerel döndürmelerle derinliği O(lg n) tutan ağaçlar: AVL, kırmızı-siyah, B-ağacı.
Hocadan “O(n) ile O(lg n) arasındaki fark, 2.000.000 ile 21 arasındaki fark gibidir.”

Dengeli Ağaçlar

AVL ağacı (Adelson-Velski ve Landis, 1962)

  • Her düğümde denge faktörü = yük(sol) − yük(sağ) ∈ {−1, 0, +1}.
  • Yükseklik en çok ≈ 1,44 lg(n+2): arama, ekleme, silme O(lg n).
  • Ekleme sıradan BST eklemesiyle yapılır; sonra eklenen yoldan köke doğru denge faktörleri güncellenir.
  • Faktörü ±2 olan ilk ata bulunur; yeni düğümün o atanın hangi torununda olduğuna göre 4 durum: LL, RR, LR, RL.
  • Eklemede tek (LL/RR) ya da çift (LR/RL) döndürme dengeyi yeniden kurar.
typedef struct avl *avl_gost;
struct avl {
    int      veri;
    int      yuk;        /* alt ağaç yüksekliği */
    avl_gost sol, sag;
};
int yuk(avl_gost t) { return t ? t->yuk : 0; }
int denge(avl_gost t) {
    return yuk(t->sol) - yuk(t->sag);
}

Dengeli Ağaçlar

Tek döndürme: LL ve RR

z+2y+1xT1T2T3T4LL: z'nin solunun solu ağır
y0xz0T1T2T3T4sağa döndür(z): y kök olur

LL: yeni düğüm z'nin sol çocuğunun (y) sol alt ağacına girdi, z'nin faktörü +2. z sağa döndürülür: y yukarı çıkar, y'nin sağ alt ağacı T3 z'nin soluna geçer. Sıra korunur: T1 x T2 y T3 z T4.

z−2T1y−1T2xT3T4RR: z'nin sağının sağı ağır
y0xz0T1T2T3T4sola döndür(z): y kök olur

RR ayna durumdur: z sola döndürülür; y'nin sol alt ağacı (T2) z'nin sağına geçer. Her iki durumda alt ağacın yüksekliği eklemeden önceki değerine döner.

Dengeli Ağaçlar

Çift döndürme: LR ve RL

z+2y−1T4T1xT2T3LR: z'nin solunun sağı ağır
zxT4yT3T1T21) sola döndür(y) → LL biçimi

LR: yeni düğüm z'nin sol çocuğunun (y) sağ alt ağacında. Tek sağa döndürme işe yaramaz; önce y sola döndürülür → LL biçimi.

zxT4yT3T1T21) sola döndür(y) → LL biçimi
xyzT1T2T3T42) sağa döndür(z): x kök

Sonra z sağa döndürülür: torun x iki atasının üstüne çıkar. RL bunun aynasıdır: önce sağ çocuk sağa, sonra z sola.

Dengeli Ağaçlar · Kodu adım adım çalıştır

sag_dondur(): LL durumunda iki işaretçi

30yük 420yük 340yük 110yük 225yük 15yük 1k2

5 eklendi: 30'un faktörü yük(20)=3 − yük(40)=1 = +2, sol-sol ağır → LL: sag_dondur(30).

30yük 420yük 340yük 110yük 225yük 15yük 1k2k1

k1 = k2->sol = 20: yeni kök olacak düğüm.

30yük 420yük 340yük 110yük 225yük 15yük 1k2k1

k2->sol = k1->sag: 25, 30'un soluna geçer (20 < 25 < 30, sıra korunur). 25'e şimdilik iki işaretçi var.

30yük 420yük 340yük 110yük 225yük 15yük 1k2k1

k1->sag = k2: 30, 20'nin sağ çocuğu olur. Bağlar bitti; çizimi düzeltelim →

20yük 310yük 230yük 25yük 125yük 140yük 1k2k1

Yükseklikler aşağıdan yukarı: önce k2 (30): 1 + MAX(1, 1) = 2.

20yük 310yük 230yük 25yük 125yük 140yük 1k1

Sonra k1 (20): 1 + MAX(yük(10)=2, 2) = 3. Alt ağacın yüksekliği eklemeden önceki gibi 3.

20yük 310yük 230yük 25yük 125yük 140yük 1k1

Yeni kök k1 döner; çağıran, babanın işaretçisini buna bağlar. Yalnızca 2 işaretçi değişti: O(1).

avl_gost sag_dondur(avl_gost k2) {  // LL
  avl_gost k1 = k2->sol;
  k2->sol = k1->sag;
  k1->sag = k2;
  k2->yuk = 1 + MAX(yuk(k2->sol), yuk(k2->sag));
  k1->yuk = 1 + MAX(yuk(k1->sol), k2->yuk);
  return k1;                       // yeni kök
}

Dengeli Ağaçlar

AVL'ye 3, 2, 1, 4, 5, 6, 7, 16, 15 ekleme

30küçük sayı: denge faktörü

3 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.

3120küçük sayı: denge faktörü

2 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.

201030küçük sayı: denge faktörü

1 eklendi. Dengesizlik → LL (3'te sağa döndürme).

2-1103-140küçük sayı: denge faktörü

4 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.

2-110403050küçük sayı: denge faktörü

5 eklendi. Dengesizlik → RR (3'te sola döndürme).

402010305-160küçük sayı: denge faktörü

6 eklendi. Dengesizlik → RR (2'de sola döndürme).

40201030605070küçük sayı: denge faktörü

7 eklendi. Dengesizlik → RR (5'te sola döndürme).

4-12010306-1507-1160küçük sayı: denge faktörü

16 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.

4-12010306-15015070160küçük sayı: denge faktörü

15 eklendi. Dengesizlik → RL (16'da sağa, 7'de sola).

Aynı sıra sıradan BST'ye eklenseydi 3–4–5–6–7–16–15 zinciri yüzünden derinlik 7 olurdu; AVL'de 4. (Örnek: Weiss, Data Structures and Algorithm Analysis in C, 4.4)

Dengeli Ağaçlar

AVL ekleme: BST eklemesi + dönüşte denge

avl_gost avl_ekle(avl_gost t, int x) {
  if (!t) return yeni_avl(x);              /* malloc, yuk = 1        */
  if (x < t->veri)      t->sol = avl_ekle(t->sol, x);
  else if (x > t->veri) t->sag = avl_ekle(t->sag, x);
  else return t;                           /* tekrar eden değer yok  */
  t->yuk = 1 + MAX(yuk(t->sol), yuk(t->sag));
  int d = denge(t);
  if (d >  1 && x < t->sol->veri) return sag_dondur(t);            /* LL */
  if (d < -1 && x > t->sag->veri) return sol_dondur(t);            /* RR */
  if (d >  1) { t->sol = sol_dondur(t->sol); return sag_dondur(t); } /* LR */
  if (d < -1) { t->sag = sag_dondur(t->sag); return sol_dondur(t); } /* RL */
  return t;
}
  • Özyinelemeden dönerken her ata güncellenir; eklemede en çok bir (tek ya da çift) döndürme yeter.
  • Kullanım: kok = avl_ekle(kok, x); — yeni kök döndüğü için *L gerekmez. Silme de benzer ama O(lg n) döndürme gerekebilir.

Dengeli Ağaçlar

Kısa bakış: kırmızı-siyah ağaç ve B-ağacı

Kırmızı-siyah ağaç (CLRS 13)
  • Her düğüm kırmızı ya da siyah; kök ve NULL yapraklar siyah.
  • Kırmızı düğümün çocukları siyah.
  • Her düğümden NULL'lara giden her yolda aynı sayıda siyah düğüm.
  • Sonuç: yükseklik ≤ 2 lg(n+1); ekleme/silmede en çok 2–3 döndürme + renk değişimi.
  • C++ std::map, Java TreeMap, Linux çekirdeği.
B-ağacı (Bayer–McCreight 1972)
  • m yollu dengeli arama ağacı: düğüm başına ⌈m/2⌉−1 … m−1 anahtar.
  • Tüm yapraklar aynı düzeyde.
  • Dolan düğüm ortadaki anahtarı babaya çıkararak bölünür; ağaç kökten büyür.
  • Bir düğüm = bir disk bloğu → çok az disk erişimi: veritabanı dizinleri, dosya sistemleri (B+ ağacı).
30 | 6010 | 2040 | 5070 | 803. dereceden B-ağacı (2-3 ağacı)

Dengeli Ağaçlar

Hangi arama ağacı?

yapıarama / ekleme / silme (en kötü)denge koşulunerede?
ikili arama ağacıO(n) (ortalama O(lg n))yokküçük, rastgele veri; öğretim
AVLO(lg n)|yük(sol) − yük(sağ)| ≤ 1arama ağırlıklı işler
kırmızı-siyahO(lg n)siyah yükseklik eşitkütüphane eşlemeleri (map)
B-ağacı / B+O(logm n) disk erişimiyapraklar aynı düzeydeveritabanı, dosya sistemi
splay (“zig-zag”)O(lg n) amortizeyok; erişilen köke çıkarsık erişilen öğeler

Sınav Soruları

Sınav sorusu: bir öğenin ağırlığı

Değerlerin tekrarlı olmadığı bir ikili ağaçta x değerini içeren öğenin ağırlığı: “uç düğümlere kadar olan yolların en uzunu”. x yoksa ya da L = NULL ise 0. int AgirlikBul(agac_gost L, int x) yordamını yazınız. Global değişken kullanmayın.

Örnek çözüm
int AgirlikBul(agac_gost L, int x) {
  /* agacta_ara: x'i içeren öğe ya da NULL */
  agac_gost agacta_ara(agac_gost L, int x);
  int derinlik(agac_gost L);
  return derinlik(agacta_ara(L, x));
}
agac_gost agacta_ara(agac_gost L, int x) {
  if (!L) return NULL;
  if (L->veri == x) return L;
  agac_gost T = agacta_ara(L->solcocuk, x);
  return T ? T : agacta_ara(L->sagcocuk, x);
}
int derinlik(agac_gost L) {
  int max(int a, int b);  /* a,b'den küçük olmayan */
  return L ? max(derinlik(L->solcocuk),
                 derinlik(L->sagcocuk)) + 1 : 0;
}

Tasarım: iki iyi tanımlı yordamın bileşimi; derinlik(NULL) = 0 sayesinde “bulunamadı” ayrıca ele alınmaz.

Sınav Soruları

Sınav sorusu: AgirlikBul kaç kez işletilir?

int AgirlikBul(oge_gost L, int x) {
  if (!L) return 0;
  else if (L->veri == x) return agirlik(L);
  if (AgirlikBul(L->solcocuk, x) >
      AgirlikBul(L->sagcocuk, x))
    AgirlikBul(L->solcocuk, x);
  if (AgirlikBul(L->sagcocuk, x) >
      AgirlikBul(L->solcocuk, x))
    AgirlikBul(L->sagcocuk, x);
  return 0;
}

Ağaç: kök 2, sol 10, sağ 20. i) x = 20 ve x = 30 için kaç çağrı? ii) En kötü durum için özyinelemeli ilişkiyi yazıp çözünüz.

Çözüm

i) x = 20: 14, x = 30: 21. Her yaprak çağrısı 1 + 4 NULL çağrısı = 5 çağrı yapar; x = 30'da kök 1 + 4·5 = 21.

ii) x ağaçta yoksa her çağrı her alt ağacı iki kez çağırır. Çarpık ağaçta T(n) = 2T(n−1) + c → Θ(2n); dengeli ağaçta T(n) = 4T(n/2) + c → Θ(n2). Doğru tasarım (ağırlık bulma sorusunun çözümü) O(n)'dir.

Sınav Soruları

Sınav sorusu: SWAP iki özyinelemeli çağrının arasında

{44, 30, 15, 18, 86, 97, 65, 113, 74, 129} ile kurulan BST için yordam'ı işletiniz; SWAP satırından her geçişte ağaç görünümü değişmişse yeniden çiziniz; son hâli yorumlayınız.

void yordam(agac_gost L) {
  agac_gost p;
  if (L) {
    yordam(L->solcocuk);
    SWAP(L->solcocuk, L->sagcocuk, p);
    yordam(L->sagcocuk);
  }
}
Çözüm (adım adım)
4430151886657497113129

Başlangıç: 44, 30, 15, 18, 86, 97, 65, 113, 74, 129 ile kurulan ikili arama ağacı.

4430151886657497113129

SWAP 15'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.

4430151886657497113129

SWAP 30'da: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.

4430151886657497113129

SWAP 15'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.

4486657497113129301518

SWAP 44'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.

4486657497113129301518

SWAP 30'da: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.

4486657497113129301518

Son hâl: kökün iki alt ağacı yer değiştirdi ama sağa geçen alt ağaçlar (eski sol) iki kez işlendiği için tam ayna olmadı.

Yorum: SWAP öncesine ya da sonrasına yazılsaydı ağacın tam aynası elde edilirdi; arada olunca sol çocuk SWAP ile sağa geçip aynı düğüm için bir kez daha işlenir.

Özet

Karmaşıklık özeti

işlemen iyi / dengelien kötü (çarpık)ek bellek
arama_agacinda_ara (döngüsel)O(lg n)O(n)O(1)
ara (özyinelemeli)O(lg n)O(n)O(h) yığın
arama_agacina_ekleΩ(lg n)O(n)O(1)
silO(lg n)O(n)O(1)
derinlikO(n)O(n)O(h)
genel ikili ağaçta araO(n)O(n)O(h)
AVL arama / ekleme / silmeO(lg n)O(lg n)O(lg n) özyineleme
tek döndürmeO(1)O(1)—

Özet

Sık yapılan hatalar

1Boş ağaca eklerken kök işaretçisini güncelleyememek (agac_gost L yerine *L gerekir).
2Arkadaki işaretçiyi (q) tutmadan NULL'a kadar inip bağlanacak düğümü kaybetmek.
3Özyinelemeli aramada alt çağrının sonucunu return etmemek.
4İki çocuklu silmede taşınan öğenin sol çocuğunu unutmak (56 kaybolur).
5Derinlik için global değişken ya da gereksiz ek parametre kullanmak.
6MAX makrosuna özyinelemeli çağrı koyup üstel süreye düşmek.

Özet

Alıştırmalar

  1. Boş bir BST'ye sırasıyla 50, 30, 70, 20, 40, 60, 80, 35 ekleyin; kök önde sırasını yazın.
  2. Aynı ağaçtan 30'u 1. ve 2. yöntemle silin; derinlikleri karşılaştırın.
  3. En küçük ve en büyük değeri döndüren döngüsel yordamları yazın.
  4. Bir ikili ağacın BST olup olmadığını O(n)'de denetleyen yordamı yazın.
  5. Boş AVL'ye 10, 20, 30, 25, 28 ekleyin; hangi döndürmeler yapılır?
  6. x'ten küçük anahtar sayısını bulan yordamı yazın (kök ortada gezinmeden yararlanılabilir).
İpuçları / cevaplar
  1. Kök önde: 50 30 20 40 35 70 60 80.
  2. 1. yöntem: 20 yukarı (derinlik 4 kalır); 2. yöntem: 35 yukarı, 40'ın solu NULL (derinlik 3).
  3. En küçük: sürekli solcocuk; en büyük: sürekli sagcocuk.
  4. Her düğüme izin verilen (alt, üst) aralığı parametre olarak geçirilir; yalnız çocukla karşılaştırmak yetmez.
  5. 30'da RR (20 kök); 28'de LR: 30'un solu 25, önce 25'te sola, sonra 30'da sağa → 20(10, 28(25, 30)).
  6. Kök ortada gezinirken x'e kadar sayılır ya da düğümlerde alt ağaç boyutu tutulur.

Özet

Bölüm özeti

Tanım

Sol alt ağaç küçük, sağ büyük, değerler tek; kök ortada = sıralı.

Ekleme

İlk NULL'a eklenir; arka işaretçi q ve *L ile boş ağaç.

Arama

Her karşılaştırma bir alt ağacı eler: O(h).

Derinlik

MAX(sol, sağ) + 1; global değişken ve gereksiz parametre yok.

Silme

0/1 çocuk kolay; 2 çocukta öncel/ardıl ile veri değiştir (1 bağ).

Denge

Sıralı veri O(n) yapar; AVL/kırmızı-siyah O(lg n) garanti eder.

Özet

Kaynaklar ve devam

  • Horowitz–Sahni–Anderson-Freed, Fundamentals of Data Structures in C, 5.7 (BST), 10.2 (AVL).
  • Cormen vd., Introduction to Algorithms, Bölüm 12 (BST), 13 (kırmızı-siyah), 18 (B-ağacı).
  • M. A. Weiss, Data Structures and Algorithm Analysis in C, 4.4 (AVL); R. Sedgewick, Algorithms in C, Bölüm 12–13.

Son

Bölüm 11: Arama Ağaçları
Sonraki bölüm: Bölüm 12: Kılavuzlu İkili Ağaçlar · Ders sayfası
1 / 1 C ile Veri Yapıları