Bölüm 10: Ağaçlar
Giriş
Öğrenme Çıktıları
Kök, ebeveyn, çocuk, kardeş, ata, soy, derece, seviye, derinlik ve yaprak kavramlarını bir ağaç üzerinde göstermek.
Lemmayı (2i−1, 2k−1) kanıtlamak; tam dolu, tam ve tam olmayan ağaçları ayırmak.
Diziyle (ebeveyn = ⌊i/2⌋) ve bağlı yapıyla (agac_gost) gösterim; bellek maliyetini karşılaştırmak.
Kök başta / ortada / sonda gezinmeleri özyinelemeli ve yığınla döngüsel yazmak; seviye seviye gezmek.
Uç düğüm yazma, kopyalama, eşbiçimlilik ve sol–sağ değiş tokuşu özyinelemeli kurmak.
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.
Terminoloji
Terminoloji: bir örnek ağaç üzerinde
B; E ve F'nin ebeveyni, E ve F B'nin çocukları. Aynı ebeveynin çocukları kardeştir (sibling).
Bir düğümün alt ağaç sayısı: A'nın derecesi 3, F'nin 0 (sağ üstteki küçük sayılar).
Derecesi 0 olan düğüm (leaf, terminal): K, L, F, G, M, I, J.
Terminoloji
Terminoloji adım adım
Kök A'dır. Seviyeler kökten başlayarak numaralanır: bu derste kök 1. seviyededir.
Alt ağaçları olan düğüm, alt ağaçlarının köklerinin ebeveynidir (parent); onlar da onun çocuklarıdır (child).
Aynı ebeveynin çocukları kardeş düğümlerdir. Terminoloji büyükbaba–torun diye diğer kuşaklara genişler.
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.
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.
Derecesi 0 olan düğümlere yaprak ya da uç düğüm denir.
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.
| kavram | bu ders | CLRS |
|---|---|---|
| kökün seviyesi | 1 | 0 |
| M'nin seviyesi | 4 | 3 |
| ağacın derinliği | 4 | yü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.
İkili Ağaç
Lemma: bir seviyede ve bir ağaçta en çok kaç düğüm?
i) i. seviyedeki en fazla düğüm sayısı 2i−1 (i ≥ 1). Taban: i = 1 için 20 = 1 (yalnız kök).
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.
ii) k derinliğinde bir ikili ağaçta en fazla 2k − 1 düğüm: Σi=1..k 2i−1 = 20 + 21 + … + 2k−1.
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ç
Derinliği k olan ve 2k − 1 düğümlü ikili ağaç: tüm seviyeler tam dolu.
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ç.
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ı).
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ç
Tam ağaç: dizi gözlerinin neredeyse tamamı dolar; bellek kaybı ya hiç olmaz ya da çok az olur.
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.
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).
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
freeedilirse ç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
ebeveynalanı eklenir).
Gösterim
Genel ağacı ikili ağaca çevirme: sol çocuk – sağ kardeş
Genel ağaçta düğüm başına çocuk sayısı değişkendir; sabit alanlı bir struct ile tutmak zordur.
Her düğüm için yalnızca ilk çocuğa bağ kalır; kardeşler kendi aralarında sağ kardeş bağıyla zincirlenir.
İ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ıra | ad | |
|---|---|---|
| 1 | Sol – Veri – Sağ | kök ortada (inorder) |
| 2 | Sol – Sağ – Veri | kök sonda (postorder) |
| 3 | Veri – Sol – Sağ | kök başta (preorder) |
| 4 | Sağ – Veri – Sol | ters kök ortada |
| 5 | Sağ – Sol – Veri | ters kök sonda |
| 6 | Veri – Sağ – Sol | ters kök başta |
Gezinmeler
Aritmetik ifade ağacı ve üç gösterim
- 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
Kök başta (V-L-R): önce kök, sonra sol alt ağacın tamamı, en son sağ alt ağaç → ABCDGHEIJKFL
Kök ortada (L-V-R): B'nin solu boş olduğundan ilk B yazılır → BGDHCEJIKALF
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); } }
Üç 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)
L = A: sol alt ağaç için kok_ortada_dolas(B) çağrılır; bu çerçeve bekler.
L = B: sol alt ağaç için kok_ortada_dolas(D) çağrılır; bu çerçeve bekler.
L = D: sol alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).
L = D: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya D eklenir.
L = D: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).
L = B: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya B eklenir.
L = B: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).
L = A: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya A eklenir.
L = A: sağ alt ağaç için kok_ortada_dolas(C) çağrılır; bu çerçeve bekler.
L = C: sol alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).
L = C: sol alt ağaç bitti; şimdi kökü yazdırır → çıktıya C eklenir.
L = C: sağ alt ağaç için kok_ortada_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).
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
L = A: kök başta: önce kendini yazdırır → çıktıya A eklenir.
L = A: sol alt ağaç için kok_onde_dolas(B) çağrılır; bu çerçeve bekler.
L = B: kök başta: önce kendini yazdırır → çıktıya B eklenir.
L = B: sol alt ağaç için kok_onde_dolas(D) çağrılır; bu çerçeve bekler.
L = D: kök başta: önce kendini yazdırır → çıktıya D eklenir.
L = D: sol alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).
L = D: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).
L = B: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).
L = A: sağ alt ağaç için kok_onde_dolas(C) çağrılır; bu çerçeve bekler.
L = C: kök başta: önce kendini yazdırır → çıktıya C eklenir.
L = C: sol alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).
L = C: sağ alt ağaç için kok_onde_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).
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
L = A: sol alt ağaç için kok_sonda_dolas(B) çağrılır; bu çerçeve bekler.
L = B: sol alt ağaç için kok_sonda_dolas(D) çağrılır; bu çerçeve bekler.
L = D: sol alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sol çocuğu yok).
L = D: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (D'nin sağ çocuğu yok).
L = D: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya D eklenir.
L = B: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (B'nin sağ çocuğu yok).
L = B: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya B eklenir.
L = A: sağ alt ağaç için kok_sonda_dolas(C) çağrılır; bu çerçeve bekler.
L = C: sol alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sol çocuğu yok).
L = C: sağ alt ağaç için kok_sonda_dolas(NULL) çağrılır; bu çerçeve bekler.
L == NULL → if (L) yanlış; bu çağrı hiçbir şey yapmadan döner (C'nin sağ çocuğu yok).
L = C: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya C eklenir.
L = A: iki alt ağaç da bitti; en son kökü yazdırır → çıktıya A eklenir.
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).
Gezinmeler
Özyinelemeli kod bizzat yazılarak öğrenilir
— Çin atasözü
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
+ bir işleç: önce sol alt ifade hesaplanır.
* bir işleç: önce sol alt ifade hesaplanır.
/ bir işleç: önce sol alt ifade hesaplanır.
8 bir yaprak: sayıyı döndürür → 8.
sol değer a = 8. Şimdi sağ alt ifade hesaplanır.
4 bir yaprak: sayıyı döndürür → 4.
b = 4; 8 / 4 = 2 döndürülür (kök sonda sırası: sol, sağ, kök).
sol değer a = 2. Şimdi sağ alt ifade hesaplanır.
3 bir yaprak: sayıyı döndürür → 3.
b = 3; 2 * 3 = 6 döndürülür (kök sonda sırası: sol, sağ, kök).
sol değer a = 6. Şimdi sağ alt ifade hesaplanır.
5 bir yaprak: sayıyı döndürür → 5.
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.
İ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)
A yığına eklenir (adresi!), sonra sola: L = A->solcocuk = B.
B yığına eklenir (adresi!), sonra sola: L = B->solcocuk = D.
D yığına eklenir (adresi!), sonra sola: L = D->solcocuk = NULL. L NULL → iç döngü biter.
Tepeden D alınır; solu bitmiş demektir → D yazılır.
L = D->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.
Tepeden B alınır; solu bitmiş demektir → B yazılır.
L = B->sagcocuk = E: sağ alt ağaç aynı biçimde gezilecek.
E yığına eklenir (adresi!), sonra sola: L = E->solcocuk = NULL. L NULL → iç döngü biter.
Tepeden E alınır; solu bitmiş demektir → E yazılır.
L = E->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.
Tepeden A alınır; solu bitmiş demektir → A yazılır.
L = A->sagcocuk = C: sağ alt ağaç aynı biçimde gezilecek.
C yığına eklenir (adresi!), sonra sola: L = C->solcocuk = NULL. L NULL → iç döngü biter.
Tepeden C alınır; solu bitmiş demektir → C yazılır.
L = C->sagcocuk = NULL: sağ alt ağaç yok, yığından devam.
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.
— Alan J. Perlis
İteratif Gezinme
Sorular: gezinme kalıbını değiştirerek yaz
- Kök başta gezinme yordamını döngüsel yazınız.
- Kök sonda gezinme yordamını döngüsel yazınız. (Bu daha zordur.)
- Uç düğümleri soldan sağa yazan özyinelemeli yordam.
- Uç düğümleri sağdan sola yazan özyinelemeli yordam.
- Uç düğümleri soldan sağa yazan döngüsel yordam.
- Uç düğümleri sağdan sola yazan döngüsel yordam.
- Yalnız sol çocuğu olan düğümleri yazan özyinelemeli yordam.
- Yalnız sağ çocuğu olan düğümleri yazan özyinelemeli yordam.
- Her iki çocuğu da olan düğümleri yazan özyinelemeli yordam.
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
printfdüğü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).
İteratif Gezinme · Kodu adım adım çalıştır
SORU 2 adım adım: A(B(D),C)
L = A yığına atılır, sola inilir: L = B.
L = B yığına atılır, sola inilir: L = D.
L = D yığına atılır, sola inilir: L = NULL.
Tepe D: sağ çocuğu yok → D yazılır, yığından alınır; son = D.
Tepe B: sağ çocuğu yok → B yazılır, yığından alınır; son = B.
Tepe A: sağ çocuğu C var ve henüz yazılmadı → önce sağ alt ağaç (L = C).
L = C yığına atılır, sola inilir: L = NULL.
Tepe C: sağ çocuğu yok → C yazılır, yığından alınır; son = C.
Tepe A: sağ çocuğu C az önce yazıldı (son) → A yazılır, yığından alınır; son = A.
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).
!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
kök ortada: HDBEAFCG · kök başta: ABDHECFG · kök sonda: HDEBFGCA
- 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_alNULL) gezinme biter. Ağaç boş gelebilir: önceif (!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
Kök kuyruğa eklenir (arka = 1). Dairesel kuyruk: on ilk öğenin bir öncesini gösterir.
Kuyruğun önünden A alınır ve yazılır (FIFO: önce gelen önce çıkar).
A'nin çocukları (B, C) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.
Kuyruğun önünden B alınır ve yazılır (FIFO: önce gelen önce çıkar).
B'nin çocukları (D, E) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.
Kuyruğun önünden C alınır ve yazılır (FIFO: önce gelen önce çıkar).
C'nin çocukları (F, G) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.
Kuyruğun önünden D alınır ve yazılır (FIFO: önce gelen önce çıkar).
D'nin çocukları (H) kuyruğun arkasına eklenir: bir sonraki seviye sırada bekler.
Kuyruğun önünden E alınır ve yazılır (FIFO: önce gelen önce çıkar).
Kuyruğun önünden F alınır ve yazılır (FIFO: önce gelen önce çıkar).
Kuyruğun önünden G alınır ve yazılır (FIFO: önce gelen önce çıkar).
Kuyruğun önünden H alınır ve yazılır (FIFO: önce gelen önce çıkar).
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.
| gezinme | yardımcı yapı | ek bellek |
|---|---|---|
| kök başta/ortada/sonda | yığın (LIFO) | O(h) |
| seviye seviye | kuyruk (FIFO) | O(w) ≤ O(n) |
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;
}- 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ğrudanmalloc+ denetim kullanıldı.
Diğer Yordamlar · Kodu adım adım çalıştır
agac_kopyala(): asıl ağaç ve kopyası
A için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.
B için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.
L NULL → kopyalanacak bir şey yok, NULL döner (B'nin sol kopyası NULL olacak).
Sol çağrı döndü → T->solcocuk = NULL.
L NULL → kopyalanacak bir şey yok, NULL döner (B'nin sağ kopyası NULL olacak).
Sağ çağrı döndü → T->sagcocuk = NULL.
Veri kopyalanır (B) ve yeni düğümün adresi çağırana döndürülür.
Sol çağrı döndü → T->solcocuk = kopya(B).
C için yeni düğüm ayrılır (malloc denetlendi); verisi henüz yazılmadı.
L NULL → kopyalanacak bir şey yok, NULL döner (C'nin sol kopyası NULL olacak).
Sol çağrı döndü → T->solcocuk = NULL.
L NULL → kopyalanacak bir şey yok, NULL döner (C'nin sağ kopyası NULL olacak).
Sağ çağrı döndü → T->sagcocuk = NULL.
Veri kopyalanır (C) ve yeni düğümün adresi çağırana döndürülür.
Sağ çağrı döndü → T->sagcocuk = kopya(C).
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.
Diğer Yordamlar · Kodu adım adım çalıştır
esitmi(): A(B,C) ile A(B,D)
İkisi de dolu ve veriler eşit (A = A) → sol alt ağaçlar karşılaştırılır.
İkisi de dolu ve veriler eşit (B = B) → sol alt ağaçlar karşılaştırılır.
İkisi de NULL: (!L && !T) doğru → 1 (boş ağaçlar eşittir); ||'nin sağı hiç hesaplanmaz.
B: sol alt ağaçlar eşit çıktı → sağ alt ağaçlar karşılaştırılır.
İkisi de NULL: (!L && !T) doğru → 1 (boş ağaçlar eşittir); ||'nin sağı hiç hesaplanmaz.
B: sağ taraf 1 döndü → bu çağrı 1 döndürür.
A: sol alt ağaçlar eşit çıktı → sağ alt ağaçlar karşılaştırılır.
Veriler farklı (C ≠ D) → && kısa devre yapar; alt ağaçlara bakılmadan 0 döner.
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
returnifadesi 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ı.
Diğer Yordamlar · Kodu adım adım çalıştır
SORU: sol ve sağ çocukları değiştirmek
A: sol (B) ve sağ (C) işaretçileri takas edilir; veri yerinde kalır.
C: sol (NULL) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.
L NULL → hiçbir şey yapmadan döner.
L NULL → hiçbir şey yapmadan döner.
B: sol (D) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.
L NULL → hiçbir şey yapmadan döner.
D: sol (NULL) ve sağ (NULL) işaretçileri takas edilir; veri yerinde kalır.
L NULL → hiçbir şey yapmadan döner.
L NULL → hiçbir şey yapmadan döner.
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
Ö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.
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)
Kök sondada en son C → kök C. Kök ortada C'den önce harf yok → C'nin sol çocuğu yok.
Kalanların kök sondada sonuncusu G → C'nin sağı. Kök ortada G'den sonra harf yok → G'nin sağı yok.
Sıradaki M. Kök ortada M'den önce (bu alt ağaçta) harf yok → M'nin solu boş; M, G'nin solunda.
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.
D ile A'dan kök sondada sonra gelen A → A atadır (K'nın solu).
D kök ortada A'dan önce basıldığına göre A'nın sol çocuğudur.
K'nın sağ alt ağacı: N, B, F, E. Kök sondada en son B → K'nın sağı B.
Kök ortada B'den önce N var → N, B'nin sol çocuğu.
Sıradaki E; B'nin sağına yerleşir.
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şlem | zaman | ek bellek | not |
|---|---|---|---|
| ebeveyn / çocuk (dizi gösterimi) | O(1) | 2k−1 göz | tam ağaçta kayıp yok |
| özyinelemeli kök başta / ortada / sonda | O(n) | O(h) | n+1 NULL çağrısı |
| yığınlı kök ortada / önde / sonda | O(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–9 | O(n) | O(h) | gezinme + koşul |
| agac_kopyala | O(n) | O(h) + n düğüm | kök sonda |
| esitmi | O(min(n₁, n₂)) | O(h) | ilk farkta durur |
| cocukdegistir | O(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
L->veri'ye NULL kontrolü yapmadan erişmek (“ağaç işaretçisi NULL gelebilir”).return'den sonra iş yapmaya çalışmak.&& yerine || yazmak.Özet
Alıştırmalar
- n0 = n2 + 1 eşitliğini kanıtlayınız.
(A+B)*(C-D)ifade ağacını çizip üç gezinmeyi yazınız.- Düğüm sayısını döndüren
int say(agac_gost L)yordamını yazınız. - Kök başta
A B D E C F, kök ortadaD B E A F Colan ağacı çiziniz. - Seviyeleri sağdan sola yazan kuyruklu yordamı yazınız.
- İki ağacın birbirinin aynası olup olmadığını denetleyen yordamı yazınız.
İpuçları / cevaplar
- n = n0+n1+n2; kök dışındaki her düğüme bir kenar girer: n−1 = n1+2n2. İkisi taraf tarafa çıkarılır.
- önde
*+AB-CD, ortadaA+B*C-D, sondaAB+CD-*. return L ? 1 + say(L->solcocuk) + say(L->sagcocuk) : 0;- A kök; sol B(D, E); sağ C(F, –).
- Kuyruğa önce sağ, sonra sol çocuk eklenir.
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
Kök, ebeveyn, çocuk, kardeş, ata, soy, derece, yaprak; seviye 1'den başlar, derinlik = en büyük seviye.
i. seviyede ≤ 2i−1, k derinlikte ≤ 2k−1 düğüm; tam ağaç diziye sığar.
Tam ve sabit ağaç → dizi; değişen ağaç → agac_gost bağları.
Üç özyinelemeli gezinme aynı iskelet; yığınla döngüsel, kuyrukla seviye seviye.
Uç düğüm, kopyala, esitmi, cocukdegistir: hepsi “gez + işle” kalıbı.
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.