Bölüm 10: Ağaçlar

C ile Veri Yapıları ve Algoritmalar
Terminolojiİkili ağaçDizi / bağlı gösterimÖzyinelemeli gezinmelerİteratif gezinmelerSeviye seviye, kopyala, eşit mi?
Kaynak: Horowitz–Sahni–Anderson-Freed, Bölüm 5

Giriş

Öğrenme Çıktıları

Terminolojiyi kullanmak

Kök, ebeveyn, çocuk, kardeş, ata, soy, derece, seviye, derinlik ve yaprak kavramlarını bir ağaç üzerinde göstermek.

İkili ağacı çözümlemek

Lemmayı (2i−1, 2k−1) kanıtlamak; tam dolu, tam ve tam olmayan ağaçları ayırmak.

İki gösterimi kurmak

Diziyle (ebeveyn = ⌊i/2⌋) ve bağlı yapıyla (agac_gost) gösterim; bellek maliyetini karşılaştırmak.

Gezinme

Kök başta / ortada / sonda gezinmeleri özyinelemeli ve yığınla döngüsel yazmak; seviye seviye gezmek.

Ağaç yordamları yazmak

Uç düğüm yazma, kopyalama, eşbiçimlilik ve sol–sağ değiş tokuşu özyinelemeli kurmak.

Maliyeti hesaplamak

Her yordamın zaman O(n) ve bellek O(h) maliyetini gerekçelendirmek.

Terminoloji

Ağaç (tree): tek ebeveynli hiyerarşi

  • Arama, sıralama, veri sıkıştırma, söz dizim çözümleme (syntax analysis), kod optimizasyonu, derleyici ara süreçleri…
  • Günlük hayatta: soy ağacı, bir kurumun yönetim şeması, dosya sistemi.
  • Ağaç, çizgenin (graph) bir alt kümesidir: ağaçta her düğümün tek bir ebeveyni olur; çizgede birden çok olabilir.
  • Her alt ağaç yine bir ağaçtır → ağaç yordamları doğal olarak özyinelemelidir.
ağaçABCDEçizge (ağaç değil)ABCDD'nin iki ebeveyni var

Terminoloji

Terminoloji: bir örnek ağaç üzerinde

seviye 1seviye 2seviye 3seviye 4A3B2EKLF0CGD3HMIJ
Ebeveyn / çocuk / kardeş

B; E ve F'nin ebeveyni, E ve F B'nin çocukları. Aynı ebeveynin çocukları kardeştir (sibling).

Derece (degree)

Bir düğümün alt ağaç sayısı: A'nın derecesi 3, F'nin 0 (sağ üstteki küçük sayılar).

Yaprak / uç düğüm

Derecesi 0 olan düğüm (leaf, terminal): K, L, F, G, M, I, J.

Terminoloji

Terminoloji adım adım

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJKök (root): ebeveyni olmayan tek düğüm → A

Kök A'dır. Seviyeler kökten başlayarak numaralanır: bu derste kök 1. seviyededir.

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJB, E ve F'nin ebeveyni; E ve F kardeş

Alt ağaçları olan düğüm, alt ağaçlarının köklerinin ebeveynidir (parent); onlar da onun çocuklarıdır (child).

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJB, C, D kardeştir (ebeveynleri A)

Aynı ebeveynin çocukları kardeş düğümlerdir. Terminoloji büyükbaba–torun diye diğer kuşaklara genişler.

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJM'nin ataları: A, D, H (kökten M'ye giden yol)

Bir düğümün ataları (ancestor) kökten o düğüme kadar olan yol (path) üzerindeki tüm düğümlerdir. D, M'nin büyükbabasıdır.

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJB'nin soyu: E, F, K, L

Bir düğümün alt ağacındaki tüm düğümler onun soyudur (descendant). Çocukların çocukları torundur: E, F, G, H, I, J A'nın torunlarıdır.

seviye 1seviye 2seviye 3seviye 4ABEKLFCGDHMIJYapraklar (derece 0): K, L, F, G, M, I, J

Derecesi 0 olan düğümlere yaprak ya da uç düğüm denir.

seviye 1seviye 2seviye 3seviye 4A3BEKLFCGD3HMIJAğacın derecesi = 3 · derinliği = 4 seviye

Ağacın derecesi düğüm derecelerinin en büyüğü (3), derinliği ise en büyük seviye numarasıdır (4).

Derece ile derinlik ayrı kavramlardır; ikisini “derecelerin maksimumu” diye aynı tanımla vermek sık yapılan bir hatadır.

Terminoloji

Derinlik, yükseklik: kitaplar farklı sayar

  • Bu ders (Horowitz–Sahni): kök 1. seviye; ağacın derinliği = en büyük seviye = düğüm sayısıyla ölçülen en uzun yol. Örnek ağaçta 4.
  • CLRS / Sedgewick: kök derinlik 0; yükseklik = kökten en uzak yaprağa kenar sayısı. Aynı ağaçta 3.
  • Sınavda hangi tanımın kullanıldığı belirtilmeli; formüller buna göre 1 kayar.
  • Bu bölümde derinlik(NULL) = 0, tek düğümlü ağacın derinliği 1.
kavrambu dersCLRS
kökün seviyesi10
M'nin seviyesi43
ağacın derinliği4yükseklik 3
boş ağaç0−1

İkili Ağaç

İkili ağaç (binary tree)

  • Her düğümün yalnızca sol alt ağacı ve/veya sağ alt ağacı vardır ya da hiç çocuğu yoktur.
  • Düğüm dereceleri 0, 1 ya da 2 olabilir.
  • Sol ile sağ ayrıdır: tek çocuklu bir düğümde çocuğun solda mı sağda mı olduğu önemlidir.
  • Kümeler: ikili ağaç ⊂ ağaç ⊂ çizge.
ABCDABDHIECFJGKsola çarpık4 seviyeli bir ikili ağaç

İkili Ağaç

Lemma: bir seviyede ve bir ağaçta en çok kaç düğüm?

seviye 1: 2^0 = 1seviye 2: —seviye 3: —seviye 4: —toplam = 2^1 − 1 = 1

i) i. seviyedeki en fazla düğüm sayısı 2i−1 (i ≥ 1). Taban: i = 1 için 20 = 1 (yalnız kök).

seviye 1: 2^0 = 1seviye 2: 2^1 = 2seviye 3: —seviye 4: —toplam = 2^2 − 1 = 3

Tümevarım: (n−1). seviyede 2n−2 düğüm varsa, her birinin en çok 2 çocuğu olduğundan n. seviyede 2·2n−2 = 2n−1 düğüm olur.

seviye 1: 2^0 = 1seviye 2: 2^1 = 2seviye 3: 2^2 = 4seviye 4: —toplam = 2^3 − 1 = 7

ii) k derinliğinde bir ikili ağaçta en fazla 2k − 1 düğüm: Σi=1..k 2i−1 = 20 + 21 + … + 2k−1.

seviye 1: 2^0 = 1seviye 2: 2^1 = 2seviye 3: 2^2 = 4seviye 4: 2^3 = 8toplam = 2^4 − 1 = 15

Geometrik toplam 2k − 1 verir. k = 4 → en çok 15 düğüm. Lemma: “doğruluğu hemen görülebilen, teoremin yumuşatılmış hâli”.

İkili Ağaç

Tam dolu (full) ve tam (complete) ikili ağaç

1234567891012345678911tam dolu (full): 2^4 − 1 = 15tam (complete): 1..10 kesintisiztam değil: 10 numaralı düğüm yok!
TANIM: Tam dolu

Derinliği k olan ve 2k − 1 düğümlü ikili ağaç: tüm seviyeler tam dolu.

TANIM: Tam

n düğümlü; ebeveyne i, sol çocuğa 2i, sağ çocuğa 2i+1 verildiğinde 1'den n'e atlamasız numaralanabilen ağaç.

Neden önemli?

Tam ağaç diziye boşluksuz sığar ve derinliği ⌈lg(n+1)⌉ ile en küçüktür.

İkili Ağaç

Derinlik ve düğüm sayısı ilişkileri

  • k derinlikli ikili ağaçta: k ≤ n ≤ 2k − 1; dolayısıyla ⌈lg(n+1)⌉ ≤ k ≤ n.
  • Tam (complete) ağaçta derinlik en küçüktür: k = ⌈lg(n+1)⌉ = ⌊lg n⌋ + 1.
  • n0 = n2 + 1: yaprak sayısı, iki çocuklu düğüm sayısından bir fazladır. Kanıt: n = n0+n1+n2 ve kenar sayısı n−1 = n1+2n2.
  • 2n işaretçi alanından n−1'i dolu → n+1 NULL işaretçi (kılavuzlu ikili ağaçların çıkış noktası).
ABDHIECFJGKn = 11: n0 = 5 yaprak, n1 = 2, n2 = 4 → 5 = 4 + 1

Kaynak: Horowitz–Sahni–Anderson-Freed, Lemma 5.2–5.3; CLRS B.5.

Gösterim

İkili ağacın dizi ile gösterimi

  • n düğümlü tam ikili ağaç bir diziye yerleştirilebilir.
  • Formüller gereği 0 değil 1 numaralı indisten başlanır!
  • baba(i) = ⌊i/2⌋ (i = 1 ise kök, ebeveyni yok)
  • solcocuk(i) = 2i (2i > n ise sol çocuk yok)
  • sagcocuk(i) = 2i+1 (2i+1 > n ise sağ çocuk yok)
  • Derinlik k ise 2k − 1 gözlük dizi açılmalıdır.
#define MAX_DUGUM 64   /* 2^k - 1 */
char agac[MAX_DUGUM + 1];  /* [0] kullanılmaz */
int  n;                    /* düğüm sayısı */

int baba(int i)     { return i / 2; }
int solcocuk(int i) { return 2 * i; }
int sagcocuk(int i) { return 2 * i + 1; }
/* çocuk var mı?  solcocuk(i) <= n */

Gösterim

Dizi gösteriminde bellek: tam ağaç ve çarpık ağaç

ABCDEFGHI1A2B3C4D5E6F7G8H9I9 göz açıldı, 9'u da dolu: kayıp yokbaba(9) = ⌊9/2⌋ = 4 → D; sol(4) = 8 → H

Tam ağaç: dizi gözlerinin neredeyse tamamı dolar; bellek kaybı ya hiç olmaz ya da çok az olur.

A1B2C4D8E161A2B3·4C5·6·7·8D9·…15·16E17·…31·derinlik 5 → 2^5 − 1 = 31 göz; yalnızca 5'i kullanılıyor“Dizi değil o! O bir ağaç!”

Ağaç tam olmaktan uzaklaştıkça âtıl bellek artar. Sola çarpık (skewed) 5 derinlikli ağaç 31 göz ister, 5'ini kullanır.

Hocadan “Depolama ve erişim, bilgisayar bilimlerinin iki ana konusudur. … Eğer ağacımız tam ikili ağaçsa ve ekleme/çıkarma yapılmayacaksa, ağaç yapısını dizi ile gerçeklemek uygun olur.”

Gösterim

İkili ağacın bağlı (işaretçili) gösterimi

typedef struct agac *agac_gost;
struct agac {
    agac_gost solcocuk;   /* sol alt ağaç */
    char      veri;
    agac_gost sagcocuk;   /* sağ alt ağaç */
};
  • Her düğüm: sol işaretçi, veri, sağ işaretçi.
  • Ekleme / silme yalnızca işaretçi atamasıdır.
  • Boş alt ağaç = NULL (koyu kutu).
wide-left
ABCDEkoyu kutu = NULLsol çocuk | veri | sağ çocuk

Gösterim

Yeni düğüm: ogeal()

agac_gost ogeal(void) {          /* yeni düğüm */
    agac_gost p = malloc(sizeof *p);
    if (p == NULL) {
        fprintf(stderr, "Bellek yok!\n");
        exit(EXIT_FAILURE);
    }
    p->solcocuk = p->sagcocuk = NULL;
    return p;
}

void agac_sil(agac_gost L) {     /* kök sonda */
    if (!L) return;
    agac_sil(L->solcocuk);
    agac_sil(L->sagcocuk);
    free(L);
}
ogeal() burada malloc sonucu denetlenerek ve çocuklar NULL yapılarak verilmiştir; tüm örnekler bunu kullanır.
  • Ağacı bırakırken kök sonda sıra zorunludur: kök önce free edilirse çocuklara giden işaretçiler kaybolur.

Gösterim

Dizi mi, bağlı gösterim mi?

Dizi

  • İşaretçi alanı yok; ebeveyn/çocuğa O(1) indis hesabı.
  • Tam ağaçta bellek kaybı yok.
  • Örn. ağdaki bilgisayarlar sabitse, ileride eklenip çıkarılmayacaksa uygundur.

Bağlı gösterim

  • Her düğüme 2 işaretçi; çarpık ağaçta bile n düğüm yeter.
  • Ekleme / silme işaretçi atamasıyla yapılır.
  • Ebeveyne doğrudan erişim yok (gerekirse ebeveyn alanı eklenir).
Kural: ağaç tam ve sabit ise dizi (ör. heap); şekli değişen ağaç için bağlı gösterim.

Gösterim

Genel ağacı ikili ağaca çevirme: sol çocuk – sağ kardeş

ABEKLFCGDHMIJ

Genel ağaçta düğüm başına çocuk sayısı değişkendir; sabit alanlı bir struct ile tutmak zordur.

ABEKLFCGDHMIJturuncu: ilk çocuk (sol) · kesikli: kardeş (sağ)

Her düğüm için yalnızca ilk çocuğa bağ kalır; kardeşler kendi aralarında sağ kardeş bağıyla zincirlenir.

ABEKLFCGDHMIJ

İlk çocuk = sol, sağ kardeş = sağ işaretçi: sonuç bir ikili ağaçtır. Kökün sağ bağı hep NULL'dur.

struct gdugum {
    char veri;
    struct gdugum *ilk_cocuk;  /* = sol */
    struct gdugum *kardes;     /* = sağ */
};
  • İkili ağacın kök başta sırası = genel ağacın kök başta sırası.
  • İkili ağacın kök ortada sırası = genel ağacın kök sonda sırası. (Horowitz 5.1.2; Knuth 2.3.2)

Gezinmeler

İkili ağaç gezinmeleri (traversals)

  • Amaç: her düğümü yalnızca bir kez ziyaret ederek tüm ağacı gezmek.
  • Bir düğümde üç şey var: veri (V), sol alt ağaç (L), sağ alt ağaç (R) → 3! = 6 gezinme sırası.
  • Solun sağdan önce gezildiği üçü standarttır: inorder, preorder, postorder.
  • Uygulama isterse diğer üçü de (sağdan sola) kullanılabilir.
sıraad
1Sol – Veri – Sağkök ortada (inorder)
2Sol – Sağ – Verikök sonda (postorder)
3Veri – Sol – Sağkök başta (preorder)
4Sağ – Veri – Solters kök ortada
5Sağ – Sol – Veriters kök sonda
6Veri – Sağ – Solters kök başta

Gezinmeler

Aritmetik ifade ağacı ve üç gösterim

+**/ABCDE
  • Kök ortada → ara-işleç (infix): ((((A/B)*C)*D)+E)
  • Kök başta → ön-işleç (prefix): +**/ABCDE
  • Kök sonda → son-işleç (postfix): AB/C*D*E+
  • İç düğümler işleç (turuncu), yapraklar işlenen. Parantezler ağacın şeklinde saklıdır; infix yazarken geri eklenir.
  • Derleyiciler ifadeleri böyle bir ağaca çevirir; postfix, yığınla değerlendirilir.

Gezinmeler

Aynı ağaç, üç gezinme

A1B2C3D4G5H6E7I8J9K10F11L12kök baştaABCDGHEIJKFLküçük sayılar: ziyaret sırası

Kök başta (V-L-R): önce kök, sonra sol alt ağacın tamamı, en son sağ alt ağaç → ABCDGHEIJKFL

A10B1C5D3G2H4E6I8J7K9F12L11kök ortadaBGDHCEJIKALFküçük sayılar: ziyaret sırası

Kök ortada (L-V-R): B'nin solu boş olduğundan ilk B yazılır → BGDHCEJIKALF

A12B9C8D3G1H2E7I6J4K5F11L10kök sondaGHDJKIECBLFAküçük sayılar: ziyaret sırası

Kök sonda (L-R-V): kök hep en sonda; A son harf → GHDJKIECBLFA

Gezinmeler

Gezinme yordamları — özyinelemeli

void kok_onde_dolas(agac_gost L) {
  if (L) {
    printf("%c", L->veri);
    kok_onde_dolas(L->solcocuk);
    kok_onde_dolas(L->sagcocuk);
  }
}
void kok_ortada_dolas(agac_gost L) {
  if (L) {
    kok_ortada_dolas(L->solcocuk);
    printf("%c", L->veri);
    kok_ortada_dolas(L->sagcocuk);
  }
}
void kok_sonda_dolas(agac_gost L) {
  if (L) {
    kok_sonda_dolas(L->solcocuk);
    kok_sonda_dolas(L->sagcocuk);
    printf("%c", L->veri);
  }
}
Hocadan “NOT: Ağaç işaretçisi NULL gelebilir, buna hiç dikkat edilmiyor!”

Üç yordam aynı iskelettir; yalnızca printf satırının yeri değişir. if (L) hem boş ağacı hem özyinelemenin durma koşulunu karşılar.

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

Kök ortada gezinme adım adım (çağrı yığınıyla)

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sol alt ağaç için kok_ortada_dolas(B) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sol alt ağaç için kok_ortada_dolas(D) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sol alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya D eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya B eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya A eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sağ alt ağaç için kok_ortada_dolas(C) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sol alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya C eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

Tüm çağrılar döndü. Çıktı: DBAC. Her düğüme bir kez girildi, bir kez çıkıldı: 2n adım.

void kok_ortada_dolas(agac_gost L) {
  if (L) {
    kok_ortada_dolas(L->solcocuk);
    printf("%c", L->veri);
    kok_ortada_dolas(L->sagcocuk);
  }
}

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

Kök başta gezinme adım adım

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: kök başta: önce kendini yazdırır → çıktıya A eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sol alt ağaç için kok_onde_dolas(B) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: kök başta: önce kendini yazdırır → çıktıya B eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sol alt ağaç için kok_onde_dolas(D) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: kök başta: önce kendini yazdırır → çıktıya D eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sol alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sağ alt ağaç için kok_onde_dolas(C) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: kök başta: önce kendini yazdırır → çıktıya C eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sol alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

Tüm çağrılar döndü. Çıktı: ABDC. Her düğüme bir kez girildi, bir kez çıkıldı: 2n adım.

void kok_onde_dolas(agac_gost L) {
  if (L) {
    printf("%c", L->veri);
    kok_onde_dolas(L->solcocuk);
    kok_onde_dolas(L->sagcocuk);
  }
}

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

Kök sonda gezinme adım adım

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sol alt ağaç için kok_sonda_dolas(B) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sol alt ağaç için kok_sonda_dolas(D) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sol alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = D: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya D eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = B: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya B eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: sağ alt ağaç için kok_sonda_dolas(C) çağrılır; bu çerçeve bekler.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sol alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.

ABDCNULLşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = C: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya C eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

L = A: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya A eklenir.

ABDCşimdi çalışançağrı yığınındayazdırıldıhenüz yazılmadı

Tüm çağrılar döndü. Çıktı: DBCA. Her düğüme bir kez girildi, bir kez çıkıldı: 2n adım.

void kok_sonda_dolas(agac_gost L) {
  if (L) {
    kok_sonda_dolas(L->solcocuk);
    kok_sonda_dolas(L->sagcocuk);
    printf("%c", L->veri);
  }
}

Gezinmeler

Gezinmenin maliyeti ve NULL çağrıları

  • Zaman: her düğüme bir girip bir çıkılır → 2n = O(n), değişmez.
  • Bellek: çağrı yığını en fazla derinlik (h) kadar çerçeve tutar → O(h). Dengeli ağaçta O(lg n); çarpık ağaçta O(n).
  • n düğümlü ağaçta n+1 NULL çağrısı yapılır. Çağrıdan önce denetlenirse bunlar kalkar:
/* önce: n+1 NULL çağrısı */
kok_onde_dolas(L->solcocuk);
kok_onde_dolas(L->sagcocuk);
/* sonra: çağırmadan denetle */
if (L->solcocuk) kok_onde_dolas(L->solcocuk);
if (L->sagcocuk) kok_onde_dolas(L->sagcocuk);

İlk çağrıdaki if (L) yine kalmalı: ağaç boş gelebilir. Bellek için O(lg n) sınırı yalnız dengeli ağaçta geçerlidir; genel sınır O(h).

Hocadan “‘Hocam, özyinelemeli algoritmalar çok bellek yer’ tarzındaki iddialar, özyinelemeli kod yazmamak için kaçamaktır.”
Hocadan “Algoritmayı yazdıktan sonra peşini bırakmıyoruz. Bilgisayar bilimlerinde algoritmanın analizini yapmak zorundayız.”

Gezinmeler

Özyinelemeli kod bizzat yazılarak öğrenilir

Hocadan “Geçmişteki tüm sorularımı internete koysam, bu yanıltıcı olur. Naim Süleymanoğlu’nu halter kaldırırken 100 kere seyretsem, yine de ben o halteri kaldıramam. ‘Bu nasıl yazıldı’ sorusuna yoğunlaşmak gerekir.”
Hocadan “Bu algoritma hâlâ anlaşılmadıysa, ağaç şeklindeki stack frame’i çizmek anlamaya yardımcı olacaktır.”
Duyarsam unuturum, görürsem hatırlarım, yaparsam öğrenirim.

— Çin atasözü

Çağrı ağacı

Her çağrı bir düğüm, her alt çağrı bir çocuk: gezinmenin çağrı ağacı, NULL çağrıları yaprak olmak üzere ağacın kendisidir (n iç + n+1 yaprak).

Gezinmeler

İfade ağacını hesaplamak: kök sonda gezinme

typedef struct ifade *ifade_gost;
struct ifade {
    char      islec;   /* '+', '-', '*', '/' */
    int       deger;   /* yalnız yapraklarda */
    ifade_gost sol, sag;
};
  • Bir işlecin değeri, iki alt ifadenin değerleri bilinmeden hesaplanamaz → önce sol, sonra sağ, en son kök: kök sonda.
  • Her iç düğümün tam iki çocuğu vardır (ikili işleçler); yaprakta işleç yok, sayı var.
  • Maliyet O(n) zaman, O(h) yığın. Tekli eksi (−x) için tek çocuklu düğüm gerekir.

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

hesapla(): (8 / 4) * 3 + 5 = 11

+*/8435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

+ bir işleç: önce sol alt ifade hesaplanır.

+*/8435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

* bir işleç: önce sol alt ifade hesaplanır.

+*/8435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

/ bir işleç: önce sol alt ifade hesaplanır.

+*/8=8435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

8 bir yaprak: sayıyı döndürür → 8.

+*/8=8435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

sol değer a = 8. Şimdi sağ alt ifade hesaplanır.

+*/8=84=435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

4 bir yaprak: sayıyı döndürür → 4.

+*/=28=84=435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

b = 4; 8 / 4 = 2 döndürülür (kök sonda sırası: sol, sağ, kök).

+*/=28=84=435(8 / 4) * 3 + 5şimdi çalışan=değer döndü

sol değer a = 2. Şimdi sağ alt ifade hesaplanır.

+*/=28=84=43=35(8 / 4) * 3 + 5şimdi çalışan=değer döndü

3 bir yaprak: sayıyı döndürür → 3.

+*=6/=28=84=43=35(8 / 4) * 3 + 5şimdi çalışan=değer döndü

b = 3; 2 * 3 = 6 döndürülür (kök sonda sırası: sol, sağ, kök).

+*=6/=28=84=43=35(8 / 4) * 3 + 5şimdi çalışan=değer döndü

sol değer a = 6. Şimdi sağ alt ifade hesaplanır.

+*=6/=28=84=43=35=5(8 / 4) * 3 + 5şimdi çalışan=değer döndü

5 bir yaprak: sayıyı döndürür → 5.

+=11*=6/=28=84=43=35=5(8 / 4) * 3 + 5şimdi çalışan=değer döndü

b = 5; 6 + 5 = 11 döndürülür (kök sonda sırası: sol, sağ, kök).

int hesapla(ifade_gost t) {
  if (!t->sol && !t->sag)        /* yaprak: sayı */
    return t->deger;
  int a = hesapla(t->sol);
  int b = hesapla(t->sag);
  switch (t->islec) {
    case '+': return a + b;
    case '-': return a - b;
    case '*': return a * b;
    default:  return a / b;
  }
}

İteratif Gezinme

İteratif gezinme: yığın nereden çıktı?

  • Her özyinelemeli algoritma döngüsel hâle getirilebilir. İki yol: (1) davranışı döngüyle ifade etmek, (2) işletim sisteminin yaptığı gibi dönüş adreslerini bir yığına atmak.
  • Kök ortada: kök basılmadan önce sol alt ağaç basılmalı → son eklenene önce erişilmek istenir: LIFO.
  • Alan karmaşıklığı artmasın diye yığına öğelerin kopyası değil adresleri konur.
  • Yığında aynı anda en fazla sol derinlik kadar öğe bulunur.
Hocadan “Öyleyse düğümleri bir sıraya koyduğumuzda son eklediğimize önce erişmek isteyeceğiz. Ya hu bu bir stack yapısı! Ne kadar estetik anlattık!”

İteratif Gezinme

Özyinelemesiz kök ortada gezinme

void kok_ortada_dolas(agac_gost L) {
  agac_gost yigit[MAX_SIZE];
  int ust = -1;
  for (;;) {
    for (; L; L = L->solcocuk)  // solları at
      yigit_ekle(&ust, L, yigit);
    L = yigit_al(&ust, yigit);  // boşsa NULL
    if (!L) break;
    printf("%c", L->veri);
    L = L->sagcocuk;
  }
}
void yigit_ekle(int *ust, agac_gost p,
                agac_gost y[]) {
  if (*ust == MAX_SIZE - 1) {
    fprintf(stderr, "Yigit dolu\n");
    exit(EXIT_FAILURE);
  }
  y[++*ust] = p;
}
agac_gost yigit_al(int *ust,
                    agac_gost y[]) {
  return (*ust == -1) ? NULL : y[(*ust)--];
}

Yığın boşsa yigit_al NULL döndürür; döngünün tek çıkışı budur.

İteratif Gezinme · Kodu adım adım çalıştır

Yığınla kök ortada: A(B(D,E),C)

ABDECLyığın0A123◀ ust=0

A yığına eklenir (adresi!), sonra sola: L = A->solcocuk = B.

ABDECLyığın0A1B23◀ ust=1

B yığına eklenir (adresi!), sonra sola: L = B->solcocuk = D.

ABDECLyığın0A1B2D3◀ ust=2

D yığına eklenir (adresi!), sonra sola: L = D->solcocuk = NULL. L NULL → iç döngü biter.

ABDECLyığın0A1B23◀ ust=1

Tepeden D alınır; solu bitmiş demektir → D yazılır.

ABDECyığın0A1B23◀ ust=1L = NULL

L = D->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.

ABDECLyığın0A123◀ ust=0

Tepeden B alınır; solu bitmiş demektir → B yazılır.

ABDECLyığın0A123◀ ust=0

L = B->sagcocuk = E: sağ alt ağaç aynı biçimde gezilecek.

ABDECLyığın0A1E23◀ ust=1

E yığına eklenir (adresi!), sonra sola: L = E->solcocuk = NULL. L NULL → iç döngü biter.

ABDECLyığın0A123◀ ust=0

Tepeden E alınır; solu bitmiş demektir → E yazılır.

ABDECyığın0A123◀ ust=0L = NULL

L = E->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.

ABDECLyığın0123ust=−1 (boş)

Tepeden A alınır; solu bitmiş demektir → A yazılır.

ABDECLyığın0123ust=−1 (boş)

L = A->sagcocuk = C: sağ alt ağaç aynı biçimde gezilecek.

ABDECLyığın0C123◀ ust=0

C yığına eklenir (adresi!), sonra sola: L = C->solcocuk = NULL. L NULL → iç döngü biter.

ABDECLyığın0123ust=−1 (boş)

Tepeden C alınır; solu bitmiş demektir → C yazılır.

ABDECyığın0123ust=−1 (boş)L = NULL

L = C->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.

ABDECyığın0123ust=−1 (boş)L = NULL

Yığın boş: yigit_al NULL döndürür → break. Gezinme bitti.

void kok_ortada_dolas(agac_gost L) {
  agac_gost yigit[MAX_SIZE];
  int ust = -1;
  for (;;) {
    for (; L; L = L->solcocuk)  // solları at
      yigit_ekle(&ust, L, yigit);
    L = yigit_al(&ust, yigit);  // boşsa NULL
    if (!L) break;
    printf("%c", L->veri);
    L = L->sagcocuk;
  }
}

İteratif Gezinme

Maliyet ve seçici yazdırma

  • Her düğüm yığına bir kez eklenir, bir kez alınır: 2n adım → O(n) zaman.
  • Bellek: yığın en kötü (çarpık) durumda O(h); sağa çarpık ağaçta yığında hep ≤ 1 öğe → O(1).
  • 9. satıra (printf) bir koşul konursa yalnızca seçilen düğümleri yazan yordam elde edilir: uç düğümler, tek çocuklular, sol çocuklular…
  • if(!(L->sagcocuk)||!(L->solcocuk)) koşulu en çok bir çocuklu düğümleri yazar; yalnız yapraklar için && gerekir.
Hocadan “Bizim bu dersteki amacımız, büyük otomasyon sistemleri vs. geliştirmek değil, küçük modülleri ustaca yazabilmektir. Küçük kodlar defalarca farklı şekillerde yazılmalı.”
“Fools ignore complexity. Pragmatists suffer it. Some can avoid it. Geniuses remove it.”

— Alan J. Perlis

İteratif Gezinme

Sorular: gezinme kalıbını değiştirerek yaz

  1. Kök başta gezinme yordamını döngüsel yazınız.
  2. Kök sonda gezinme yordamını döngüsel yazınız. (Bu daha zordur.)
  3. Uç düğümleri soldan sağa yazan özyinelemeli yordam.
  4. Uç düğümleri sağdan sola yazan özyinelemeli yordam.
  5. Uç düğümleri soldan sağa yazan döngüsel yordam.
  6. Uç düğümleri sağdan sola yazan döngüsel yordam.
  7. Yalnız sol çocuğu olan düğümleri yazan özyinelemeli yordam.
  8. Yalnız sağ çocuğu olan düğümleri yazan özyinelemeli yordam.
  9. Her iki çocuğu da olan düğümleri yazan özyinelemeli yordam.
Hepsi iki kalıptan birine indirgenir: özyinelemeli gezinme + koşullu printf ya da yığınlı gezinme + koşullu printf. “Sağdan sola” için solcocuk ile sagcocuk yer değiştirir.

İteratif Gezinme

SORU 1: döngüsel kök başta

void kok_onde_dolas_d(agac_gost L) {
  agac_gost yigit[MAX_SIZE];
  int ust = -1;
  for (;;) {
    for (; L; L = L->solcocuk) {
      printf("%c", L->veri);   // girerken yaz
      yigit_ekle(&ust, L, yigit);
    }
    L = yigit_al(&ust, yigit);
    if (!L) break;
    L = L->sagcocuk;
  }
}
  • İteratif kök ortada kodunun aynısı; yalnızca printf düğüm yığına girerken yapılır.
  • Böylece kök, sol alt ağacından önce yazılır: V-L-R.
  • Alternatif: kök yığına atılır; her adımda alınır, yazılır, önce sağ sonra sol çocuk atılır.
İkinci çözümü göster
if (L) yigit_ekle(&ust, L, yigit);
while ((L = yigit_al(&ust, yigit))) {
  printf("%c", L->veri);
  if (L->sagcocuk)
    yigit_ekle(&ust, L->sagcocuk, yigit);
  if (L->solcocuk)
    yigit_ekle(&ust, L->solcocuk, yigit);
}

İteratif Gezinme

SORU 2: döngüsel kök sonda — neden daha zor?

  • Bir düğüm yığından alınmadan önce iki kez tepeye gelir: sol alt ağaçtan dönünce ve sağ alt ağaçtan dönünce.
  • Hangisinden döndüğümüzü bilmek için son yazılan düğümü (son) tutarız.
  • Tepedeki t'nin sağ çocuğu varsa ve son yazılan o değilse → önce sağa inilir; değilse t yazılır ve çıkarılır.
  • Alternatif: iki yığın (V-R-L sırasını ikinci yığında tersine çevirmek).
Zaman O(n): her düğüm bir kez eklenir, bir kez çıkarılır, en çok iki kez tepeye bakılır. Bellek O(h).

İteratif Gezinme · Kodu adım adım çalıştır

SORU 2 adım adım: A(B(D),C)

ABDCLyığın0A12◀ ust=0

L = A yığına atılır, sola inilir: L = B.

ABDCLyığın0A1B2◀ ust=1

L = B yığına atılır, sola inilir: L = D.

ABDCyığın0A1B2D◀ ust=2

L = D yığına atılır, sola inilir: L = NULL.

ABDCyığın0A1B2◀ ust=1

Tepe D: sağ çocuğu yok → D yazılır, yığından alınır; son = D.

ABDCyığın0A12◀ ust=0

Tepe B: sağ çocuğu yok → B yazılır, yığından alınır; son = B.

ABDCLyığın0A12◀ ust=0

Tepe A: sağ çocuğu C var ve henüz yazılmadı → önce sağ alt ağaç (L = C).

ABDCyığın0A1C2◀ ust=1

L = C yığına atılır, sola inilir: L = NULL.

ABDCyığın0A12◀ ust=0

Tepe C: sağ çocuğu yok → C yazılır, yığından alınır; son = C.

ABDCyığın012ust=−1 (boş)

Tepe A: sağ çocuğu C az önce yazıldı (son) → A yazılır, yığından alınır; son = A.

ABDCyığın012ust=−1 (boş)

L NULL ve yığın boş: döngü biter. Kök en sona kaldı: kök sonda sırası.

void kok_sonda_dolas_d(agac_gost L) {
  agac_gost yigit[MAX_SIZE], son = NULL, t;
  int ust = -1;
  while (L || ust != -1) {
    if (L) {                     // sola in
      yigit_ekle(&ust, L, yigit);
      L = L->solcocuk;
    } else {
      t = yigit[ust];            // tepeye bak
      if (t->sagcocuk && t->sagcocuk != son)
        L = t->sagcocuk;         // önce sağ
      else {
        printf("%c", t->veri);
        son = yigit_al(&ust, yigit);
      }
    }
  }
}

İteratif Gezinme

SORU 3–6: uç düğümleri yazmak

void uc_dugum_yaz(agac_gost L) {
  if (L) {
    uc_dugum_yaz(L->solcocuk);
    if (!L->solcocuk && !L->sagcocuk)
      printf("%c", L->veri);
    uc_dugum_yaz(L->sagcocuk);
  }
}

SORU 3'ün çözümü. Örnek ağaçta (A…L) çıktı: GHJKL.

SORU 4: sağdan sola (özyinelemeli)

Çağrıların sırası değiştirilir: önce L->sagcocuk, sonra L->solcocuk. Çıktı: LKJHG.

SORU 5: soldan sağa (döngüsel)

İteratif kök ortada kodunda printf satırı koşula bağlanır:

if (!L->solcocuk && !L->sagcocuk)
  printf("%c", L->veri);
SORU 6: sağdan sola (döngüsel)

İteratif kodda her yerde solcocuk ↔ sagcocuk: yığına sağlar atılır, alınandan sonra sola gidilir. Yapraklar ters sırada çıkar.

İteratif Gezinme

SORU 7–9: çocuk sayısına göre seçmek

void yalniz_sol_yaz(agac_gost L) {           /* SORU 7 */
  if (!L) return;
  if (L->solcocuk && !L->sagcocuk)
    printf("%c", L->veri);
  yalniz_sol_yaz(L->solcocuk);
  yalniz_sol_yaz(L->sagcocuk);
}
SORU 8: yalnız sağ çocuklu

Koşul: !L->solcocuk && L->sagcocuk.

SORU 9: iki çocuklu

Koşul: L->solcocuk && L->sagcocuk. Örnek ağaçta: ACDI (kök başta sırayla).

Ortak kalıp: “ağacı gez, koşul sağlanıyorsa yaz”. !L->solcocuk = “sol çocuğu yok”.

Örnek ağaçta yalnız sol çocuklu: F, yalnız sağ çocuklu: BE.

Diğer Yordamlar

SORU: düğümleri seviye seviye yazdırmak — I. yöntem

ABDHECFG

kök ortada: HDBEAFCG · kök başta: ABDHECFG · kök sonda: HDEBFGCA

1A2BC3DEFG4H
  • Ağaç (herhangi bir gezinmeyle) gezilir; her düğüm seviyesinin listesinin sonuna eklenir; sonra listeler sırayla yazılır.
  • Her öğe 3 kez ele alınır → 3n = O(n) zaman; bellek O(n + h) (lg n ≤ h < n).

Diğer Yordamlar

II. yöntem: kuyrukla seviye seviye gezinme

  • Seviye seviye yazmak için önce gelen önce çıkmalı: bir düğümün çocukları, aynı seviyedeki diğer düğümlerden sonra yazılır → kuyruk (FIFO).
  • Kök kuyruğa konur; her adımda önden bir düğüm alınır, yazılır, çocukları arkaya eklenir.
  • Kuyruk boşalınca (kuyruk_al NULL) gezinme biter. Ağaç boş gelebilir: önce if (!L) return;
  • Kuyruğa da düğümlerin adresleri konur (dairesel kuyruk).
void kuyruk_ekle(int on, int *arka,
                 agac_gost p, agac_gost k[]) {
  *arka = (*arka + 1) % MAX_SIZE;
  if (*arka == on) {            /* dolu */
    fprintf(stderr, "Kuyruk dolu\n");
    exit(EXIT_FAILURE);
  }
  k[*arka] = p;
}
agac_gost kuyruk_al(int *on, int arka,
                    agac_gost k[]) {
  if (*on == arka) return NULL; /* boş */
  *on = (*on + 1) % MAX_SIZE;
  return k[*on];
}

Diğer Yordamlar · Kodu adım adım çalıştır

Kuyrukla seviye seviye: A B C D E F G H

ABDHECFGL0·1A2·3·4·5·6·7·8·▲on▲arka

Kök kuyruğa eklenir (arka = 1). Dairesel kuyruk: on ilk öğenin bir öncesini gösterir.

ABDHECFGL0·1A2·3·4·5·6·7·8·▲on▲arka

Kuyruğun önünden A alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4·5·6·7·8·▲on▲arka

A'nin çocukları (B, C) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.

ABDHECFGL0·1A2B3C4·5·6·7·8·▲on▲arka

Kuyruğun önünden B alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6·7·8·▲on▲arka

B'nin çocukları (D, E) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.

ABDHECFGL0·1A2B3C4D5E6·7·8·▲on▲arka

Kuyruğun önünden C alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6F7G8·▲on▲arka

C'nin çocukları (F, G) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.

ABDHECFGL0·1A2B3C4D5E6F7G8·▲on▲arka

Kuyruğun önünden D alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6F7G8H▲on▲arka

D'nin çocukları (H) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.

ABDHECFGL0·1A2B3C4D5E6F7G8H▲on▲arka

Kuyruğun önünden E alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6F7G8H▲on▲arka

Kuyruğun önünden F alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6F7G8H▲on▲arka

Kuyruğun önünden G alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFGL0·1A2B3C4D5E6F7G8H▲on▲arka

Kuyruğun önünden H alınır ve yazılır (FIFO: önce gelen önce çıkar).

ABDHECFG0·1A2B3C4D5E6F7G8H▲on▲arka

Kuyruk boş (on == arka): kuyruk_al NULL döner, döngü biter. Çıktı seviye seviye: ABCDEFGH.

void duzey_dolas(agac_gost L) {
  agac_gost kuyruk[MAX_SIZE];
  int on = 0, arka = 0;
  if (!L) return;              // ağaç boş olabilir
  kuyruk_ekle(on, &arka, L, kuyruk);
  while ((L = kuyruk_al(&on, arka, kuyruk))) {
    printf("%c", L->veri);
    if (L->solcocuk)
      kuyruk_ekle(on, &arka, L->solcocuk, kuyruk);
    if (L->sagcocuk)
      kuyruk_ekle(on, &arka, L->sagcocuk, kuyruk);
  }
}

Diğer Yordamlar

Seviye sırası gezinmenin maliyeti

  • Her düğüm kuyruğa bir kez girer, bir kez çıkar: 2n = O(n) zaman.
  • Kuyrukta aynı anda en fazla bir seviyenin genişliği (w) kadar düğüm bulunur (artı birkaç sonraki seviye düğümü).
  • Tam ağaçta son seviye ≈ n/2 düğüm → bellek O(n); çarpık ağaçta O(1).
  • O(lg n) bellek sınırı kuyruklu seviye sırası gezinme için değil, (dengeli ağaçta) yığınlı, derinlik öncelikli gezinmeler için geçerlidir.
  • Aynı fikir çizgelerde genişlik öncelikli aramadır (BFS); kuyruk yerine yığın kullanılırsa derinlik öncelikli aramaya (DFS) döner.
gezinmeyardımcı yapıek bellek
kök başta/ortada/sondayığın (LIFO)O(h)
seviye seviyekuyruk (FIFO)O(w) ≤ O(n)
Hocadan “Nasıl ki derinlik öncelikli aramada problemin doğası gereği yığın kullanma zorunluluğu vardı; bunda da kuyruk kullanma zorunluluğu vardır.”

Diğer Yordamlar

SORU: ağacın kopyasını çıkartmak

agac_gost agac_kopyala(agac_gost L) {
  if (!L) return NULL;
  agac_gost T = malloc(sizeof *T);
  if (!T) { perror("malloc"); exit(1); }
  T->solcocuk = agac_kopyala(L->solcocuk);
  T->sagcocuk = agac_kopyala(L->sagcocuk);
  T->veri = L->veri;
  return T;
}
Hocadan “Önce kâğıt üzerinde benim tahtada yaptığım çalışma yapılmalı, özyinelemeli çözüldüğü görülmeli. Bu yordam bottom-up çalışıyor, top-down da yazılabilirdi.”
  • Veri, iki çocuk kopyalandıktan sonra yazılıyor: yordam ağacı kök sonda biçiminde geziyor.
  • Satırların yeri değiştirilerek kök başta / kök ortada biçimine kolayca çevrilir.
  • ogeal() yerine doğrudan malloc + denetim kullanıldı.

Diğer Yordamlar · Kodu adım adım çalıştır

agac_kopyala(): asıl ağaç ve kopyası

L (asıl)T (kopya)ABC?

A için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.

L (asıl)T (kopya)ABC??

B için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.

L (asıl)T (kopya)ABCNULL??

L NULL → kopyalanacak bir şey yok, NULL döner (B'nin sol kopyası NULL olacak).

L (asıl)T (kopya)ABC??

Sol çağrı döndü → T->solcocuk = NULL.

L (asıl)T (kopya)ABCNULL??

L NULL → kopyalanacak bir şey yok, NULL döner (B'nin sağ kopyası NULL olacak).

L (asıl)T (kopya)ABC??

Sağ çağrı döndü → T->sagcocuk = NULL.

L (asıl)T (kopya)ABC?B

Veri kopyalanır (B) ve yeni düğümün adresi çağırana döndürülür.

L (asıl)T (kopya)ABC?B

Sol çağrı döndü → T->solcocuk = kopya(B).

L (asıl)T (kopya)ABC?B?

C için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.

L (asıl)T (kopya)ABCNULL?B?

L NULL → kopyalanacak bir şey yok, NULL döner (C'nin sol kopyası NULL olacak).

L (asıl)T (kopya)ABC?B?

Sol çağrı döndü → T->solcocuk = NULL.

L (asıl)T (kopya)ABCNULL?B?

L NULL → kopyalanacak bir şey yok, NULL döner (C'nin sağ kopyası NULL olacak).

L (asıl)T (kopya)ABC?B?

Sağ çağrı döndü → T->sagcocuk = NULL.

L (asıl)T (kopya)ABC?BC

Veri kopyalanır (C) ve yeni düğümün adresi çağırana döndürülür.

L (asıl)T (kopya)ABC?BC

Sağ çağrı döndü → T->sagcocuk = kopya(C).

L (asıl)T (kopya)ABCABC

Veri kopyalanır (A) ve yeni düğümün adresi çağırana döndürülür.

agac_gost agac_kopyala(agac_gost L) {
  if (!L) return NULL;
  agac_gost T = malloc(sizeof *T);
  if (!T) { perror("malloc"); exit(1); }
  T->solcocuk = agac_kopyala(L->solcocuk);
  T->sagcocuk = agac_kopyala(L->sagcocuk);
  T->veri = L->veri;
  return T;
}

Diğer Yordamlar

SORU: iki ağaç eş biçimli (isomorphic) mi?

int esitmi(agac_gost L, agac_gost T) {
  return (!L && !T) ||
         (L && T && L->veri == T->veri
          && esitmi(L->solcocuk, T->solcocuk)
          && esitmi(L->sagcocuk, T->sagcocuk));
}

Yalnızca biçim benzerliği isteniyorsa (veriler önemsizse) L->veri == T->veri ifadesi kaldırılır.

  • İkisi de boşsa eşittir; biri boş diğeri doluysa eşit değildir.
  • İkisi de doluysa: kökler eşit ve sol alt ağaçlar eşit ve sağ alt ağaçlar eşit.
  • && ve || kısa devre yapar: ilk farkta özyineleme durur.
Hocadan “İkili ağaçların izomorfik olup olmadığını denetlemek bu kadar kolay! İki çizgenin (graf) eş biçimliliğini bulmak ise oldukça zor bir problemdir.”

Diğer Yordamlar · Kodu adım adım çalıştır

esitmi(): A(B,C) ile A(B,D)

LTABCABD

İkisi de dolu ve veriler eşit (A = A) → sol alt ağaçlar karşılaştırılır.

LTABCABD

İkisi de dolu ve veriler eşit (B = B) → sol alt ağaçlar karşılaştırılır.

LTABCABD

İkisi de NULL: (!L && !T) doğru → 1 (boş ağaçlar eşittir); ||'nin sağı hiç hesaplanmaz.

LTABCABD

B: sol alt ağaçlar eşit çıktı → sağ alt ağaçlar karşılaştırılır.

LTABCABD

İkisi de NULL: (!L && !T) doğru → 1 (boş ağaçlar eşittir); ||'nin sağı hiç hesaplanmaz.

LTABCABD

B: sağ taraf 1 döndü → bu çağrı 1 döndürür.

LTABCABD

A: sol alt ağaçlar eşit çıktı → sağ alt ağaçlar karşılaştırılır.

LTABCABD

Veriler farklı (C ≠ D) → && kısa devre yapar; alt ağaçlara bakılmadan 0 döner.

LTABCABD

A: sağ taraf 0 döndü → bu çağrı 0 döndürür.

int esitmi(agac_gost L, agac_gost T) {
  return (!L && !T) ||
         (L && T && L->veri == T->veri
          && esitmi(L->solcocuk, T->solcocuk)
          && esitmi(L->sagcocuk, T->sagcocuk));
}

Diğer Yordamlar

SORU: esitmi() yordamını farklı biçimde yazınız

int esitmi2(agac_gost L, agac_gost T) {
  if (L == NULL || T == NULL)
    return L == T;          /* ikisi de NULL mı? */
  if (L->veri != T->veri)
    return 0;
  if (!esitmi2(L->solcocuk, T->solcocuk))
    return 0;
  return esitmi2(L->sagcocuk, T->sagcocuk);
}
  • Tek bir return ifadesi yerine erken dönüşlü (guard) yazım; davranış aynı.
  • L == T: biri NULL iken ikisi de NULL ise doğru.
  • Her özyinelemeli çağrının dönüş değeri kullanılmalı.
Hocadan ““Hocam, benimki çok güzel çalışıyor da geri dönmüyor. Çağırıyorum, çağırıyorum, ama dönmüyor, tıkanıp kalıyor.” O zaman yine özyinelemeli algoritma yazmayı öğrenememişiz demektir.”

Diğer Yordamlar · Kodu adım adım çalıştır

SORU: sol ve sağ çocukları değiştirmek

ACBDşimdi çalışançocukları değiştiyığında bekliyor

A: sol (B) ve sağ (C) işaretçileri takas edilir; veri yerinde kalır.

ACBDşimdi çalışançocukları değiştiyığında bekliyor

C: sol (NULL) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.

ACBDNULLşimdi çalışançocukları değiştiyığında bekliyor

L NULL → hiçbir şey yapmadan döner.

ACBDNULLşimdi çalışançocukları değiştiyığında bekliyor

L NULL → hiçbir şey yapmadan döner.

ACBDşimdi çalışançocukları değiştiyığında bekliyor

B: sol (D) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.

ACBDNULLşimdi çalışançocukları değiştiyığında bekliyor

L NULL → hiçbir şey yapmadan döner.

ACBDşimdi çalışançocukları değiştiyığında bekliyor

D: sol (NULL) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.

ACBDNULLşimdi çalışançocukları değiştiyığında bekliyor

L NULL → hiçbir şey yapmadan döner.

ACBDNULLşimdi çalışançocukları değiştiyığında bekliyor

L NULL → hiçbir şey yapmadan döner.

ACBDşimdi çalışançocukları değiştiyığında bekliyor

Bitti: ağaç aynadaki görüntüsüne döndü (kök ortada sırası tersine çevrildi: CABD).

#define SWAP(x, y, t) ((t) = (x), (x) = (y), (y) = (t))
void cocukdegistir(agac_gost L) {
  agac_gost p;
  if (L) {
    SWAP(L->solcocuk, L->sagcocuk, p);
    cocukdegistir(L->solcocuk);
    cocukdegistir(L->sagcocuk);
  }
}

“Verileri değiştirmiyorum, sadece işaretçileri takas ediyorum.”

Diğer Yordamlar

SWAP özyinelemeli çağrıların öncesinde, arasında, sonunda

ABCDbaşlangıç: sağa çarpık A-B-C-D
ABCDSWAP öncesinde → tam ayna

Örnek ağaç: tüm sağ alt ağaçlar tek kollu. SWAP çağrılardan önce: her düğümde takas, sonuç sola çarpık ayna.

ABCDSWAP sonunda → yine tam ayna
ABCDSWAP arasında → yalnız kök takaslandı

SWAP sonda (kök sonda) aynı sonucu verir. Arasında ise: sol NULL işlenir, takas olur, sonra “sağ” (artık NULL) işlenir → B alt ağacına hiç dokunulmaz.

“Aynı sonucu üretir” gözlemi önce/sonra için doğrudur; ara konumda sonuç farklıdır.

Sınav Sorusu

Sınav sorusu: kök ortada + kök sonda → ağaç

Kök ortada sırası C M D A K N B F E G, kök sonda sırası D A N F E B K M G C olan ikili ağacı çiziniz. Tek bir ağaç görünümü vardır.

Çözüm (adım adım)
Ckök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 1

Kök sondada en son C → kök C. Kök ortada C'den önce harf yok → C'nin sol çocuğu yok.

CGkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 2

Kalanların kök sondada sonuncusu G → C'nin sağı. Kök ortada G'den sonra harf yok → G'nin sağı yok.

CGMkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 3

Sıradaki M. Kök ortada M'den önce (bu alt ağaçta) harf yok → M'nin solu boş; M, G'nin solunda.

CGMKkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 4

Sıradaki K; M'nin sağına. Kök ortada K'dan önce D, A var → D ve A K'nın sol alt ağacında.

CGMKAkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 5

D ile A'dan kök sondada sonra gelen A → A atadır (K'nın solu).

CGMKADkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 6

D kök ortada A'dan önce basıldığına göre A'nın sol çocuğudur.

CGMKADBkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 7

K'nın sağ alt ağacı: N, B, F, E. Kök sondada en son B → K'nın sağı B.

CGMKADBNkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 8

Kök ortada B'den önce N var → N, B'nin sol çocuğu.

CGMKADBNEkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 9

Sıradaki E; B'nin sağına yerleşir.

CGMKADBNEFkök ortada:kök sonda:CMDAKNBFEGDANFEBKMGCADIM 10

Son düğüm F: kök ortada E'nin solunda → E'nin sol çocuğu. Ağaç tamam ve tektir.

Yöntem: kök sonda sırası sondan başa okunur (her harf bir alt ağacın kökü); kök ortada o harfin solunu/sağını ayırır. Kök başta + kök ortada da tek ağaç verir; kök başta + kök sonda genelde vermez (tek çocuğun solda mı sağda mı olduğu anlaşılmaz).

Özet

Karmaşıklık özeti (n düğüm, h derinlik)

işlemzamanek belleknot
ebeveyn / çocuk (dizi gösterimi)O(1)2k−1 göztam ağaçta kayıp yok
özyinelemeli kök başta / ortada / sondaO(n)O(h)n+1 NULL çağrısı
yığınlı kök ortada / önde / sondaO(n)O(h)yığına adres konur
seviye seviye (kuyruk)O(n)O(w) ≤ O(n)w: en geniş seviye
uç düğüm yaz, SORU 7–9O(n)O(h)gezinme + koşul
agac_kopyalaO(n)O(h) + n düğümkök sonda
esitmiO(min(n₁, n₂))O(h)ilk farkta durur
cocukdegistirO(n)O(h)yalnız işaretçiler

h: lg n ≤ h ≤ n (dengeli ↔ çarpık). Bu derste derinlik seviye sayısıyla ölçülür.

Özet

Sık yapılan hatalar

1L->veri'ye NULL kontrolü yapmadan erişmek (“ağaç işaretçisi NULL gelebilir”).
2Dizi gösteriminde 0'dan başlayıp 1 tabanlı formülleri (2i, 2i+1) kullanmak.
3Yığına düğümün kopyasını koymak; adres yetmez mi?
4Özyinelemeli çağrının dönüş değerini kullanmamak ya da return'den sonra iş yapmaya çalışmak.
5Uç düğüm koşulunda && yerine || yazmak.
6Derece ile derinliği, seviye 1 ile derinlik 0 tanımlarını karıştırmak.

Özet

Alıştırmalar

  1. n0 = n2 + 1 eşitliğini kanıtlayınız.
  2. (A+B)*(C-D) ifade ağacını çizip üç gezinmeyi yazınız.
  3. Düğüm sayısını döndüren int say(agac_gost L) yordamını yazınız.
  4. Kök başta A B D E C F, kök ortada D B E A F C olan ağacı çiziniz.
  5. Seviyeleri sağdan sola yazan kuyruklu yordamı yazınız.
  6. İki ağacın birbirinin aynası olup olmadığını denetleyen yordamı yazınız.
İpuçları / cevaplar
  1. n = n0+n1+n2; kök dışındaki her düğüme bir kenar girer: n−1 = n1+2n2. İkisi taraf tarafa çıkarılır.
  2. önde *+AB-CD, ortada A+B*C-D, sonda AB+CD-*.
  3. return L ? 1 + say(L->solcocuk) + say(L->sagcocuk) : 0;
  4. A kök; sol B(D, E); sağ C(F, –).
  5. Kuyruğa önce sağ, sonra sol çocuk eklenir.
  6. esitmi'de L'nin solunu T'nin sağıyla, L'nin sağı T'nin soluyla karşılaştırılır.

Özet

Bölüm özeti

Terminoloji

Kök, ebeveyn, çocuk, kardeş, ata, soy, derece, yaprak; seviye 1'den başlar, derinlik = en büyük seviye.

İkili ağaç

i. seviyede ≤ 2i−1, k derinlikte ≤ 2k−1 düğüm; tam ağaç diziye sığar.

Gösterim

Tam ve sabit ağaç → dizi; değişen ağaç → agac_gost bağları.

Gezinme

Üç özyinelemeli gezinme aynı iskelet; yığınla döngüsel, kuyrukla seviye seviye.

Yordamlar

Uç düğüm, kopyala, esitmi, cocukdegistir: hepsi “gez + işle” kalıbı.

Maliyet

Zaman O(n); bellek derinlikle orantılı O(h).

Özet

Kaynaklar ve devam

  • E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, 2. bs., Bölüm 5 (Trees).
  • T. H. Cormen vd., Introduction to Algorithms, Ek B.5 ve Bölüm 12.
  • R. Sedgewick, Algorithms in C, Kısım 1–4, Bölüm 5 (Trees).

Konunun devamı: ikili arama ağaçları ve kılavuzlu ikili ağaçlar.

Son

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