Bölüm 12: Kılavuzlu İkili Ağaçlar

C ile Veri Yapıları ve Algoritmalar
n+1 NULL işaretçiKurma kurallarıBaşlık düğümüArdıl bulmaYığınsız gezinmeSağa / sola eklemeMorris gezinmesi
Kaynak: Horowitz–Sahni–Anderson-Freed 5.5

Giriş

Öğrenme Çıktıları

Boş işaretçileri saymak

n düğümlü ikili ağaçta n+1 NULL işaretçi olduğunu kanıtlamak.

Kılavuz kurmak

Sol NULL → kök ortada öncel, sağ NULL → kök ortada ardıl kuralını uygulamak.

Ayırt etmek

solKilavuz / sagKilavuz bayrakları ve başlık düğümüyle yapıyı eksiksiz tanımlamak.

Yığınsız gezinmek

kokortada_ardil_bul ile O(1) ek bellekle kök ortada (ve kök önde) gezinmek.

Ekleme yapmak

saga_ekle'nin iki durumunu tek yordamda birleştirmek; sola_ekle'yi türetmek.

İyi kod yazmak

Açık tanımlı yordam, anlamlı açıklama satırı, NULL denetiminin yeri.

Neden Kılavuz?

n düğümde n+1 NULL işaretçi

  • Her düğümde 2 işaretçi alanı → ağaçta toplam 2n işaretçi.
  • Kök dışındaki her düğüme babasından bir işaretçi gelir → n − 1 işaretçi dolu.
  • Geri kalan 2n − (n−1) = n + 1 işaretçi NULL: işaretçilerin yarıdan fazlası boşa!
“A year spent in artificial intelligence is enough to make one believe in God.”

— Alan J. Perlis

ABDCn = 4 NULL = 5ABCDn = 4 NULL = 5ABDHECFJGKn = 10 NULL = 11

Kesikli kısa çizgiler NULL işaretçiler; üç örnekte de NULL sayısı n + 1.

Neden Kılavuz?

Fikir: boş işaretçileri yol gösterici yapmak

  • A. J. Perlis ve C. Thornton, NULL işaretçilerin ağaçtaki diğer düğümleri göstermek için kullanılabileceğini gösterdiler; bu işaretçilere kılavuz (thread) dediler.
  • Kazanç: kök ortada gezinme için yığın ya da özyineleme gerekmez; her düğümden ardılına doğrudan gidilir.
  • Bedel: her işaretçinin çocuk mu kılavuz mu olduğunu söyleyen birer bit.
Özyinelemeli gezinme O(h) çağrı yığını, iteratif gezinme O(h) yığın kullanır. Kılavuzlu ağaçta ek bellek O(1).

Kurma Kuralları

Kılavuzlu ikili ağaç kurma kuralları

  1. p->solcocuk == NULL ise p->solcocuk, kök ortada gezinmede p'den önce ziyaret edilen öğeyi gösterir: NULL, p'nin kök ortada önceli (inorder predecessor) ile değiştirilir.
  2. p->sagcocuk == NULL ise p->sagcocuk, p'den sonra ziyaret edilen öğeyi gösterir: NULL, p'nin kök ortada ardılı (inorder successor) ile değiştirilir.
başlıkABDHIECFGkök ortada: H D I B E A F C G

Kesikli kırmızı oklar kılavuzlardır. Kılavuzlar, ağacın kök ortada sırasını ağacın içine “örer”.

Kurma Kuralları

Kılavuzları tek tek kurmak

ABDHIECFGHDIBEAFCG

Sıradan ikili ağaç: 9 düğüm, 10 NULL işaretçi (kesikli). Kök ortada sıra: H D I B E A F C G.

ABDHIECFGHDIBEAFCG

H'nin sol işaretçisi NULL → kök ortada önceli başlığı gösterir (öncel/ardıl yok: şimdilik havada, başlık düğümüne bağlanacak).

ABDHIECFGHDIBEAFCG

H'nin sağ işaretçisi NULL → kök ortada ardılı D'yi gösterir.

ABDHIECFGHDIBEAFCG

I'nın sol işaretçisi NULL → kök ortada önceli D'yi gösterir.

ABDHIECFGHDIBEAFCG

I'nın sağ işaretçisi NULL → kök ortada ardılı B'yi gösterir.

ABDHIECFGHDIBEAFCG

E'nin sol işaretçisi NULL → kök ortada önceli B'yi gösterir.

ABDHIECFGHDIBEAFCG

E'nin sağ işaretçisi NULL → kök ortada ardılı A'yı gösterir.

ABDHIECFGHDIBEAFCG

F'nin sol işaretçisi NULL → kök ortada önceli A'yı gösterir.

ABDHIECFGHDIBEAFCG

F'nin sağ işaretçisi NULL → kök ortada ardılı C'yi gösterir.

ABDHIECFGHDIBEAFCG

G'nin sol işaretçisi NULL → kök ortada önceli C'yi gösterir.

ABDHIECFGHDIBEAFCG

G'nin sağ işaretçisi NULL → kök ortada ardılı başlığı gösterir (öncel/ardıl yok: şimdilik havada, başlık düğümüne bağlanacak).

Kurma Kuralları

Kırmızı oku derleyiciye nasıl anlatırız?

typedef struct kilavuz_agac *kilavuz_gost;
struct kilavuz_agac {
    short int    solKilavuz; /* TRUE: solCocuk kılavuz */
    kilavuz_gost solCocuk;
    char         veri;
    kilavuz_gost sagCocuk;
    short int    sagKilavuz; /* TRUE: sagCocuk kılavuz */
};

Bir struct tanımı kapanıştaki ; ile biter (satır 8); unutmak sık yapılan bir hatadır. TRUE/FALSE: enum { FALSE, TRUE }.

Hocadan “Derleyicide kırmızı pointer var mı? Onların kırmızı olduğunu nereden anlayacağız? Fazladan iki bilgi tutmak gerekir.”
Hocadan “Alan karmaşıklığı artmaz mı? Artsın, zaman karmaşıklığı alan karmaşıklığından daha önemlidir. Kaldı ki, bunu 2 bitle bile halledebilirim.”

Kurma Kuralları

Havada kalan iki kılavuz ve başlık düğümü

  • H'nin sol ve G'nin sağ kılavuzunun gösterecek öğesi yok: havada asılı.
  • Bir başlık öğesi ekleriz: gerçek ağacın kökü L->solCocuk ile erişilir.
  • Asılı iki kılavuz başlığı gösterir; başlığın sağı hep kendisidir, sağ kılavuzu FALSE.
  • Boş ağaçta sol işaretçi de başlığı gösterir, sol kılavuz TRUE; öğe eklenince FALSE yapılır.
solKilavuzsolCocukverisagCocuksagKilavuz
TRUE→ başlık–→ başlıkFALSE

Boş kılavuzlu ağaç

Kurma Kuralları

Başlık düğümü: boş ağaç ve dolu ağaç

başlıkTFsolCocuk → kendisi (kılavuz, TRUE)sagCocuk → kendisi (FALSE)

Boş ağaç: yalnızca başlık; iki işaretçisi de kendini gösterir.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTT

Dolu ağaç: başlığın solu köke (A); H'nin sol ve G'nin sağ kılavuzu başlığa bağlandı. T/F: kılavuz bayrakları.

Kurma Kuralları

Ağaç, kök ortada gezinmeye göre kurulur

Hocadan “İkili ağacı inorder gezinmeye göre oluşturuyoruz. Preorder gezinmeye göre zaten kurulamaz.”
gezinmesıra
kök ortada (in-order)H D I B E A F C G
kök önde (pre-order)A B D H I E C F G
kök sonda (post-order)H I D E B F G C A

Kılavuzlar kök ortada öncel/ardıla göre kurulur; kök önde ve kök sonda gezinme de (daha dolambaçlı yoldan) bu kılavuzlarla yapılabilir.

Kök Ortada Gezinme

Kök ortada ardılı bulmak

  1. p->sagKilavuz == TRUE ise p->sagCocuk (kılavuz olduğu için) zaten p'nin ardılıdır.
  2. p->sagKilavuz == FALSE ise sağ çocuktan başlanıp solCocuk bağları solKilavuz TRUE oluncaya dek izlenir: sağ alt ağacın en soldaki düğümü ardıldır.
kilavuz_gost kokortada_ardil_bul(kilavuz_gost L) {
  // L ile işaret edilen (NULL olmayan) öğenin ardılı
  kilavuz_gost p = L->sagCocuk;
  if (!L->sagKilavuz)        // sağ alt ağaç var mı
    while (!p->solKilavuz)   // en soluna dek in
      p = p->solCocuk;
  return p;
}

Kök Ortada Gezinme · Kodu adım adım çalıştır

kokortada_ardil_bul(): başlık, A ve I için

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTL,p

kokortada_ardil_bul(başlık): p = başlık->sagCocuk = başlık.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTL,p

sagKilavuz FALSE: başlığın gerçek bir sağ alt ağacı var; ardıl oranın en solunda.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

başlık->solKilavuz FALSE → sola: p = A.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

A->solKilavuz FALSE → sola: p = B.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

B->solKilavuz FALSE → sola: p = D.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

D->solKilavuz FALSE → sola: p = H.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

H->solKilavuz TRUE: daha sola gidilemez → en soldaki düğüm.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

H döndürülür: başlığın kök ortada ardılı. (Başlığın ardılı = ilk ziyaret edilen düğüm.)

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

kokortada_ardil_bul(A): p = A->sagCocuk = C.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

sagKilavuz FALSE: A'nın gerçek bir sağ alt ağacı var; ardıl oranın en solunda.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

C->solKilavuz FALSE → sola: p = F.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

F->solKilavuz TRUE: daha sola gidilemez → en soldaki düğüm.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

F döndürülür: A'nın kök ortada ardılı.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

kokortada_ardil_bul(I): p = I->sagCocuk = B.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

I'nın sağ işaretçisi bir kılavuz (TRUE): ardıl doğrudan B. Döngüye girilmez.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTLp

B döndürülür: I'nın kök ortada ardılı.

kilavuz_gost kokortada_ardil_bul(kilavuz_gost L) {
  // L ile işaret edilen (NULL olmayan) öğenin ardılı
  kilavuz_gost p = L->sagCocuk;
  if (!L->sagKilavuz)        // sağ alt ağaç var mı
    while (!p->solKilavuz)   // en soluna dek in
      p = p->solCocuk;
  return p;
}

Kök Ortada Gezinme · Kodu adım adım çalıştır

kilavuz_kokortada_dolas(): – H D I B E A F C G –

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = L: başlıktan başlanır. Yığın yok, özyineleme yok; tek bir işaretçi yeter.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(başlık) = H (sağ alt ağacın en solu); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(H) = D (kılavuzla doğrudan); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(D) = I (sağ alt ağacın en solu); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(I) = B (kılavuzla doğrudan); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(B) = E (sağ alt ağacın en solu); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(E) = A (kılavuzla doğrudan); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(A) = F (sağ alt ağacın en solu); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(F) = C (kılavuzla doğrudan); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

p = ardıl(C) = G (sağ alt ağacın en solu); yazdırılır.

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

G'nin ardılı (kılavuzla doğrudan) başlık: gezinme tamamlandı → break.

void kilavuz_kokortada_dolas(kilavuz_gost L) {
  kilavuz_gost p = L;        // L: başlık düğümü
  for (;;) {
    p = kokortada_ardil_bul(p);
    if (p == L) break;       // başlığa dönüldü mü
    printf("%c ", p->veri);
  }
}

Kök Ortada Gezinme

Kılavuzlu gezinmenin maliyeti

  • Her kılavuz ve her gerçek sol/sağ bağ en çok bir kez izlenir → O(n) zaman.
  • Ek bellek O(1): yalnızca p işaretçisi (özyinelemeli gezinmede O(h) çağrı yığını, iteratifte O(h) yığın).
  • Herhangi bir düğümden ardılına, gezinmeyi baştan yapmadan geçilebilir.
Hocadan “Bu iki zât diyor ki: “Kök ortada gezinmeye göre kurduğum bu ağaçta döngüsel olarak hem postorder hem de preorder gezinebilirim.” Preorder kolayca yazılabilir; postorder’ı yazmak öncekinden zordur.”
Hocadan “Bir problemin çözümüne yönelik makaleler okunur; O(n²)’de çözülebilmiş. Oturup çalışarak O(n1.89)’a düşüren bir algoritma yazmak büyük iştir! Literatüre o algoritma geçer.”
Satır numaraları

Kod parçalarındaki satır numaraları, editörde ALT basılı tutularak sütun seçimiyle seçilip DEL ile silinebilir.

Kök Ortada Gezinme · Kodu adım adım çalıştır

Kök önde gezinme aynı kılavuzlarla

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(başlık) = A (sol çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(A) = B (sol çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(B) = D (sol çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(D) = H (sol çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(H) = I (sağ kılavuzlarla D üzerinden sağ çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(I) = E (sağ kılavuzlarla B üzerinden sağ çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(E) = C (sağ kılavuzlarla A üzerinden sağ çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(C) = F (sol çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

ardıl(F) = G (sağ kılavuzlarla C üzerinden sağ çocuk).

başlıkFFAFFBFFDFFHTTITTETTCFFFTTGTTp

G: sağ kılavuzlarla başlık üzerinden sağ çocuk → başlık: gezinme biter.

kilavuz_gost kokonde_ardil_bul(kilavuz_gost p) {
  if (!p->solKilavuz)        // sol çocuk varsa o
    return p->solCocuk;
  while (p->sagKilavuz)      // sağ kılavuzlarla çık
    p = p->sagCocuk;
  return p->sagCocuk;        // ilk gerçek sağ çocuk
}

Sol çocuk varsa ardıl odur; yoksa sağ kılavuzlarla yukarı çıkılıp ilk gerçek sağ çocuğa geçilir. Kök sonda için baba bilgisi gerekir (daha zor).

Kök Ortada Gezinme

Morris gezinmesi: kılavuzları geçici kurmak

void morris_kokortada(agac_gost L) {
  while (L) {
    if (!L->solcocuk) {             /* sol yok: yaz, sağa */
      printf("%c", L->veri);  L = L->sagcocuk;
    } else {
      agac_gost p = L->solcocuk;    /* kök ortada önceli */
      while (p->sagcocuk && p->sagcocuk != L)
        p = p->sagcocuk;
      if (!p->sagcocuk) {           /* geçici kılavuz kur */
        p->sagcocuk = L;  L = L->solcocuk;
      } else {                      /* kılavuzu sök */
        p->sagcocuk = NULL;
        printf("%c", L->veri);  L = L->sagcocuk;
      }
    }
  }
}

J. M. Morris (1979). Bayrak alanı gerekmez: kılavuz gezinme sırasında kurulup sökülür; ağaç o sırada değiştiği için eşzamanlı okumaya uygun değildir.

Kök Ortada Gezinme

Morris gezinmesi adım adım: A(B(D,E),C)

ABDECLçıktı:

Başlangıç: yığın yok. Solu olan her düğümde, öncelinin (boş) sağ işaretçisine geçici kılavuz kurulacak.

ABDECLçıktı:

A'nın önceli E; sağı boş → kılavuz E → A kurulur, sola inilir.

ABDECLçıktı:

B'nin önceli D; sağı boş → kılavuz D → B kurulur, sola inilir.

ABDECLçıktı: D

D'nin solu yok: yazılır, sağa (ya da geçici kılavuzla) geçilir → B.

ABDECLçıktı: DB

Öncel D'den kılavuzla B'ye dönüldü: kılavuz sökülür, B yazılır, sağa.

ABDECLçıktı: DBE

E'nin solu yok: yazılır, sağa (ya da geçici kılavuzla) geçilir → A.

ABDECLçıktı: DBEA

Öncel E'den kılavuzla A'ya dönüldü: kılavuz sökülür, A yazılır, sağa.

ABDECLçıktı: DBEAC

C'nin solu yok: yazılır, sağa (ya da geçici kılavuzla) geçilir → NULL.

ABDECçıktı: DBEAC

Bitti: DBEAC. Ağaç eski hâline döndü; ek bellek O(1), zaman O(n).

Düğüm Ekleme

Kılavuzlu ağaca düğüm ekleme

  • Ekleme tek başına kolay bir yordam değildir, biraz girifttir.
  • Sağa ve sola ekleme biçimi farklı → iki ayrı yordam: saga_ekle, sola_ekle.
  • Sağa eklemede iki durum: (1) babanın sağ alt ağacı yok, (2) var.
  • BST'de ekleme hep çocuğu olmayan yere tek atamaydı; burada kılavuzların (kırmızı okların) da doğru yeri göstermesi gerekir.
cocuk->sagCocuk  = baba->sagCocuk;
cocuk->sagKilavuz = TRUE;  // babanın sağı yoktu
cocuk->solCocuk  = baba;
cocuk->solKilavuz = TRUE;
baba->sagCocuk   = cocuk;
baba->sagKilavuz = FALSE;  // TRUE idi

Durum 1: 6 aktarma komutuyla ekleme.

Düğüm Ekleme · Kodu adım adım çalıştır

Durum 1: sağ alt ağacı olmayan babaya ekleme

başlıkFFATFBFTCTTbabacocukDcocukFF

cocuk->sagCocuk = baba->sagCocuk: babanın sağ kılavuzu (ardılı) çocuğa geçer. (Başlık olup olmadığı bizi ilgilendirmiyor.)

başlıkFFATFBFTCTTbabacocukDcocukFT

cocuk->sagKilavuz = TRUE: babanın alt ağacı yoktu, çocuğun da olamaz → kırmızı ok.

başlıkFFATFBFTCTTbabacocukDcocukFT

cocuk->solCocuk = baba: çocuğun kök ortada önceli baba.

başlıkFFATFBFTCTTbabacocukDcocukTT

cocuk->solKilavuz = TRUE: bu da bir kılavuz (kırmızı).

başlıkFFATFBFTCTTbabacocuk

baba->sagCocuk = cocuk: babadan çocuğa gerçek (siyah) bağ.

başlıkFFATFBFFCTTDTTbabacocuk

baba->sagKilavuz = FALSE: babanın sağı artık kılavuz değil, çocuk. Ekleme bitti: 6 aktarma.

cocuk->sagCocuk  = baba->sagCocuk;
cocuk->sagKilavuz = TRUE;  // babanın sağı yoktu
cocuk->solCocuk  = baba;
cocuk->solKilavuz = TRUE;
baba->sagCocuk   = cocuk;
baba->sagKilavuz = FALSE;  // TRUE idi

Düğüm Ekleme

Durum 2: babanın sağ alt ağacı var

  • İstenen: D, B'nin çocuğu iken X'in B'nin çocuğu yapılması; ama D alt ağacı kaybolmamalı, kırmızı işaretçiler yine doğru yerleri göstermeli.
  • X'in önceli B olur. D alt ağacının en solundaki E'nin eski önceli B idi; şimdi E, X'in ardılı oldu.
  • Öyleyse: çocuk, babanın eski ardılının önceli yapılmalıdır.
  • Durum 2 kodunda: 2. satır cocuk->sagKilavuz = FALSE (babanın alt ağacı şimdi çocuğun alt ağacı); E'nin sol kılavuzu ayrıca düzeltilmelidir.
SONUÇ: 2. durum için babanın eski ardılının öncel bilgisini günlemek ve 2 numaralı aktarmayı cocuk->sagKilavuz = baba->sagKilavuz yazmak suretiyle her iki durumun da üstesinden gelen tek bir yordam yazılabilir.

Düğüm Ekleme · Kodu adım adım çalıştır

saga_ekle(B, X): tek yordam, 2. durum

başlıkFFATFBFFCTTDFFETTFTTbabacocukXcocukFF

cocuk->sagCocuk = baba->sagCocuk: X'in sağına B'nin eski sağ alt ağacı (D) gelir.

başlıkFFATFBFFCTTDFFETTFTTbabacocukXcocukFF

cocuk->sagKilavuz = baba->sagKilavuz (FALSE): babanın alt ağacı artık çocuğun alt ağacı; siyahlık korunur.

başlıkFFATFBFFCTTDFFETTFTTbabacocukXcocukFF

cocuk->solCocuk = baba: X'in önceli B.

başlıkFFATFBFFCTTDFFETTFTTbabacocukXcocukTF

cocuk->solKilavuz = TRUE: kırmızı ok.

başlıkFFATFBFFCTTXTFDFFETTFTTbabacocuk

baba->sagCocuk = cocuk: B → X gerçek bağ.

başlıkFFATFBFFCTTXTFDFFETTFTTbabacocuk

baba->sagKilavuz = FALSE: zaten FALSE idi; 1. durum için gerekli, burada zararsız. “Kodun doğru çalıştığına hemen karar verilmemeli!”

başlıkFFATFBFFCTTXTFDFFETTFTTbabacocuk

cocuk->sagKilavuz FALSE → X'in sağ alt ağacı var: X'in ardılının sol kılavuzu hâlâ B'yi gösteriyor, düzeltilmeli.

başlıkFFATFBFFCTTXTFDFFETTFTTbabacocuk

p = kokortada_ardil_bul(X) = E (D'nin en solu). E'nin eski önceli B idi.

başlıkFFATFBFFCTTXTFDFFETTFTTbabacocuk

p->solCocuk = cocuk: E'nin sol kılavuzu artık X'i gösterir. Çocuk, babanın eski ardılının önceli yapıldı.

void saga_ekle(kilavuz_gost baba,
               kilavuz_gost cocuk) {
  // cocuk'u baba'nın sağ çocuğu yapar (kılavuzlu)
  kilavuz_gost p;
  cocuk->sagCocuk   = baba->sagCocuk;
  cocuk->sagKilavuz = baba->sagKilavuz;
  cocuk->solCocuk   = baba;
  cocuk->solKilavuz = TRUE;
  baba->sagCocuk    = cocuk;
  baba->sagKilavuz  = FALSE;
  if (!cocuk->sagKilavuz) { // babanın alt ağacı var mı
    p = kokortada_ardil_bul(cocuk);
    p->solCocuk = cocuk;     // ardılın önceli çocuk
  }
}

Düğüm Ekleme

saga_ekle satır satır

[1] Açık tanım

Adı belli, giden parametreler belli, dönüş değeri belli.

[4–5]

Çocuğun sağ işaretçi ve sağ kılavuzu babadan alınır: 1. durumda kırmızılık (TRUE), 2. durumda siyahlık (FALSE) korunur.

[6–7]

Çocuğun kök ortada önceli baba: sol işaretçi babaya, sol kılavuz TRUE (kırmızı).

[9]

Babanın sağ kılavuzuna FALSE: 2. durumda gereksiz ama zararsız; 1. durumda gerekli.

[10–12]

Çocuğun sağ kılavuzu FALSE ise (2. durum) ardılın sol kılavuzu çocuğa çevrilir; 1. durumda yalnızca bir test yapılıp dönülür.

Büyük/küçük harf

C büyük/küçük harfe duyarlıdır: baba->SagCocuk, baba->SagKilavuz (büyük S) derlenmez. Denetlenen koşul babanın sağ çocuğunun olup olmadığıdır.

Düğüm Ekleme

NULL mı kontrolü nerede yapılmalı?

Hocadan “saga_ekle yordamı, müstakil olarak kullanılmayıp, başka bir yordam içinde kullanılacağı için NULL mı kontrolünü burada yapmaya gerek yoktur. Bu kontrolü, saga_ekle yordamını kullanan yordamda yapmak gerekir. Ama saga_ekle yordamını müstakil olarak birisi kullanacaksa o zaman bunu da kontrol etmeliyiz.”
  • Örnek: kılavuzlu arama ağacına eklerken sağa mı sola mı ekleneceği bulununca, tek atama yerine saga_ekle ya da sola_ekle çağrılır; baba zaten NULL değildir.
  • Kodun giriş kısmında denetim yapılmıyorsa bu açıklamada belirtilmelidir: “baba ve cocuk NULL olmamalı”.
  • Genel kütüphane yordamında savunmacı denetim (assert ya da hata dönüşü) eklenir.

Düğüm Ekleme

Açıklama satırı ne söylemeli?

/* KÖTÜ: kodu tekrarlıyor */
if (!cocuk->sagKilavuz)  // sağ kılavuz FALSE mu
/* İYİ: niyeti söylüyor */
if (!cocuk->sagKilavuz)  // babanın alt ağacı var mı
  • Açıklama, kodun ne yapmak istediğini söylemeli; kodun kendisini tekrarlamamalı.
  • Böylece okuyan “yapmak istediği doğru, ama kodu yanlış yazmış” diyebilir: hata ayıklama kolaylaşır.
Hocadan “Buraya “babanın sağ çocuğu FALSE mu” diye sakın yazılmasın! Sonradan gelen okuyucu “Ben onu zaten biliyorum.” demesin. Açıklama satırları, kodda söylenenden farklı şeyler de söylesin.”

Düğüm Ekleme

Tek yordama indirmek ve yazılım mühendisliği

Hocadan “Her duruma ayrı ayrı yordam yazsak, işin tadı kaçacak; cocukYok_sagaEkle, cocukVar_sagaEkle, cocukYok_solaEkle, cocukVar_solaEkle. Karmaşıklığı artırmadan bunları tek yordama indirebilirsek, iyi! … “İç içe if… if… deriz, tüm durumları tek yordamda yazabiliriz.” Ondan kaçınılmalı. Öyle olacağına ayrı ayrı yordamlar olsun.”
  • Önce 3–5 farklı durum analiz edilir; sonra yordam adı, dönüş değeri, parametreler belirlenir.
  • Ortak işler ortak biçimde tanımlanır (burada: kılavuz bayrağını babadan kopyalamak).
  • Sonuç: kod bölük pörçük değil; sade, okunabilir, bakımı kolay.
Hocadan “Yazılım mühendisliği kavramlarını keşke 1. sınıfta öğretseler, ama çok erken olur. … Bu bölüm bana göre 6 yıl olmalı.”

Düğüm Ekleme

SORU: sola ekleme yordamını düşününüz

Çocukla işaret edilen öğeyi, babanın sol çocuğu olacak biçimde kılavuzlu ağaca ekleyen sola_ekle yordamını yazınız.

  • Sol ↔ sağ, öncel ↔ ardıl yer değiştirir: babanın eski önceli, çocuğun önceli olur; onun sağ kılavuzu çocuğa çevrilir.
  • Kök ortada öncel bulma: sol alt ağaç varsa onun en sağı, yoksa sol kılavuz.

Düğüm Ekleme

Çözüm: sola_ekle ve kokortada_oncel_bul

kilavuz_gost kokortada_oncel_bul(kilavuz_gost L) {
  kilavuz_gost p = L->solCocuk;
  if (!L->solKilavuz)
    while (!p->sagKilavuz) p = p->sagCocuk;
  return p;
}
void sola_ekle(kilavuz_gost baba, kilavuz_gost cocuk) {
  cocuk->solCocuk   = baba->solCocuk;
  cocuk->solKilavuz = baba->solKilavuz;
  cocuk->sagCocuk   = baba;
  cocuk->sagKilavuz = TRUE;      // ardılı baba
  baba->solCocuk    = cocuk;
  baba->solKilavuz  = FALSE;
  if (!cocuk->solKilavuz) {      // baba'nın sol alt ağacı vardı
    kilavuz_gost p = kokortada_oncel_bul(cocuk);
    p->sagCocuk = cocuk;         // öncelin ardılı çocuk
  }
}

Düğüm Ekleme

Kod yazarken teyakkuz!

Hocadan “Kod yazarken teyakkuz! Her an dinamite basılabilir. “Bunu yapıyorum, ama doğru mu yapıyorum?””
Hocadan “Bu satırdan sonra kodun doğru çalıştığına hemen karar verilmemeli. Daha işletilecek bir if bloğu var.”

Kılavuzlu ağaç

  • Yığınsız O(1) bellekli gezinme
  • Her düğümden ardıl/öncel O(h)'de, gezinmede amortize O(1)
  • NULL alanlar boşa gitmez

Bedeli

  • Her düğümde 2 bayrak
  • Ekleme/silme daha girift
  • Her yordam kılavuzları gözetmeli

Özet

Karmaşıklık özeti

işlemzamanek belleknot
kokortada_ardil_bulO(h) en kötüO(1)kılavuz varsa O(1)
kilavuz_kokortada_dolasO(n)O(1)yığın / özyineleme yok
kök önde kılavuzlu gezinmeO(n)O(1)aynı kılavuzlarla
saga_ekle / sola_ekleO(h)O(1)ardıl/öncel araması
Morris gezinmesi (bayraksız)O(n)O(1)geçici kılavuzlar
özyinelemeli kök ortada (karşılaştırma)O(n)O(h)çağrı yığını

Özet

Sık yapılan hatalar

1Kılavuz bayrağına bakmadan solCocuk/sagCocuk'u gerçek çocuk sanmak (sonsuz döngü).
2Başlık düğümünü unutmak: H'nin sol ve G'nin sağ kılavuzu havada kalır.
3Sağa eklemede 2. durumda ardılın sol kılavuzunu güncellememek.
4cocuk->sagKilavuz'u sabit TRUE yazıp 2. durumu bozmak.
5Boş ağaçta başlığın solKilavuz = TRUE olduğunu unutup kökü aramak.
6Büyük/küçük harf: SagCocuk ≠ sagCocuk (C büyük/küçük harfe duyarlıdır).

Özet

Alıştırmalar

  1. A(B(D, E), C(–, F)) ağacının kılavuzlarını çiziniz; kaç kılavuz vardır?
  2. Kök ortada öncel bulan kokortada_oncel_bul yordamını yazınız.
  3. Kılavuzlu ağacı kök ortada tersten (G C F A …) gezinen yordamı yazınız.
  4. Boş ağaca (yalnız başlık) ilk düğümü sola_ekle(baslik, x) ile eklemek doğru mu?
  5. Kök sonda gezinme kılavuzlarla neden zordur? Hangi ek bilgi işi kolaylaştırır?
  6. Kılavuzlu ikili arama ağacına ekleme yordamını saga_ekle/sola_ekle kullanarak yazınız.
İpuçları / cevaplar
  1. n = 6 → n + 1 = 7 kılavuz (2'si başlığa).
  2. Ardıl bulmanın aynası: sol kılavuz TRUE ise doğrudan; değilse sol çocuktan sağa sağa (Sola ekleme çözümünde).
  3. Başlıktan başlanır; kokortada_oncel_bul ile başlığa dönene dek ilerlenir.
  4. Evet: başlığın sol kılavuzu TRUE olduğu için çocuk iki kılavuzuyla başlığı gösterir, başlığın solKilavuz'u FALSE olur.
  5. Bir düğümün kök sonda ardılı babaya bağlıdır; baba işaretçisi eklemek işi kolaylaştırır.
  6. Arama: kılavuz bayrağı TRUE olan yere gelince durulur; x küçükse sola_ekle, büyükse saga_ekle.

Özet

Bölüm özeti

n + 1 NULL

2n işaretçinin n−1'i dolu; kalanlar kılavuz olur.

Kurallar

Sol NULL → öncel, sağ NULL → ardıl; bayraklar ayırt eder; başlık asılı kılavuzları toplar.

Ardıl

Sağ kılavuz TRUE ise doğrudan; değilse sağ alt ağacın en solu.

Gezinme

Yığınsız O(n) zaman, O(1) bellek; kök önde de mümkün.

Ekleme

saga_ekle: kılavuz bayrağı babadan kopyalanır; 2. durumda ardılın önceli çocuk.

Kod kalitesi

Açık tanım, anlamlı açıklama, NULL denetiminin yeri, tek yordam.

Özet

Kaynaklar ve devam

  • Horowitz–Sahni–Anderson-Freed, Fundamentals of Data Structures in C, 5.5 Threaded Binary Trees.
  • A. J. Perlis, C. Thornton, “Symbol manipulation by threaded lists”, CACM 3(4), 1960.
  • J. M. Morris, “Traversing binary trees simply and cheaply”, Information Processing Letters 9(5), 1979; Knuth, TAOCP 1, 2.3.1.

Son

Bölüm 12: Kılavuzlu İkili Ağaçlar
Sonraki bölüm: Bölüm 13: Öncelikli Kuyruk ve Heap · Ders sayfası
1 / 1 C ile Veri Yapıları