Veri Yapıları · Ders Notu
Bölüm 9: Ağaçlar
Diziler, bağlı listeler, yığınlar ve kuyruklar öğeleri tek bir sıra hâlinde tutar: her öğenin en çok bir öncülü ve bir ardılı vardır. Oysa bilgisayarda saklanan verinin önemli bir kısmı sıralı değil, hiyerarşiktir. Klasörlerin içinde klasörler, bir kitabın içinde kısımlar ve bölümler, bir web sayfasının içinde iç içe geçmiş öğeler bulunur. Bu tür ilişkileri doğal olarak tutan yapı ağaçtır. Not, ağacın terimleriyle başlar: kök, ebeveyn, çocuk, yaprak, derece, derinlik ve yükseklik. Ardından her düğümün en çok iki çocuğu olan ikili ağaçlara, bunların dolu, tam ve mükemmel türlerine ve düğüm sayısıyla yükseklik arasındaki bağıntılara geçer. Ağacın bellekte dizi ve bağlantılarla nasıl tutulacağı anlatıldıktan sonra notun ağırlık merkezi gezinmelerdir: kök başta, kök ortada, kök sonda ve seviye sırası. Bu gezinmeler önce özyinelemeyle, sonra açık bir yığın ve kuyrukla kodlanır; her kod adım adım izlenir. Son kesimler ifade ağaçlarını, ağaç üzerinde çalışan özyinelemeli metotları ve iki gezinme çıktısından ağacı yeniden kurmayı işler.
Öğrenme çıktıları
- Temel kavramlar
- Kök, ebeveyn, çocuk, kardeş, ata, soy, yaprak, derece, derinlik ve yükseklik terimlerini açıklamak ve verilen bir ağaçta bu değerleri bulmak.
- Gösterimler
- Ağaçları düğüm bağlantılarıyla ve dizilerle temsil etmek; iki gösterimin bellek ve erişim maliyetlerini karşılaştırmak.
- Gezinmeler
- Kök başta, kök ortada, kök sonda ve seviye sırası gezinmelerini elle ve kodla uygulamak; özyinelemeli çağrıları çağrı yığını üzerinde izlemek.
- Uygulama ve maliyet
- Genel ve ikili ağaçları kullanım alanları ve işlem maliyetleriyle karşılaştırmak; ağaç üzerinde çalışan özyinelemeli metotları yazmak.
9.1Ağaç kavramı
Şimdiye kadarki veri yapılarının hepsi doğrusaldır: öğeler bir çizgi üzerinde dizilir, her öğeden sonra en çok bir öğe gelir. Bu düzen, sıra bilgisinin önemli olduğu işler için uygundur. Ancak bir şirketin örgüt şeması, bir bilgisayarın dosya sistemi ya da bir soy ağacı tek bir çizgiye sığmaz. Bu yapılarda bir öğenin altında birden çok öğe bulunabilir; her biri de kendi altındakilerin üstüdür. Bu kesim, böyle hiyerarşileri tutan ağaç yapısının dilini kurar.
9.1.1Ağaç nedir?
Ağaç (tree), öğeleri hiyerarşik biçimde tutan, doğrusal olmayan bir veri yapısıdır. Öğelerin her birine düğüm (node), iki düğümü birbirine bağlayan çizgiye kenar (edge) denir. En üstte tek bir düğüm bulunur; buna kök (root) adı verilir. Bilgisayar biliminde ağaç ters çizilir: kök yukarıda durur, dallar aşağı doğru iner. Şekil 9.1 küçük bir ağacı bu biçimde gösterir.
Kenarlar bir yön taşır: her kenar bir üst düğümü bir alt düğüme bağlar. Üstteki düğüm alttakinin ebeveyni (parent), alttaki de üsttekinin çocuğudur (child). Ağacı ağaç yapan kural şudur: kök dışındaki her düğümün tam olarak bir ebeveyni vardır. Kökün ise hiç ebeveyni yoktur. Bir düğümün çocuk sayısına bu tanımda bir sınır konmaz; sıfır, bir ya da daha çok çocuk olabilir.
Bir ağaç, düğümlerden ve düğümleri ebeveyn–çocuk ilişkisiyle bağlayan kenarlardan oluşan bir yapıdır. Ağaç boş değilse şu iki koşulu sağlar: (1) ebeveyni olmayan tek bir düğüm vardır; bu düğüm köktür. (2) Kök dışındaki her düğümün tam olarak bir ebeveyni vardır ve her düğümden ebeveynden ebeveyne yukarı çıkılarak köke ulaşılır.
Aynı yapı özyinelemeli olarak da tanımlanabilir: ağaç ya boştur ya da bir kök ile kökün altına asılan sıfır ya da daha çok ağaçtan oluşur. Köke asılan bu ağaçlara kökün alt ağaçları denir.
Tanımdaki ikinci koşulun sonucu önemlidir: ağaçta döngü (cycle) bulunamaz. Bir düğümden başlayıp kenarlar üzerinden yürüyerek aynı düğüme, aynı kenarı iki kez kullanmadan dönmek mümkün değildir. Döngü olsaydı, döngüdeki düğümlerden birinin iki ayrı yoldan gelen iki ebeveyni olması ya da yukarı çıkışın köke hiç varmayıp dönüp durması gerekirdi. Her iki durum da tanıma aykırıdır.
Ağaçlarla ilgili en çok kullanılan sayısal gerçek, kenar sayısıdır. Kök dışındaki her düğüm ebeveynine bir kenarla bağlanır ve her kenar tam olarak bir düğümü ebeveynine bağlar. Kenarları, alttaki uçlarındaki düğümlerle eşlersek bire bir bir eşleme elde ederiz.
n ≥ 1 düğümlü her ağaçta tam olarak n − 1 kenar vardır.
Her kenarı alt ucundaki düğümle, yani çocukla eşleyelim. Bir düğümün tek ebeveyni olduğu için her çocuk en çok bir kenarla eşlenir. Kök hiçbir kenarın alt ucu değildir; kök dışındaki her düğüm ise tam bir kenarın alt ucudur. Böylece kenarlar ile kök dışındaki n − 1 düğüm arasında bire bir bir eşleme kurulur ve kenar sayısı n − 1 olur. □
Şekil 9.1 bu kuralı doğrular: yedi düğüm, altı kenar. Kural iki yönde de iş görür. Bir ağacın düğüm sayısı biliniyorsa kenar sayısı hemen bulunur. Bağlantılı bir yapıda kenar sayısı düğüm sayısından küçük değilse, yapı ağaç olamaz; içinde mutlaka bir döngü vardır.
9.1.2Günlük hayatta hiyerarşiler
Ağaç, bilgisayar bilimine dışarıdan gelmiş bir fikirdir; insanlar hiyerarşileri çok önceden ağaç biçiminde çizer. Birkaç örnek kavramı somutlaştırır.
- Dosya sistemi. En üstte kök dizin bulunur (Unix benzeri sistemlerde
/). Klasörler başka klasör ve dosyaları içerir, dosyalar ise hiçbir şey içermez. Klasörler ağacın iç düğümleri, dosyalar yapraklarıdır. - Kitap. Bir kitap kısımlara, kısımlar bölümlere, bölümler alt başlıklara ayrılır. İçindekiler sayfası bu ağacın girintili bir yazımıdır.
- Coğrafya. Kıtalar ülkelerden, ülkeler eyalet ya da illerden, iller de şehirlerden oluşur.
- Web sayfası. Bir HTML belgesi iç içe öğelerden oluşur:
<html>öğesinin içinde<head>ve<body>, onların içinde başka öğeler bulunur. Tarayıcı belgeyi okurken bu öğelerden bir ağaç kurar. Bu ağaca belge nesne modeli (Document Object Model, kısaca DOM) denir.
/ altında etc, bin, home ve var bulunur; home altında ali ve ayse klasörleri vardır. Turuncu kutular dosyalardır ve ağacın yapraklarını oluşturur.Şekil 9.2 bir dosyanın tam yolunun neden tek olduğunu da gösterir. notlar.txt dosyasına kökten başlayıp /home/ali/notlar.txt yolundan ulaşılır; başka bir yol yoktur. Her düğümün tek ebeveyni olduğundan kökten bir düğüme inen yol da tektir. Kısayol (bağ) dosyaları bu kuralı delen ek bağlantılardır; onlar eklenince yapı artık saf bir ağaç olmaz.
9.1.3Bir yapı ne zaman ağaçtır?
Kökü işaretlenmemiş, rastgele çizilmiş bir bağlantı yapısının ağaç olup olmadığını anlamak için tanımdaki koşulları yapının kendi özelliklerine çevirmek gerekir. Kök henüz seçilmemişse ebeveyn kavramı da yoktur. Bu durumda ağaç olmak iki koşula bağlıdır. Birincisi, yapı bağlantılıdır: her düğümden her düğüme bir yol vardır. İkincisi, yapıda döngü yoktur. Bu iki koşulun sonucu olarak kenar sayısı, düğüm sayısından bir eksik çıkar. Bu koşulları sağlayan bir yapıda herhangi bir düğüm kök seçilebilir. Seçilen kökten uzaklaştıkça her düğümün ebeveyni, köke doğru olan tek komşusu olur.
Şekil 9.3'ün solundaki yapı bir ağaç mıdır?
- Düğüm ve kenar sayısı. Düğümler A'dan P'ye 16 tanedir. Kenarlar sayılınca 15 bulunur: A–B, B–C, A–H, H–G, G–F, H–E, E–D, D–I, I–J, I–K, K–P, P–L, I–M, M–N, M–O.
- Bağlantılılık. I'dan başlanırsa D, J, K, M'ye; D'den E'ye, E'den H'ye, H'den A ve G'ye; A'dan B'ye, B'den C'ye; G'den F'ye; K'den P'ye, P'den L'ye; M'den N ve O'ya ulaşılır. 16 düğümün hepsine varılmıştır.
- Sonuç. Yapı bağlantılıdır ve 16 düğüme 15 kenar düşer; bu nedenle döngü yoktur ve yapı bir ağaçtır. Herhangi bir düğüm, örneğin I, kök seçilebilir. Sağdaki çizim bu seçimin sonucudur.
Örnekte döngünün yokluğunu ayrıca denetlemedik; sayım bunu kendiliğinden verdi. Bağlantılı bir yapıyı ağaç hâline getirmek için en az n − 1 kenar gerekir. Bağlantılı bir yapıya bir kenar daha eklemek, zaten arasında yol bulunan iki düğümü ikinci kez bağlamak demektir. Bu da bir döngü oluşturur. Dolayısıyla “bağlantılı ve n − 1 kenarlı” olmak, “bağlantılı ve döngüsüz” olmakla aynı şeydir.
Şekil 9.4 ağaç tanımını bozan üç tipik durumu gösterir. Soldaki üçgende üç düğüme üç kenar düşer; kenar sayısı bir fazladır ve döngü oluşmuştur. Ortadaki yapıda alttaki düğüm iki üst düğüme bağlıdır; hangisi kök seçilirse seçilsin, bu düğümün kökten iki ayrı yolla ulaşılması gerekir. Sağdaki yapıda ise üç düğüme bir kenar düşer. Kenar eksik olduğu için yapı iki parçaya ayrılmıştır. Birbirinden kopuk ağaçların oluşturduğu böyle bir topluluğa orman (forest) denir.
Yalnız bağlantılılığa bakıp yapıyı ağaç saymak. Bağlantılı, 10 düğümlü ve 10 kenarlı bir yapı ağaç değildir: ağaçta kenar sayısı 9 olmalıdır ve fazladan kenar mutlaka bir döngü kurar. Tersine, kenar sayısının n − 1 olması da tek başına yetmez; döngülü bir parça ile kopuk bir düğüm birlikte de n − 1 kenar verebilir. İki koşulun birlikte aranması gerekir: bağlantılılık ve n − 1 kenar (ya da bağlantılılık ve döngüsüzlük).
9.1.4Kök, iç düğüm ve yaprak
Ağaçtaki düğümler çocuk sahibi olup olmadıklarına göre iki gruba ayrılır. Bu ayrım, ağaç üzerindeki algoritmaların neredeyse hepsinde karşımıza çıkar; çünkü özyinelemeli bir metot çoğu zaman yapraklarda durur, iç düğümlerde çocuklarına iner.
- Kök düğüm (root): Ağacın en üstündeki, ebeveyni olmayan düğümdür. Dosya sisteminde kök dizine karşılık gelir.
- İç düğüm (internal node): En az bir çocuğu olan düğümdür. Dosya sisteminde klasörlere karşılık gelir.
- Yaprak (leaf): Hiç çocuğu olmayan düğümdür. Ağacın dış kenarında durduğu için dış düğüm (external node) adıyla da anılır. Dosya sisteminde dosyalara karşılık gelir.
Şekil 9.5 iki ince noktaya dikkat çeker. Birincisi, kök de çocuğu varsa bir iç düğümdür; “kök” ve “iç düğüm” birbirini dışlayan sınıflar değildir. Kök, konumuyla; iç düğüm ve yaprak ise çocuk sayısıyla tanımlanır. İkincisi, yaprakların hepsi aynı seviyede olmak zorunda değildir: 9, kökün doğrudan çocuğudur ama 4, 5, 6, 7 gibi o da bir yapraktır. Tek düğümlü bir ağaçta ise kökün çocuğu yoktur; bu durumda kök aynı zamanda yapraktır.
Yaprakları yalnız en alt satırda aramak. Yaprak, çizimde nerede durduğuna göre değil, çocuğu olup olmadığına göre belirlenir. Şekil 9.5'teki 9 düğümü ikinci satırdadır ama çocuğu yoktur, yani yapraktır. Aynı biçimde kökü yaprak saymamak da hatalı olabilir: tek düğümlü ağaçta kök yapraktır.
9.1.5Düğümler arası ilişkiler
Ağaç terimlerinin çoğu bir aile ağacından alınmıştır. Bir düğümün hemen üstündeki düğüm ebeveyn, hemen altındakiler çocuklardır. Aynı ebeveynin çocukları birbirinin kardeşidir (sibling). Bir düğümden köke giden yol üzerindeki düğümler o düğümün atalarıdır (ancestor). Bir düğümün altında kalan, yani o düğümden aşağı inerek ulaşılabilen bütün düğümler ise onun soyudur (descendant). Tablo 9.1 bu ilişkileri Şekil 9.6'daki ağaç üzerinde örnekler.
| Terim | Anlamı | Örnek |
|---|---|---|
| Ebeveyn | Hemen üstteki düğüm | D, H'nin ebeveynidir. |
| Çocuk | Hemen alttaki düğüm | H, I ve J, D'nin çocuklarıdır. |
| Kardeş | Ebeveyni aynı olan düğüm | H, I ve J kardeştir. |
| Ata | Köke giden yoldaki düğüm | M'nin ataları H, D ve A'dır. |
| Soy | Alt ağaçtaki düğüm | D'nin soyu H, I, J ve M'dir. |
Ata ve soy birbirinin tersidir: M'nin ataları H, D ve A olduğuna göre M de H'nin, D'nin ve A'nın soyundandır. Kök, ağaçtaki bütün düğümlerin atasıdır; yapraklar ise kimsenin atası değildir. Bazı kaynaklar bir düğümü kendi atası ve kendi soyu da sayar. Bu notta, aksi söylenmedikçe, ata ve soy düğümün kendisini içermez. Bir düğümün soyu, o düğümün alt ağacındaki diğer düğümlerdir.
Kardeşlik çoğu zaman yanlış anlaşılır. İki düğümün aynı derinlikte, yani çizimde aynı satırda olması onları kardeş yapmaz. Şekil 9.6'daki F ve G aynı satırdadır, ama F'nin ebeveyni B, G'ninki C'dir; bu yüzden kardeş değillerdir. Akrabalık diliyle söylenirse F ile G kuzendir. Kardeşlik yalnız ortak ebeveyn ile tanımlanır.
Aynı seviyedeki düğümleri kardeş saymak ya da bir düğümün soyunu yalnız çocukları sanmak. Soy, alt ağacın tamamıdır: D'nin soyu yalnız H, I, J değil, H'nin çocuğu M ile birlikte H, I, J, M'dir. Benzer biçimde M'nin ataları yalnız ebeveyni H değil, köke kadar bütün yoldur: H, D, A.
Şekil 9.7 aynı ağacı, terimleri adım adım gösteren sunum çiziminin dört karesiyle verir. Ağaçta 13 düğüm vardır: A, B, C, D, E, F, G, H, I, J, K, L, M. Yapraklar çocuğu olmayan yedi düğümdür: K, L, F, G, M, I, J. İç düğümler en az bir çocuğu olan altı düğümdür: A, B, C, D, E, H. Sayılar tutar: 7 + 6 = 13. Kenar sayısı da Teorem 9.1'e göre 12'dir.
9.1.6Derece, derinlik, seviye ve yükseklik
Ağaçla ilgili hemen her maliyet hesabı birkaç sayısal ölçüye dayanır. Bunların en temeli, bir düğümün ne kadar dallandığını ve kökten ne kadar uzakta olduğunu söyleyen ölçülerdir.
- Bir düğümün derecesi (degree), çocuklarının sayısıdır; başka bir deyişle düğümün altına asılan alt ağaçların sayısıdır. Ağacın derecesi, düğümlerin derecelerinin en büyüğüdür.
- Bir düğümün derinliği (depth), kökten o düğüme inen yoldaki kenar sayısıdır. Kökün derinliği 0'dır.
- Aynı derinlikteki düğümlerin kümesine bir seviye (level) denir. Derinliği d olan düğümler d. seviyeyi oluşturur.
- Bir düğümün yüksekliği (height), o düğümden altındaki en uzak yaprağa inen yoldaki kenar sayısıdır. Yaprağın yüksekliği 0'dır.
- Ağacın yüksekliği, kökün yüksekliğidir. Bu değer, düğümlerin derinliklerinin en büyüğüne eşittir.
Derinlik ile yükseklik ters yönlere bakar. Derinlik yukarıya, köke doğru ölçülür: “Bu düğüm kökten ne kadar aşağıda?” Yükseklik aşağıya, yapraklara doğru ölçülür: “Bu düğümün altında ne kadar uzun bir dal var?” Kökün derinliği her zaman 0'dır; yüksekliği ise ağacın yüksekliğidir. Yaprakların yüksekliği her zaman 0'dır; derinlikleri ise yaprağın nerede durduğuna göre değişir.
| Ölçü | Tanım | Örnek |
|---|---|---|
| Derece | Düğümün çocuk sayısı | D → 3 |
| Derinlik | Kökten düğüme kenar sayısı; kökte 0 | E → 2 (A→B→E) |
| Seviye | Aynı derinlikteki düğümler | Seviye 2: E, F, G, H, I, J |
| Düğüm yüksekliği | Düğümden en uzak yaprağa kenar sayısı; yaprakta 0 | D → 2 (D→H→M) |
| Ağacın yüksekliği | Kökün yüksekliği = en büyük derinlik | 3 |
Bu notta derinlik ve yükseklik kenar sayısıyla ölçülür. Bazı kitaplar bunun yerine yol üzerindeki düğüm sayısını kullanır ve seviyeleri 1'den başlatır. O zaman kökün derinliği 1, yaprağın yüksekliği 1 olur; bütün değerler bir fazla çıkar. İki tanım da tutarlıdır, ama karıştırılırsa sonuçlar bir kayar. Kaynaklar karşılaştırılırken hangi tanımın kullanıldığına bakılmalıdır.
Şekil 9.8'deki ağaçta her düğümün derinliğini ve yüksekliğini bulalım.
- Derinlikler yukarıdan aşağı. Kök A'nın derinliği 0'dır. Çocukları B, C, D, E'nin derinliği 1'dir. Bir alt satırdaki F, G, H, I, J'nin derinliği 2'dir. En alttaki K'nin derinliği 3'tür.
- Yükseklikler aşağıdan yukarı. Yapraklar C, F, G, H, I, K'nin yüksekliği 0'dır. Bir iç düğümün yüksekliği, çocuklarının yüksekliklerinin en büyüğünün bir fazlasıdır. J'nin tek çocuğu K'dir: 1 + 0 = 1. B'nin çocukları F ve G'dir: 1 + max(0, 0) = 1. D'nin tek çocuğu H'dir: 1. E'nin çocukları I (0) ve J'dir (1): 1 + max(0, 1) = 2.
- Kök. A'nın çocuklarının yükseklikleri B 1, C 0, D 1, E 2'dir. A'nın yüksekliği 1 + 2 = 3'tür. Ağacın yüksekliği de 3'tür ve en derin düğüm K'nin derinliğine eşittir.
| Düğüm | A | B | C | D | E | F | G | H | I | J | K |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Yükseklik | 3 | 1 | 0 | 1 | 2 | 0 | 0 | 0 | 0 | 1 | 0 |
| Derinlik | 0 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 3 |
Tablodaki iki satır farklı bilgiler taşır. Derinliği bulmak için yalnız düğümün atalarına bakmak yeter; yükseklik ise düğümün bütün soyuna bakmayı gerektirir. Bu fark kodda da görülür: derinlik hesabı ebeveyn zinciri boyunca yukarı çıkar, yükseklik hesabı çocuklar üzerinden aşağı iner.
Bir düğümün yüksekliğini ağacın yüksekliğinden kendi derinliğini çıkararak bulmaya çalışmak. Bu yalnız düğüm en uzun dal üzerindeyse doğrudur. Şekil 9.8'deki B'nin derinliği 1, ağacın yüksekliği 3'tür; ama B'nin yüksekliği 3 − 1 = 2 değil, 1'dir. Çünkü B'nin altındaki en uzun dal yalnız bir kenar uzunluğundadır. Yükseklik her zaman düğümün kendi alt ağacına bakılarak hesaplanır.
9.1.7Yol ve alt ağaç
Kenarlar boyunca bir düğümden diğerine yürümek bir yol (path) oluşturur. Biçimsel olarak bir yol, düğümler ve kenarlar arasında gidip gelen bir dizidir:
p = (v0, e1, v1, e2, …, ek, vk)
Burada her ei kenarı vi−1 ile vi düğümlerini bağlar. Yolun başı start(p) = v0, sonu end(p) = vk, uzunluğu da kenar sayısı olan length(p) = k'dir. Hiçbir düğümün tekrar etmediği yola basit yol denir. Derinlik, kökten düğüme inen yolun uzunluğudur; yükseklik de düğümden en uzak yaprağa inen yolun uzunluğudur.
Bir ağaçta herhangi iki düğüm arasında tam olarak bir basit yol vardır.
Varlık: Her iki düğümden de ebeveyn zinciri boyunca köke çıkılabilir. İki zincirin ilk ortak düğümü c olsun. Birinci düğümden c'ye çıkıp oradan ikinci düğüme inmek bir basit yol verir. Teklik: İki düğüm arasında iki farklı basit yol bulunsaydı, bu yollar bir noktada ayrılıp daha sonra yeniden birleşirdi. Ayrılma noktasından birleşme noktasına bir yoldan gidip öteki yoldan dönmek bir döngü oluştururdu. Ağaçta döngü olmadığından ikinci bir yol bulunamaz. □
Şekil 9.9 ispattaki inşayı somut olarak gösterir. K'nin ataları E, B, A; M'nin ataları H, D, A'dır. İki zincirin ilk ortak düğümü A'dır. Yol K'den A'ya üç kenarla çıkar, A'dan M'ye üç kenarla iner; toplam uzunluk 6'dır. Bu iki düğüm arasında başka bir yol yoktur. Dosya sistemindeki “bir dosyanın tek bir tam yolu vardır” gözlemi de bu teoremin özel bir durumudur.
Alt ağaç (subtree), bir düğüm ile onun bütün soyundan oluşan yapıdır. Alt ağaç kendi başına bir ağaçtır: kökü seçilen düğümdür, içindeki her düğümün ebeveyni yine alt ağacın içindedir. Şekil 9.7'nin (d) karesindeki kesikli çerçeve, D'nin alt ağacını gösterir: D, H, I, J ve M. Dosya sisteminde bir klasör ile altındaki her şey bir alt ağaçtır. Bir klasörü kopyalamak ya da silmek, aslında bir alt ağacı kopyalamak ya da silmektir.
Alt ağaç kavramı, ağaç algoritmalarının özyinelemeli yazılmasının temelidir. Bir düğümün çocukları, kendi alt ağaçlarının kökleridir. Bir ağaç üzerinde yapılacak bir iş çoğu zaman şöyle tarif edilir: aynı işi her alt ağaçta yap, sonuçları birleştir, kökü de hesaba kat. Yüksekliğin tanımı bunun ilk örneğidir: bir düğümün yüksekliği, alt ağaçlarının yüksekliklerinin en büyüğünün bir fazlasıdır.
9.1.8Temel ağaç türleri
Ağaç tanımı bir düğümün çocuk sayısına sınır koymaz. Uygulamada ise çocuk sayısını sınırlamak ya da çocuklara bir düzen getirmek çoğu zaman işleri kolaylaştırır. Tablo 9.4 bu derste karşılaşılan dört türü özetler.
| Tür | Özellik |
|---|---|
| Genel ağaç | Bir düğümün çocuk sayısına sınır yoktur. |
| k-lı ağaç (k-ary tree) | Her düğümün en çok k çocuğu vardır. |
| İkili ağaç (binary tree) | Her düğümün en çok iki çocuğu vardır: sol ve sağ. |
| İkili arama ağacı (binary search tree, BST) | İkili ağaçtır; ayrıca her düğümde sol alt ağaçtaki anahtarlar düğümünkinden küçük, sağ alt ağaçtakiler büyüktür. |
Türler birbirinin içine geçer. Her ikili arama ağacı bir ikili ağaçtır, her ikili ağaç k = 2 için bir k-lı ağaçtır, her k-lı ağaç da bir genel ağaçtır. Kısıt arttıkça yapı daha az esnek, ama üzerinde çalışmak daha kolay olur. Örneğin ikili arama ağacında bir anahtarı aramak için her düğümde yalnız bir çocuğa inmek yeter: aranan anahtar küçükse sola, büyükse sağa gidilir. Şekil 9.10'un sağındaki ağaçta 6 aranırken 8'den sola, 3'ten sağa inilir ve iki karşılaştırmada 6'ya ulaşılır.
Bu notun ağırlığı ikili ağaçlardadır. Genel ağaçlar da önemlidir, ama her genel ağaç bir ikili ağaç biçiminde saklanabilir; bu dönüşüm gösterimler kesiminde anlatılır. İkili arama ağaçlarının ekleme, silme ve dengeleme işlemleri ise kendi başına geniş bir konudur; burada yalnız tanım düzeyinde anılır.
9.1.9Ağaç ADT'si
Ağacı kodda kullanmadan önce ona hangi işlemlerle erişileceğini belirlemek gerekir. Yığın ve kuyrukta olduğu gibi burada da işlemleri gerçeklemeden ayırmak işe yarar. Yapının ne yaptığını söyleyen tarife soyut veri tipi (abstract data type, ADT) denir. Aşağıdaki Java arayüzü, Goodrich, Tamassia ve Goldwasser'in ağaç ADT'sinin Türkçe adlarla yazılmış hâlidir.
public interface Agac<E> extends Iterable<E> {
Konum<E> kok();
Konum<E> ebeveyn(Konum<E> p);
Iterable<Konum<E>> cocuklar(Konum<E> p);
int cocukSayisi(Konum<E> p);
boolean icMi(Konum<E> p); // isInternal
boolean yaprakMi(Konum<E> p); // isExternal
boolean kokMu(Konum<E> p); // isRoot
int size();
boolean isEmpty();
Iterable<Konum<E>> konumlar(); // positions
}
// Konum<E> (Position): E getElement();Arayüzün merkezinde konum (position) kavramı vardır. Konum<E>, ağaçtaki bir yeri temsil eden küçük bir nesnedir; tek bir işlemi vardır: getElement(), o yerdeki öğeyi döndürür. Kullanıcı ağacın içindeki düğüm nesnelerini doğrudan görmez, onlara referans tutamaz, alanlarını değiştiremez. Yalnız konumlar üzerinden ADT metotlarını çağırır. Böylece ağacın bellekte bağlantılarla mı yoksa bir diziyle mi tutulduğu kullanıcıdan gizlenir. Gerçekleme değişse bile ağacı kullanan kod değişmez.
Metotlar üç grupta toplanabilir.
- Gezinme metotları.
kok()kökün konumunu döndürür (ağaç boşsanull).ebeveyn(p),p'nin ebeveyninin konumunu döndürür;pköksenulldöner.cocuklar(p)çocukların konumlarını sırayla verir; dönüş tipiIterableolduğu için sonuç doğrudan birfor-eachdöngüsünde kullanılabilir.cocukSayisi(p)çocukların sayısını, yanip'nin derecesini verir. - Sorgu metotları.
icMi(p),yaprakMi(p)vekokMu(p)bir konumun iç düğüm, yaprak ya da kök olup olmadığını söyler. Yorumlardaki İngilizce adlar (isInternal,isExternal,isRoot) kaynak kitaptakilerdir. - Genel metotlar.
size()düğüm sayısını,isEmpty()ağacın boş olup olmadığını verir.konumlar()bütün konumları,iterator()ise (Iterable<E>'den gelir) bütün öğeleri bir sırayla dolaşır.
İkili ağaç ADT'si bu arayüzü genişletir ve üç metot ekler: sol(p), sag(p) ve kardes(p). İlk ikisi p'nin sol ve sağ çocuğunu (yoksa null), sonuncusu p'nin kardeşini döndürür. Kaynak kitapta bu metotlar left, right ve sibling adını taşır.
p B düğümünün konumudur. ebeveyn(p) kök A'yı, cocuklar(p) D ve E'yi verir. D bir yaprak, A köktür; E'nin tek çocuğu G, C'nin tek çocuğu F'dir.Şekil 9.11 üzerinde birkaç çağrının sonucunu yazalım. cocukSayisi(p) 2 döndürür. icMi(p) true, yaprakMi(p) false, kokMu(p) false döndürür. kok() A'nın konumunu verir; ebeveyn(kok()) ise null'dır. Ağaçta yedi düğüm olduğu için size() 7 döndürür.
9.1.10Kod izleme: derinlik ve yükseklik
ADT metotlarıyla, ağacın nasıl gerçeklendiğini bilmeden derinlik ve yükseklik hesaplanabilir. İki metot da tanımlarını neredeyse kelimesi kelimesine koda çevirir.
int derinlik(Konum<E> p) {
if (kokMu(p)) return 0;
return 1 + derinlik(ebeveyn(p));
}
int yukseklik(Konum<E> p) {
int h = 0;
for (Konum<E> c : cocuklar(p))
h = Math.max(h, 1 + yukseklik(c));
return h;
}derinlik(p) tanımı izler: kökün derinliği 0'dır; kök olmayan bir düğümün derinliği, ebeveyninin derinliğinin bir fazlasıdır. Metot p'den başlayıp ebeveyn zinciri boyunca köke kadar çıkar. Her çağrı bir kenar yukarı çıktığı için p'nin derinliği d ise metot d + 1 kez çağrılır; zaman O(d + 1)'dir. Çağrı yığınında da en çok d + 1 çerçeve birikir.
yukseklik(p) de tanımı izler, ama aşağı bakar. h 0'dan başlar. Döngü p'nin her çocuğu c için 1 + yukseklik(c) değerini hesaplar ve h'yi bu değerle büyütür. Çocuğu olmayan bir düğümde döngü hiç dönmez ve 0 döner; bu, yaprağın yüksekliğidir. Metot p'nin alt ağacındaki her düğüm için bir kez çağrılır. Bir düğümün çocuklarını dolaşmanın maliyeti çocuk sayısıyla orantılıdır; bütün düğümlerin çocuk sayıları toplamı da kenar sayısı kadardır. Bu yüzden yukseklik(p), alt ağaçta np düğüm varsa O(np) sürer.
İzlemeyi Şekil 9.11'deki ağaç üzerinde yapalım: önce derinlik(G), sonra yukseklik(B). Tablo 9.5 her adımda çağrı yığınını (alttan üste) ve dönen değerleri verir.
| Adım | Çalışan çağrı | Çağrı yığını | Ne olur? |
|---|---|---|---|
| 1 | derinlik(G) | main, derinlik(G) | G kök değil; ebeveyni E için derinlik(E) çağrılır. |
| 2 | derinlik(E) | …, derinlik(E) | E kök değil; derinlik(B) çağrılır. |
| 3 | derinlik(B) | …, derinlik(B) | B kök değil; derinlik(A) çağrılır. |
| 4 | derinlik(A) | main, G, E, B, A (5 çerçeve) | A kök: taban durum, 0 döner. |
| 5 | derinlik(B) | main, G, E, B | 1 + 0 = 1 döner. |
| 6 | derinlik(E) | main, G, E | 1 + 1 = 2 döner. |
| 7 | derinlik(G) | main, G | 1 + 2 = 3 döner. G'nin derinliği 3'tür (A–B–E–G). |
| 8 | yukseklik(B) | main, yukseklik(B) | h = 0; çocuklar D, E. İlk çocuk D için özyineleme. |
| 9 | yukseklik(D) | …, B, D | D'nin çocuğu yok; döngü dönmez, 0 döner. |
| 10 | yukseklik(B) | main, B | h = max(0, 1 + 0) = 1; sıradaki çocuk E. |
| 11 | yukseklik(E) | …, B, E | h = 0; tek çocuk G için özyineleme. |
| 12 | yukseklik(G) | …, B, E, G | G yaprak: 0 döner. |
| 13 | yukseklik(E) | …, B, E | h = max(0, 1 + 0) = 1; çocuk kalmadı, 1 döner. |
| 14 | yukseklik(B) | main, B | h = max(1, 1 + 1) = 2; çocuk kalmadı, 2 döner. |
derinlik(A) taban durumdadır; G, E, B çerçeveleri yığında bekler. (b) Değerler geri döner: B 1, E 2, G 3. (c) yukseklik(B) D'den 0 almış, E'nin alt ağacına inmiştir. (d) G'den 0, E'den 1 gelmiş; yukseklik(B) = 2.İzleme iki metodun simetrisini açıkça gösterir. derinlik yukarı çıkarken çağrı yaparsa, değerler aşağı inerken hesaplanır: önce kök 0 der, sonra her çerçeve aldığı değere 1 ekler. yukseklik aşağı inerken çağrı yaparsa, değerler yukarı çıkarken hesaplanır: önce yapraklar 0 der, her iç düğüm çocuklarından gelenlerin en büyüğüne 1 ekler. yukseklik(B) dört kez çağrılmıştır: B, D, E ve G için. Bunlar B'nin alt ağacındaki düğümlerin tamamıdır; ağacın geri kalanına (A, C, F) hiç dokunulmamıştır.
Ağacın yüksekliği iki yolla hesaplanabilir. Birincisi yukseklik(kok()) çağrısıdır ve O(n) sürer. İkincisi, bütün düğümlerin derinliklerini derinlik ile hesaplayıp en büyüğünü almaktır. Bu ikinci yol her düğüm için ayrı bir yukarı tırmanış yapar. Derinliği dp olan düğüm için maliyet O(dp + 1) olduğundan toplam maliyet O(n + Σ dp) olur. Zincir biçimli bir ağaçta derinlikler 0, 1, …, n − 1'dir ve toplamları n(n − 1)/2'ye ulaşır; maliyet O(n2)'ye çıkar. Doğru yöntem birincisidir.
yukseklik metodunda h'yi 0 yerine 1 ile başlatmak ya da yaprakta 1 döndürmek. O zaman metot yüksekliği kenar değil düğüm sayısıyla ölçer ve bütün değerler bir fazla çıkar. Tanım kenar sayısıyla yapıldıysa yaprak 0 döndürmelidir. Benzer biçimde derinlik'te taban durumu unutmak (kokMu(p) denetimi olmadan ebeveyn(p) çağırmak) kökte null bir konumla devam edip hataya yol açar.
9.2İkili ağaçlar
Genel ağaçta bir düğümün kaç çocuğu olacağı bilinmez. Bu esneklik, ağacı saklayan kodu da karmaşıklaştırır: her düğüm değişken uzunlukta bir çocuk listesi tutmak zorunda kalır. Çocuk sayısı en çok ikiyle sınırlanınca hem saklama hem de algoritmalar sadeleşir. İkili ağaçlar bu yüzden ağaçların en çok kullanılan biçimidir. İfade ağaçları, ikili arama ağaçları, heap yapıları ve Huffman kodlama ağaçları ikili ağaç üzerine kuruludur.
9.2.1İkili ağacın tanımı
İkili ağaçta her düğümün en çok iki çocuğu vardır. Ama asıl ayırt edici özellik bu sayı değil, çocukların adlandırılmasıdır. Her çocuk ya sol çocuk ya da sağ çocuktur. Tek çocuklu bir düğümde bile o çocuğun solda mı sağda mı olduğu bilinir ve önemlidir.
Bir ikili ağaç (binary tree) ya boştur ya da bir kök düğüm ile iki ikili ağaçtan oluşur. Bu iki ikili ağaca kökün sol alt ağacı ve sağ alt ağacı denir. Sol alt ağaç boş değilse kökü, ana kökün sol çocuğudur; sağ alt ağaç için de aynısı geçerlidir.
Tanım özyinelemelidir: ikili ağacı yine ikili ağaçlar cinsinden tarif eder. Bu bir döngü değildir, çünkü her adımda alt ağaçlar daha küçüktür ve sonunda boş ağaca varılır. Boş ağaç özyinelemenin taban durumudur (base case). Tanımın bu biçimi, ikili ağaç metotlarının yazılışını da belirler. Metot boş ağaçta bir değer döndürür. Boş olmayan ağaçta kendini sol ve sağ alt ağaca uygular, sonra sonuçları birleştirir.
Şekil 9.13 bu farkı gösterir. İki çizimde de A'nın tek bir çocuğu, B vardır. Genel ağaç gözüyle bakılırsa iki yapı aynıdır: bir kök ve onun tek çocuğu. İkili ağaç gözüyle bakılırsa farklıdır: birinde sol alt ağaç dolu, sağ alt ağaç boştur; ötekinde tersi. Ayrımın pratik bir anlamı vardır. Örneğin ikili arama ağacında sol, küçük anahtarlar demektir. Kök ortada gezinme de sol ve sağı ayırır; iki ağaç için farklı çıktılar verir: soldaki için B A, sağdaki için A B.
İkili ağacın düğümleri dengeli dağılmak zorunda değildir. Şekil 9.14'ün solundaki yapıda her düğümün yalnız sol çocuğu vardır ve ağaç bir zincire dönüşmüştür. Böyle ağaçlara çarpık (skewed) ağaç denir. Çarpık ağaç da geçerli bir ikili ağaçtır, ama ikili ağacın sağladığı avantajların çoğunu kaybeder. Dört düğümlü zincirin yüksekliği 3'tür; aynı dört düğüm dengeli dizilseydi yükseklik 2 olurdu. Fark küçük ağaçta önemsiz görünür, ama bir milyon düğümde yaklaşık 20 ile bir milyon arasındaki farka dönüşür.
9.2.2Dolu, tam ve mükemmel ikili ağaç
İkili ağaçların biçimini anlatmak için üç özel ad kullanılır. Bu adlar birbirine çok yakın olduğu için en çok karıştırılan kavramlar arasındadır. Türkçe kaynaklarda “dolu” ve “tam” sözcükleri yer yer ters anlamda da kullanılır. Karışıklığı önlemenin en güvenli yolu İngilizce karşılıkları esas almaktır: full, complete ve perfect.
- Dolu ikili ağaç (full binary tree): Her düğümün ya 0 ya da 2 çocuğu vardır. Tek çocuklu düğüm yoktur.
- Tam ikili ağaç (complete binary tree): Son seviye dışındaki bütün seviyeler tamamen doludur; son seviyedeki düğümler soldan sağa boşluksuz yerleşir.
- Mükemmel ikili ağaç (perfect binary tree): Bütün iç düğümlerin iki çocuğu vardır ve bütün yapraklar aynı derinliktedir.
Üç tanım farklı şeylere bakar. Dolu olmak, her düğümün kendi çocuk sayısıyla ilgili yerel bir koşuldur; ağacın genel şekline bakmaz. Bu yüzden dolu bir ağaç çok dengesiz olabilir: her adımda sağ çocuğu bir yaprak olan, sol çocuğu ise yeniden ikiye dallanan bir ağaç doludur ama bir zincire çok benzer. Tam olmak, ağacın seviyelerine bakan genel bir koşuldur; tek çocuklu bir düğüme izin verir, yeter ki bu düğüm son seviyenin hemen üstünde ve doğru konumda dursun. Mükemmel olmak ise en güçlü koşuldur: ağaç, yüksekliğinin izin verdiği bütün düğümleri içerir.
Tam ağacı anlamanın pratik bir yolu, düğümleri seviye seviye ve her seviyede soldan sağa numaralamaktır. Kök 1, onun çocukları 2 ve 3, bir alt seviye 4, 5, 6, 7 numarasını alır. Bir ikili ağacın n düğümü 1'den n'ye kadar bütün numaraları boşluksuz dolduruyorsa ağaç tamdır. Arada bir numara boş kalıyorsa, örneğin 10 boşken 11 doluysa, ağaç tam değildir. Bu numaralama, dizi gösteriminin de temelidir.
Tanımlar arasındaki ilişkiler şöyle özetlenebilir. Mükemmel bir ağaç hem dolu hem tamdır: iç düğümlerin hepsinde iki çocuk vardır ve bütün seviyeler, son seviye de dahil, doludur. Dolu ile tam ise birbirini gerektirmez. Dolu ama tam olmayan, tam ama dolu olmayan, ne dolu ne tam olan ağaçlar vardır.
“Dolu” ile “tam”ı aynı sanmak ya da birinin ötekini gerektirdiğini düşünmek. Dolu, her düğümün 0 ya da 2 çocuğu olmasıdır; tam, son seviye dışındaki seviyelerin dolu ve son seviyenin soldan dolması demektir. Tam bir ağaçta tek çocuklu bir düğüm bulunabilir; dolu bir ağaçta da üst seviyelerde boşluklar olabilir. Bir ağacın iki özelliği de ayrı ayrı denetlenmelidir.
9.2.3Dört ağaç üzerinde ayrım
Tanımları sınamanın en iyi yolu, onları somut ağaçlara uygulamaktır. Şekil 9.16 dört ağaç verir; her biri için üç özellik ayrı ayrı denetlenir.
Şekil 9.16'daki her ağaç dolu mu, tam mı, mükemmel mi?
- (a) Çocuk sayıları: A 2, B 2, E 2, C, D, H, I 0. Tek çocuklu düğüm yoktur; ağaç doludur. Seviyelere bakalım: 0. seviyede A, 1. seviyede B ve C, 2. seviyede yalnız D ve E vardır. C'nin altındaki iki konum boştur. Buna karşın 3. seviyede H ve I bulunur. Son seviyeden önceki seviye dolu olmadığı için ağaç tam değildir. Yapraklar farklı derinliklerde olduğu için (C 1'de, D 2'de, H ve I 3'te) mükemmel de değildir.
- (b) Düğümler 1'den 10'a kadar boşluksuz numaralanmıştır: 0., 1. ve 2. seviyeler doludur; son seviyede 8, 9, 10 soldan sağa dizilmiştir. Ağaç tamdır. Ancak 5'in yalnız bir çocuğu (10) vardır; ağaç dolu değildir. Dolu olmadığı için mükemmel de değildir.
- (c) 5'in tek çocuğu vardır: 11 numaralı sağ çocuk. Ağaç dolu değildir. Numaralamada 10 boşken 11 doludur; son seviye soldan sağa boşluksuz dolmamıştır. Ağaç tam da değildir. Ne dolu ne tam olan bir ağaç mükemmel de olamaz.
- (d) Bütün iç düğümlerin (1, 2, 3) iki çocuğu vardır ve bütün yapraklar (4, 5, 6, 7) aynı derinliktedir. Ağaç mükemmeldir; dolayısıyla hem dolu hem tamdır.
| Ağaç | Dolu | Tam | Mükemmel |
|---|---|---|---|
| (a) | evet | hayır | hayır |
| (b) | hayır | evet | hayır |
| (c) | hayır | hayır | hayır |
| (d) | evet | evet | evet |
Tablonun ilk iki satırı, dolu ile tamın bağımsız olduğunun somut kanıtıdır: (a) dolu ama tam değil, (b) tam ama dolu değil. (b) ile (c) arasındaki fark da öğreticidir. İki ağaçta da 5 tek çocukludur. (b)'de bu çocuk sol konumdadır (10) ve numaralar boşluksuz devam eder. (c)'de çocuk sağ konumdadır (11) ve 10 boş kalır. Tek bir düğümün sol yerine sağda durması, ağacı tam olmaktan çıkarmıştır.
9.2.4Düğüm sayısı ve yükseklik bağıntıları
İkili ağaçta her düğüm en çok iki çocuk taşıdığından, her seviyedeki düğüm sayısı bir öncekinin en çok iki katı olabilir. Bu basit gözlem, düğüm sayısı ile yükseklik arasındaki bütün bağıntıların kaynağıdır. Tablo 9.7 bu bağıntıları toplar; ardından her birinin gerekçesi verilir.
| Özellik | Formül | Örnek |
|---|---|---|
| d derinliğindeki en çok düğüm | 2d | d = 3 → en çok 8 düğüm |
| Yüksekliği h olan ağaçta en çok düğüm | 2h+1 − 1 | h = 3 → 15 (mükemmel ağaç) |
| Yüksekliği h olan ağaçta en az düğüm | h + 1 | çarpık (zincir) ağaç |
| n düğümlü ağacın yüksekliği | ⌊log2 n⌋ ≤ h ≤ n − 1 | n = 15 → 3 ≤ h ≤ 14 |
| n düğümlü ağaçta kenar sayısı | n − 1 | her düğümün tek ebeveyni |
| Dolu ikili ağaçta yaprak sayısı | iç düğüm sayısı + 1 | 7 düğüm → 3 iç, 4 yaprak |
Bir ikili ağaçta (a) d derinliğinde en çok 2d düğüm bulunur; (b) yüksekliği h olan ağaçta düğüm sayısı n için h + 1 ≤ n ≤ 2h+1 − 1 geçerlidir; (c) n ≥ 1 düğümlü ağacın yüksekliği ⌊log2 n⌋ ≤ h ≤ n − 1 aralığındadır.
(a) Tümevarımla: 0. derinlikte yalnız kök vardır, 20 = 1. d derinliğinde en çok 2d düğüm varsa, her biri en çok iki çocuk taşıdığı için d + 1 derinliğinde en çok 2 · 2d = 2d+1 düğüm olabilir.
(b) Üst sınır: Yüksekliği h olan ağaçta derinlikler 0'dan h'ye kadardır. (a)'ya göre toplam en çok 1 + 2 + 4 + … + 2h = 2h+1 − 1'dir. Alt sınır: Yükseklik h ise kökten en derin yaprağa h kenarlı bir yol vardır ve bu yol h + 1 düğüm içerir.
(c) (b)'deki iki eşitsizlik h için çözülür. h + 1 ≤ n'den h ≤ n − 1 çıkar. n ≤ 2h+1 − 1'den n < 2h+1, yani log2 n < h + 1 çıkar. h tam sayı olduğundan h ≥ ⌊log2 n⌋ olur. □
Üst sınırdaki toplam, ikinin ardışık kuvvetlerinin toplamıdır ve her zaman bir sonraki kuvvetten bir eksiktir: 1 + 2 + 4 + 8 = 15 = 16 − 1. Bu sınıra eşitlik yalnız mükemmel ağaçta ulaşılır. Yüksekliği 3 olan mükemmel ağaç 15 düğüm içerir; yüksekliği 4 olan 31 düğüm içerir. Alt sınıra ise her düğümün tek çocuklu olduğu zincirde ulaşılır: yüksekliği 3 olan zincir 4 düğümlüdür.
1000 düğümlü bir ikili ağacın yüksekliği en az ve en çok kaç olabilir?
- En çok. Her düğümün tek çocuğu olduğu zincirde yükseklik n − 1 = 999'dur.
- En az. Yüksekliği h olan bir ağaçta en çok 2h+1 − 1 düğüm bulunur. h = 8 için bu değer 511'dir; 1000 düğüme yetmez. h = 9 için 1023'tür; yeter. En küçük yükseklik 9'dur. Formülle: ⌊log2 1000⌋ = ⌊9,97⌋ = 9.
- Yorum. Kökten yaprağa inen bir işlem, dengeli ağaçta yaklaşık 10, zincir biçimli ağaçta yaklaşık 1000 adım sürer.
Bu bağıntıların neden önemli olduğu son satırda görülür. Ağaç üzerindeki birçok işlem, örneğin bir anahtarı aramak ya da yeni bir düğümü uygun yere eklemek, kökten bir yaprağa doğru tek bir yol boyunca ilerler. Böyle işlemlerin maliyeti O(h)'dir. Dengeli bir ağaçta h ≈ log2 n olduğundan bir milyon düğümlü ağaçta yaklaşık 20 seviye vardır (220 ≈ 106). Çarpık ağaçta ise h = n − 1'dir ve aynı işlem bir bağlı listede arama kadar yavaşlar. Arama ağaçlarının dengelenmesi bu farkı kapatmak için yapılır.
Yüksekliği h olan ağaçta en çok düğüm sayısını 2h ya da 2h − 1 yazmak. 2h, yalnız en alt seviyedeki (derinlik h) düğüm sayısının sınırıdır. Bütün seviyeler toplanınca 2h+1 − 1 bulunur. 2h − 1 değeri ise yüksekliği düğüm sayısıyla ölçen kaynaklardaki formüldür; iki tanım karıştırıldığında ortaya çıkar.
9.2.5Dolu ikili ağaçta yaprak ve iç düğüm sayısı
Dolu ikili ağaçların ilginç bir sayma özelliği vardır: yaprak sayısı her zaman iç düğüm sayısından tam bir fazladır. Tablo 9.7'deki son satır bunu 7 düğümlü mükemmel ağaçla örnekler: 3 iç düğüm (kök ve onun iki çocuğu), 4 yaprak.
Boş olmayan dolu bir ikili ağaçta iç düğüm sayısı i, yaprak sayısı y ise y = i + 1'dir. Dolayısıyla düğüm sayısı n = i + y = 2i + 1 her zaman tektir.
İspatı en sezdiren yol, her dolu ikili ağacın tek bir düğümden başlayıp aynı işlemin tekrarıyla elde edilebildiğini görmektir. Dolu ağaçta bir yaprağa tek çocuk eklenemez; ağaç dolu kalsın diye her seferinde bir yaprağa iki çocuk birden eklenir. Şekil 9.17 bu büyümeyi adım adım gösterir.
Ağaçtaki düğüm sayısı üzerinden tümevarım yapalım. Taban: Tek düğümlü ağaçta kök bir yapraktır: i = 0, y = 1, y = i + 1. Adım: Birden çok düğümlü dolu bir ağaçta iki çocuğu da yaprak olan bir iç düğüm vardır. En derin yaprağın ebeveyni böyledir. Dolu olma koşulu gereği bu yaprağın bir kardeşi bulunur; kardeşin çocuğu olsaydı en derin yaprak o olurdu, bu yüzden kardeş de yapraktır. Bu iki yaprak silinirse ebeveynleri yaprak olur ve yine dolu, daha küçük bir ağaç elde edilir. Bu küçük ağaçta y' = i' + 1'dir. İki yaprağı geri eklemek, bir yaprağı iç düğüme çevirir (i = i' + 1) ve yaprak sayısını y = y' − 1 + 2 = y' + 1 yapar. Fark değişmez: y − i = y' − i' = 1. □
Teoremin sonucu olan n = 2i + 1, dolu ikili ağaçta düğüm sayısının hiçbir zaman çift olamayacağını söyler. 6 düğümlü dolu ikili ağaç yoktur. Ayrıca düğüm sayısı biliniyorsa yaprak ve iç düğüm sayıları da bilinir: y = (n + 1)/2, i = (n − 1)/2. 15 düğümlü bir dolu ağaçta 8 yaprak ve 7 iç düğüm vardır.
Bu özellik birçok uygulamada doğrudan kullanılır. Bir aritmetik ifade ağacında her işleç iki işlenen alır; işleçler iç düğüm, sayılar yapraktır ve ağaç doludur. Dolayısıyla ikili işleçlerle yazılmış bir ifadede sayı adedi, işleç adedinden her zaman bir fazladır: (3 + 4) × (5 − 2) ifadesinde 3 işleç ve 4 sayı vardır. Huffman kodlamasında da her iç düğüm iki alt ağacı birleştirerek oluşur. k farklı karakter için kurulan ağacın k yaprağı ve k − 1 iç düğümü olur.
y = i + 1 eşitliğini her ikili ağaca uygulamak. Eşitlik yalnız dolu ağaçlarda geçerlidir. Tek çocuklu düğümler yaprak sayısını artırmadan iç düğüm sayısını artırır. Örneğin üç düğümlü bir zincirde 2 iç düğüm ve yalnız 1 yaprak vardır. Genel ikili ağaçta doğru bağıntı şudur: iki çocuklu düğümlerin sayısı i2 ise yaprak sayısı i2 + 1'dir; tek çocuklu düğümler hesaba girmez.
Kutudaki genel bağıntı da kenar sayılarak kolayca gösterilir. Tek çocuklu düğüm sayısı i1, iki çocuklu düğüm sayısı i2 olsun. Kenar sayısı bir yandan n − 1 = y + i1 + i2 − 1'dir. Her kenar bir çocuğa da iner; yani kenar sayısı i1 + 2i2'dir. İkisi eşitlenince y = i2 + 1 kalır. Dolu ağaçta i1 = 0 olduğu için i2 = i olur ve teorem yeniden elde edilir.
9.3Gösterimler
Ağaç kâğıt üzerinde düğümler ve çizgilerden oluşur; bellekte ise ya ardışık hücrelerden oluşan bir dizi ya da birbirini gösteren nesneler vardır. Bir ağacı bu iki araçtan biriyle temsil etmenin yollarına gösterim denir. Hangi gösterimin seçildiği, ebeveyn ve çocuklara ne kadar hızlı ulaşılacağını, ne kadar bellek harcanacağını ve ağacın ne kadar kolay büyütüleceğini belirler.
9.3.1Dizi gösterimi
Tam ağaçların numaralanması, ikili ağacı bir diziye yerleştirmenin doğal yolunu verir. Düğümler seviye seviye, her seviyede soldan sağa okunur ve bu sırayla dizinin hücrelerine yazılır. Kök ilk hücreye, onun çocukları sonraki iki hücreye, bir alt seviye sonraki dört hücreye gider. Bu düzende bir düğümün çocuklarının ve ebeveyninin indisi, düğümün kendi indisinden basit bir aritmetikle bulunur.
1 tabanlı (kök a[1]'de) | 0 tabanlı (kök a[0]'da) | |
|---|---|---|
| Sol çocuk | 2i | 2i + 1 |
| Sağ çocuk | 2i + 1 | 2i + 2 |
| Ebeveyn | ⌊i / 2⌋ | ⌊(i − 1) / 2⌋ |
1 tabanlı düzende formüller daha sadedir; bu yüzden birçok kitap dizinin 0. hücresini boş bırakır ve kökü 1. hücreye koyar. Formüllerin nedeni şöyle görülür. d derinliğindeki ilk düğüm 2d indisini alır, çünkü üstündeki seviyelerde toplam 2d − 1 düğüm vardır. Bir düğümden önce kendi seviyesinde j düğüm bulunuyorsa, bir alt seviyede onların 2j çocuğu bulunur. Bu sayımlar birleşince indis i'deki düğümün sol çocuğu 2i'ye düşer; sağ çocuk hemen yanındadır. Ebeveyn formülü bu ikisinin tersidir: hem 2i hem 2i + 1, 2'ye tam sayı bölmesiyle i'yi verir.
0 tabanlı formüller, 1 tabanlılardan indisleri bir kaydırarak elde edilir. 0 tabanlı indis i, 1 tabanlı i + 1'e karşılık gelir. Onun sol çocuğu 1 tabanlıda 2(i + 1) = 2i + 2'dir; 0 tabanlıya geri dönmek için bir çıkarılır ve 2i + 1 bulunur. Diğer formüller de aynı yolla çıkar.
Şekil 9.18'deki ağaçta E indis 5'tedir. Sol çocuğu 2 · 5 = 10, sağ çocuğu 11 indisinde olurdu. Dizide yalnız 9 düğüm bulunduğu için 10 ve 11 dizinin dışındadır; E'nin çocuğu yoktur, E bir yapraktır. E'nin ebeveyni ⌊5 / 2⌋ = 2, yani B'dir. Bu gösterimde düğümler referans taşımaz. Ebeveyn ve çocuk bilgisi, konumun kendisinde saklıdır; ulaşmak için birkaç aritmetik işlem yeterlidir ve her erişim O(1) sürer.
Dizi gösterimi en çok tam ikili ağaçlara yakışır. Tam ağaçta düğümler 1'den n'ye kadar bütün indisleri boşluksuz doldurur; dizi tam n hücre kullanır ve hiç boşluk kalmaz. İkili heap bu yüzden hemen her zaman bir dizide tutulur. Tam olmayan ağaçlarda ise boş konumlar dizide boş hücre olarak kalır; bunun bedeli aşağıda hesaplanır.
9.3.2Kod izleme: indis hesabı ve köke giden yol
Aşağıdaki kod 1 tabanlı formülleri üç küçük metoda çevirir ve onları kullanarak bir düğümden köke kadar bütün ataları yazdırır.
int sol(int i) { return 2 * i; }
int sag(int i) { return 2 * i + 1; }
int ebeveyn(int i) { return i / 2; }
void kokeYol(char[] a, int i) { // i'den köke
while (i >= 1) {
System.out.print(a[i] + " ");
i = ebeveyn(i);
}
}sol, sag ve ebeveyn tek satırlık metotlardır. ebeveyn'de i / 2 ifadesi Java'da tam sayı bölmesidir; sonucun ondalık kısmı atılır, yani aşağı yuvarlanır. Bu, ⌊i / 2⌋'nin ta kendisidir. kokeYol(a, i), i 1'den küçük olmadığı sürece döner. Her turda a[i]'yi yazdırır ve i'yi ebeveynin indisine çeker. Kökün indisi 1'dir; onun “ebeveyni” ⌊1 / 2⌋ = 0 çıkar ve döngü durur. 0. hücrenin boş bırakılması bu yüzden kullanışlıdır: 0 indisi “ebeveyn yok” anlamına gelir.
| Çağrı / adım | Hesap | Sonuç |
|---|---|---|
sol(2) | 2 · 2 = 4 | a[4] = D, B'nin sol çocuğu |
sag(2) | 2 · 2 + 1 = 5 | a[5] = E, B'nin sağ çocuğu |
ebeveyn(2) | 2 / 2 = 1 | a[1] = A, kök |
sol(4), sag(4) | 8 ve 9 | H ve I, D'nin çocukları |
sol(5), sag(5) | 10 ve 11 > n | dizinin dışında: E yapraktır |
ebeveyn(9) | 9 / 2 = 4 (4,5 aşağı yuvarlanır) | D; ebeveyn(8) de 4'tür |
kokeYol 1. tur | i = 9 ≥ 1 | I yazdırılır; i = 4 |
| 2. tur | i = 4 ≥ 1 | D yazdırılır; i = 2 |
| 3. tur | i = 2 ≥ 1 | B yazdırılır; i = 1 |
| 4. tur | i = 1 ≥ 1 | A yazdırılır; i = 0 |
| Döngü sonu | 0 ≥ 1 yanlış | çıktı: I D B A |
kokeYol(a, 9) bitmiştir: I, D, B ve A sırayla yazdırılmıştır.İzlemede iki nokta öne çıkar. Birincisi, bir indisin geçerli bir çocuk olup olmadığı dizi uzunluğuyla denetlenir: n düğümlü bir ağaçta 2i ≤ n ise sol çocuk vardır, 2i + 1 ≤ n ise sağ çocuk vardır. Bu denetim yapılmazsa dizinin sonundan sonraki bir hücre okunmaya çalışılır ve ArrayIndexOutOfBoundsException alınır. Tam olmayan ağaçlarda ayrıca hücrenin boş olup olmadığına da bakmak gerekir. İkincisi, kardeş düğümler aynı ebeveyni verir: 8 ve 9'un ikisi de 4'e gider. Tam sayı bölmesi, sağ çocuğun indisindeki fazladan 1'i siler.
kokeYol'un her turu indisi yaklaşık yarıya indirir. Döngü, düğümün derinliğinin bir fazlası kadar döner. Tam ağaçta derinlik en çok ⌊log2 n⌋ olduğundan maliyet O(log n)'dir. Bu sayının küçüklüğü, heap işlemlerinin neden hızlı olduğunu da açıklar: bir öğeyi yukarı taşımak, bu döngüdeki gibi en çok yükseklik kadar adım sürer.
1 tabanlı ve 0 tabanlı formülleri karıştırmak. Kök a[0]'daysa sol çocuk 2i değil, 2i + 1'dir; kökün “sol çocuğu” 2 · 0 = 0 çıkar ve kök kendini gösterir. 0 tabanlı dizide indis 6'daki düğümün ebeveyni ⌊(6 − 1) / 2⌋ = 2, sol çocuğu 2 · 6 + 1 = 13'tür; 1 tabanlı formüllerle bulunan 3 ve 12 yanlıştır. Kod yazılmadan önce hangi düzenin kullanıldığı açıkça belirlenmelidir.
9.3.3Dizide bellek israfı
Dizi gösteriminin zayıf yanı, ağacın şeklinden bağımsız olarak her konuma bir hücre ayırmasıdır. Bir düğümün indisi, düğümün ağaçtaki konumuyla belirlenir; öndeki konumların dolu olup olmadığına bakılmaz. Ağaç tam değilse aradaki boş konumlar dizide boş hücre olarak kalır.
Şekil 9.20'deki zincirde her düğüm bir öncekinin sol çocuğudur, bu yüzden her adımda indis iki katına çıkar: 1, 2, 4, 8, 16. Beş düğüm için 16 hücre gerekir ve 11'i boş kalır. Zincir sağa çarpık olsaydı durum daha da kötüleşirdi. Sağ çocuk indisi 2i + 1 olduğundan indisler 1, 3, 7, 15, 31 olur; beş düğüm için 31 hücre gerekir.
Genel olarak n düğümlü bir ikili ağaç, dizi gösteriminde 2n − 1 hücreye kadar yer isteyebilir. En kötü durum, yüksekliği n − 1 olan bir zincirdir; dizi bu yükseklikteki mükemmel ağacın bütün konumlarına, yani 2n − 1 hücreye yer açmak zorundadır. 30 düğümlü sağa çarpık bir zincir bir milyardan fazla hücre ister. Bu, üstel bir israftır ve dizi gösterimini şekli öngörülemeyen ağaçlar için kullanışsız yapar.
| Artılar | Eksiler |
|---|---|
| Referans tutulmaz; ebeveyn ve çocuk indisleri O(1)'de hesaplanır. | Seyrek ya da çarpık ağaçta büyük israf: 2n − 1 hücreye kadar. |
| Tam ağaçlarda boşluk kalmaz; bellek en sıkı biçimde kullanılır. | Dizi boyutu sabittir; ağaç büyüyünce yeni ve daha büyük bir diziye kopyalama gerekir. |
9.3.4Bağlı gösterim
İkinci yol, her düğümü ayrı bir nesne yapmak ve çocuklarını referanslarla göstermektir. Bu, bağlı listedeki düğüm fikrinin ağaca uyarlanmış hâlidir. Bağlı listede her düğüm bir sonraki düğümü gösterir; ikili ağaçta ise iki düğümü gösterir: sol çocuğu ve sağ çocuğu.
public class AgacDugumu {
int veri;
AgacDugumu sol; // sol çocuk (yoksa null)
AgacDugumu sag; // sağ çocuk (yoksa null)
public AgacDugumu(int veri) {
this.veri = veri; // sol = sag = null
}
}AgacDugumu sınıfının üç alanı vardır. veri düğümde saklanan değeri, sol ve sag ise çocuk düğümleri gösterir. Bir çocuk yoksa ilgili referans null'dır. Yapıcı yalnız veri'yi atar. Java'da atanmayan referans alanları varsayılan olarak null olduğu için yeni oluşturulan her düğüm başlangıçta bir yapraktır.
Küçük bir ağaç, düğümleri oluşturup referansları elle bağlayarak kurulabilir:
// 9 kökünde küçük bir ağaç kuralım
AgacDugumu kok = new AgacDugumu(9);
kok.sol = new AgacDugumu(2);
kok.sag = new AgacDugumu(3);
kok.sol.sol = new AgacDugumu(4);İlk satır kökü, 9 değerli düğümü oluşturur. Sonraki iki satır kökün sol ve sağ çocuğunu, 2 ve 3'ü ekler. Son satır kok.sol.sol zinciriyle 2'nin sol çocuğuna 4'ü yerleştirir. Ortaya çıkan ağaç dört düğümlüdür: 9 kök; 2 ve 3 onun çocukları; 4, 2'nin sol çocuğudur. 2'nin sağ çocuğu ile 3'ün iki çocuğu yoktur.
Bağlı gösterimde bellek yalnız var olan düğümler için ayrılır. Ağaç ne kadar çarpık olursa olsun n düğüm n nesne demektir. Yeni bir düğüm eklemek, bir nesne oluşturup uygun referansı ona yöneltmekten ibarettir; ekleme yeri biliniyorsa bu O(1) sürer. Bedeli ise erişimdedir. Bir düğüme ulaşmak için kökten başlayıp referansları izlemek gerekir; indisle doğrudan atlamak mümkün değildir. Ebeveyne çıkmak da ancak düğüme ayrıca bir ebeveyn referansı eklenirse mümkündür.
null referanslardır. Yaprakların (D, E, F, G) iki referansı da null'dır.Şekil 9.22 referansların nasıl dağıldığını sayma fırsatı verir. Yedi düğümün her birinde iki referans alanı vardır: toplam 14. Bunların 6'sı doludur ve her biri bir kenara karşılık gelir; Teorem 9.1'e göre yedi düğümlü ağaçta 6 kenar vardır. Kalan 8 referans null'dır. Sayıların bu dağılımı rastlantı değildir.
Sol ve sağ referanslı bağlı gösterimde, n ≥ 1 düğümlü her ikili ağaçta tam olarak n + 1 tane null çocuk referansı vardır.
Her düğümde iki referans vardır; toplam 2n. Dolu bir referans bir kenara karşılık gelir: ebeveynden çocuğa giden kenar. Kenar sayısı n − 1 olduğundan dolu referans sayısı da n − 1'dir. Geriye 2n − (n − 1) = n + 1 boş referans kalır. □
Teoremin pratik anlamı şudur: bağlı ikili ağaçta referans alanlarının yarısından fazlası her zaman boştur. 50 düğümlü bir ağaçta 100 referansın 51'i null'dır. Bu boş alanları değerlendirmek için geliştirilmiş yapılar da vardır. Örneğin “iplikli” (threaded) ikili ağaçlarda boş sol ve sağ referanslar, kök ortada sırasındaki önceki ve sonraki düğümü gösterecek biçimde kullanılır.
Bir düğümün alanlarına erişmeden önce null denetimini unutmak. kok.sol.sol ifadesi, kok.sol null ise NullPointerException fırlatır. Ağaç kodunda her referans izlenmeden önce null olup olmadığı düşünülmelidir. Özyinelemeli metotlarda bu denetim genellikle metodun ilk satırındaki taban durumla bir kez yapılır.
9.3.5Dizi mi, bağlı gösterim mi?
İki gösterimin güçlü ve zayıf yanları birbirini tamamlar. Tablo 9.11 bunları yan yana koyar.
| Ölçüt | Dizi gösterimi | Bağlı gösterim |
|---|---|---|
| Bellek | Tam ağaçta en az; seyrek ağaçta 2n − 1'e kadar israf | Düğüm başına 2 ek referans; israf yok |
| Çocuk / ebeveyn erişimi | O(1), indis aritmetiğiyle | Çocuk O(1); ebeveyn için ek referans gerekir |
| i. seviyedeki k. düğüm | O(1): indis 2i + k − 1 (1 tabanlı, k ≥ 1) | Kökten gezinme gerekir |
| Büyüme | Dizi dolunca kopyalanarak büyütülür | Yeni düğüm eklemek O(1) (konum biliniyorsa) |
| Uygun olduğu yer | Tam ikili ağaçlar (heap) | Genel ve değişken şekilli ağaçlar (BST, ifade ağacı) |
Tablodaki üçüncü satır, dizi gösteriminin bir başka avantajını gösterir. i. seviyenin ilk düğümü 2i indisindedir; o seviyedeki k. düğüm 2i + k − 1 indisine düşer. Şekil 9.18'deki ağaçta 2. seviyenin 3. düğümü 4 + 3 − 1 = 6 indisindeki F'dir. Bağlı gösterimde aynı düğümü bulmak için seviye seviye gezinmek gerekir.
Seçim için kısa bir kural yeter: ağacın şekli her zaman tam kalıyorsa dizi, şekli öngörülemiyorsa bağlı gösterim tercih edilir. Heap, ekleme ve silmelerde şeklini tam tutar; bu yüzden dizide saklanır. İkili arama ağacı ise eklenen anahtarlara göre herhangi bir şekil alabilir; bağlı gösterim kullanılır.
9.3.6Genel ağaçların gösterimi
Çocuk sayısı sınırsız olduğunda düğüme sabit sayıda referans alanı koymak işe yaramaz. İki yaygın çözüm vardır.
// 1) Çocuk listesi
class GenelDugum {
int veri;
List<GenelDugum> cocuklar
= new ArrayList<>();
}
// 2) İlk çocuk – sonraki kardeş
class KardesDugum {
int veri;
KardesDugum ilkCocuk;
KardesDugum sonrakiKardes;
}Birinci yolda (GenelDugum) her düğüm, çocuklarının referanslarını bir listede tutar. ArrayList gerektikçe büyüdüğü için çocuk sayısına sınır yoktur. cocuklar listesi üzerinde bir for-each döngüsü, ağaç ADT'sindeki cocuklar(p) metodunun doğrudan karşılığıdır. Bu gösterim sezgiseldir, ama her düğüm ayrı bir liste nesnesi taşır.
İkinci yolda (KardesDugum) her düğüm yalnız iki referans tutar: ilk çocuk (ilkCocuk) ve sonraki kardeş (sonrakiKardes). Bir düğümün bütün çocuklarına ulaşmak için önce ilk çocuğa inilir, sonra kardeş zinciri boyunca sağa yürünür. Çocuk listesi böylece düğümlerin kendisine dağıtılmış bir bağlı listeye dönüşür.
Şekil 9.23'teki ağacın ilk çocuk – sonraki kardeş gösteriminde her düğümün iki referansını yazalım.
- A. İlk çocuk B; kardeşi yok (kök):
ilkCocuk = B,sonrakiKardes = null. - B, C, D. Üçü A'nın çocuklarıdır ve kardeş zinciri B → C → D'dir. B'nin ilk çocuğu E, sonraki kardeşi C'dir. C'nin çocuğu yok, sonraki kardeşi D'dir. D'nin ilk çocuğu G, sonraki kardeşi yoktur.
- E, F, G. E'nin çocuğu yok, sonraki kardeşi F'dir. F ve G'nin iki referansı da
null'dır. - Sayım. Yedi düğümde 14 referans alanı vardır. Dolu olanlar A→B, B→E, B→C, C→D, D→G, E→F'dir: 6 tane. Bu da n − 1 = 6'dır.
İkinci gösterimde her düğüm tam iki referans taşır. İlk çocuk referansı “sol”, sonraki kardeş referansı “sağ” sayılırsa her genel ağaç bir ikili ağaç olarak saklanabilir. Şekil 9.23'ün sağ yarısı, 45 derece döndürülerek bakıldığında bir ikili ağaçtır: A'nın sol çocuğu B; B'nin sol çocuğu E, sağ çocuğu C; C'nin sağ çocuğu D; E'nin sağ çocuğu F; D'nin sol çocuğu G. Bu dönüşüm, ikili ağaçlar için yazılmış birçok algoritmanın genel ağaçlara da uygulanabilmesini sağlar. Aynı dönüşümle bir orman da tek bir ikili ağaca çevrilebilir: ormandaki ağaçların kökleri birbirinin kardeşi sayılır.
İki gösterim arasında seçim yine kullanım biçimine bağlıdır. Bir düğümün k. çocuğuna sık sık doğrudan erişilecekse çocuk listesi uygundur: cocuklar.get(k) O(1) sürer. İlk çocuk – sonraki kardeş gösteriminde aynı çocuğa kardeş zinciri boyunca k adımda ulaşılır. Buna karşılık bu gösterim, her düğümde sabit ve küçük bir bellek kullanır ve çocuk eklemek için liste nesnesine ihtiyaç duymaz.
9.4Ağaç gezinmeleri
Dizide ya da bağlı listede bütün öğeleri dolaşmanın tek doğal sırası vardır: baştan sona. Ağaçta ise böyle bir tek sıra yoktur. Bir düğüme gelindiğinde önce düğümün kendisi mi işlenecek, yoksa önce alt ağaçları mı? Alt ağaçlardan hangisi önce gezilecek? Yoksa düğümler seviye seviye mi taranacak? Bu soruların her yanıtı farklı bir gezinme verir ve her gezinme farklı bir problemin doğal çözümüdür.
9.4.1Gezinme nedir?
Bir ağacın gezinmesi (traversal), ağaçtaki her düğümü belirli bir sırayla tam olarak bir kez ziyaret etmektir. Ziyaret, düğüm üzerinde yapılacak işin genel adıdır: değeri yazdırmak, bir toplama eklemek, düğümü kopyalamak ya da silmek olabilir.
Gezinmeler iki aileye ayrılır. Derinlik öncelikli (depth-first) gezinmeler bir dalın sonuna kadar iner, sonra geri dönüp bir sonraki dala geçer. Bu ailede üç gezinme vardır ve aralarındaki tek fark, kökün alt ağaçlara göre ne zaman ziyaret edildiğidir. Genişlik öncelikli (breadth-first) gezinme ise ağacı seviye seviye tarar: önce kökü, sonra bütün çocukları, sonra bütün torunları ziyaret eder. Bu gezinmeye seviye sırası denir.
| Gezinme | Sıra | Çıktı | Tipik kullanım |
|---|---|---|---|
| Kök başta (preorder) | kök → sol → sağ | 1 2 4 5 3 6 7 | Ağacı kopyalamak, önek ifade |
| Kök ortada (inorder) | sol → kök → sağ | 4 2 5 1 6 3 7 | BST'yi sıralı gezmek |
| Kök sonda (postorder) | sol → sağ → kök | 4 5 2 6 7 3 1 | Silme, boyut ve yükseklik hesabı |
| Seviye sırası (level-order) | seviye seviye, soldan sağa | 1 2 3 4 5 6 7 | Seviyeye dayalı işlemler |
Derinlik öncelikli üç gezinmenin adları, kökün sıradaki yerinden gelir: kök başta, ortada ya da sonda. Üçünde de sol alt ağaç sağ alt ağaçtan önce gezilir. Değişen tek şey kökün bu iki alt ağaca göre konumudur. Gezinmeler, ikili ağacın özyinelemeli tanımını izler: kökte ne yapılacağı söylenir, alt ağaçlar için aynı gezinme yeniden uygulanır. Bu yüzden bir gezinmenin çıktısını elle bulurken her alt ağaç, kendi başına küçük bir ağaç gibi ele alınır.
9.4.2Kök başta gezinme
Kök başta (preorder) gezinme üç adımdan oluşur:
- Kökü ziyaret et.
- Sol alt ağacı kök başta gez.
- Sağ alt ağacı kök başta gez.
ziyaret(kok);
kokBasta(kok.sol);
kokBasta(kok.sag);Bir düğüm, altındaki bütün düğümlerden önce ziyaret edilir. Çıktıda her düğüm, kendi soyunun hepsinden önce gelir. Şekil 9.24'teki ağaçta adımlar şöyledir. Kök 1 ziyaret edilir. Sonra sol alt ağaca, 2 köklü ağaca geçilir ve onun da kökü, 2, hemen ziyaret edilir. 2'nin sol alt ağacı tek düğümlü 4'tür: 4 ziyaret edilir. 2'nin sağ alt ağacı 5'tir: 5 ziyaret edilir. Böylece 1'in sol alt ağacı biter ve sağ alt ağaca geçilir: 3, ardından 3'ün sol çocuğu 6 ve sağ çocuğu 7. Çıktı 1 2 4 5 3 6 7'dir.
1 2 4. Sağda gezinme bitmiştir; çıktı 1 2 4 5 3 6 7.Kök başta gezinmenin tipik kullanım alanları, düğümün çocuklarından önce işlenmesinin gerektiği durumlardır. Ağacı kopyalamak bunun en açık örneğidir: bir düğümün kopyası oluşturulmadan onun çocuklarının kopyaları bağlanamaz. Ağacı bir diziye ya da dosyaya yazmak da kök başta sırayla yapılır; dosyayı okuyan kod, her düğümü ebeveyninden sonra gördüğü için ağacı aynı sırayla yeniden kurabilir. İçindekiler sayfası da bir kök başta gezinmedir: bölüm başlığı alt başlıklarından önce yazılır. İfade ağaçlarında kök başta gezinme önek (prefix) gösterimi verir; işleç, işlenenlerinden önce yazılır.
9.4.3Kök ortada gezinme
Kök ortada (inorder) gezinmede kök, iki alt ağacın arasında ziyaret edilir:
- Sol alt ağacı kök ortada gez.
- Kökü ziyaret et.
- Sağ alt ağacı kök ortada gez.
kokOrtada(kok.sol);
ziyaret(kok);
kokOrtada(kok.sag);Bir düğüm, sol alt ağacındaki bütün düğümlerden sonra ve sağ alt ağacındakilerin hepsinden önce ziyaret edilir. Bu yüzden gezinme kökten başlamaz; kökten başlayıp sola sola inilir ve ilk ziyaret edilen düğüm en soldaki düğüm olur. Şekil 9.24'teki ağaçta 1'den 2'ye, 2'den 4'e inilir. 4'ün sol alt ağacı boştur; 4 ziyaret edilir. 4'ün sağ alt ağacı da boştur ve 2'ye dönülür. 2'nin sol alt ağacı bittiği için 2 ziyaret edilir, ardından sağ alt ağacı olan 5. 1'in sol alt ağacı tamamlanmıştır; şimdi 1 ziyaret edilir. Sağ alt ağaçta yine en sola inilir: 6, sonra 3, sonra 7. Çıktı 4 2 5 1 6 3 7'dir.
4 2 5 1 6 3 7.Kök ortada çıktısının dikkat çekici bir geometrik anlamı vardır: düğümler, ağaç çiziminde soldan sağa göründükleri sırayla yazılır. Ağaç yatay eksene izdüşürülse, yani her düğüm dikey olarak aşağıya “düşürülse”, sıralama kök ortada çıktısını verir. Şekil 9.24'te soldan sağa sıra 4, 2, 5, 1, 6, 3, 7'dir.
Bu gezinmenin en önemli kullanımı ikili arama ağaçlarındadır. İkili arama ağacında her düğümün solundaki anahtarlar küçük, sağındakiler büyüktür. Kök ortada gezinme önce küçükleri, sonra düğümü, sonra büyükleri yazar. Bu her alt ağaçta tekrarlandığı için anahtarlar küçükten büyüğe sıralı çıkar. Tersi de doğrudur: kök ortada çıktısı sıralı olan bir ikili ağaç, (farklı anahtarlar için) bir ikili arama ağacıdır. İfade ağaçlarında kök ortada gezinme ara ek (infix) gösterimi verir; ama parantezler kaybolur.
Kök ortada gezinmeye kökten başlamak. Kök, sol alt ağacının bütün düğümleri yazıldıktan sonra yazılır; ilk yazılan düğüm, kökten sola sola inilerek ulaşılan en soldaki düğümdür. En soldaki düğümün sağ alt ağacı olabileceğini unutmak da sık görülür. Bu düğüm yazıldıktan sonra sağ alt ağacına geçilir ve orada yine en sola inilir.
9.4.4Kök sonda gezinme
Kök sonda (postorder) gezinmede kök, iki alt ağacın ikisi de bittikten sonra ziyaret edilir:
- Sol alt ağacı kök sonda gez.
- Sağ alt ağacı kök sonda gez.
- Kökü ziyaret et.
kokSonda(kok.sol);
kokSonda(kok.sag);
ziyaret(kok);Her düğüm, bütün soyundan sonra ziyaret edilir. Kök en son yazılır. Şekil 9.24'teki ağaçta önce en soldaki yaprak 4 ziyaret edilir. Sonra 2'nin sağ çocuğu 5 gelir. 2'nin iki alt ağacı da bittiği için 2 yazılır. 1'in sağ alt ağacına geçilir: önce 6, sonra 7, sonra 3. En sonda kök 1 yazılır. Çıktı 4 5 2 6 7 3 1'dir.
4 5 2. Sağda kök en son ziyaret edilmiştir; çıktı 4 5 2 6 7 3 1.Kök sonda gezinme, bir düğümün işinin çocuklarının sonucuna bağlı olduğu her durumda kullanılır. Ağacı silmek bunun tipik örneğidir: önce çocuklar silinmelidir, yoksa ebeveyn silindiğinde çocuklara ulaşan referanslar kaybolur. Dosya sisteminde bir klasörü silmek de böyle çalışır; boş olmayan bir klasör silinemez, önce içindekiler silinir. Bir klasörün diskte kapladığı toplam alanı hesaplamak da kök sondadır: önce alt klasörlerin boyutları bulunur, sonra toplanır. Ağacın düğüm sayısı ve yüksekliği de aynı mantıkla hesaplanır. İfade ağaçlarında kök sonda gezinme sonek (postfix) gösterimi verir; ifadenin değeri de bu sırayla hesaplanır.
Üç gezinmenin çıktıları arasında kolay denetlenebilir bir ilişki vardır. Kök başta çıktısının ilk öğesi köktür; kök sonda çıktısının son öğesi köktür; kök ortada çıktısında kök, sol ve sağ alt ağaçların arasında durur. Tablo 9.12'deki çıktılarda bu kolayca görülür: 1, kök başta çıktısının başında, kök sonda çıktısının sonunda ve kök ortada çıktısının tam ortasındadır. Bu gözlem, iki gezinme çıktısından ağacı yeniden kurmanın da temelidir.
9.4.5Üç gezinme, tek tur: Euler turu
Derinlik öncelikli üç gezinmeyi tek bir resimde birleştirmenin zarif bir yolu vardır. Ağacın çevresinde, kökün solundan başlayıp kenarlara sol elimizle dokunarak yürüdüğümüzü düşünelim. Bu yürüyüşe Euler turu (Euler tour) denir. Tur ağacın bütün kenarlarını iki kez geçer: bir kez inerken, bir kez çıkarken. Her düğüme de üç kez uğranır: önce solundan, sonra altından, sonra sağından.
Bir düğümü hangi uğrayışta yazdığımıza göre üç gezinmeden biri ortaya çıkar.
- Düğümü solundan geçerken, yani ona ilk kez varıldığında yazarsak kök başta sırası çıkar:
A B D E C. - Altından geçerken, yani sol alt ağacı bitmiş ve sağ alt ağaca henüz başlanmamışken yazarsak kök ortada sırası çıkar:
D B E A C. - Sağından geçerken, yani iki alt ağaç da bitmişken yazarsak kök sonda sırası çıkar:
D E B C A.
Yaprakların üç uğrayışı art arda gelir, çünkü yaprağın alt ağaçları boştur. Bu yüzden yapraklar üç gezinmede de aynı göreli sırada çıkar: D, E, C. Bir gezinmeden ötekine yalnız iç düğümlerin yeri değişir. Euler turu, üç gezinmenin aslında aynı turun farklı anları olduğunu gösterir. Tur her kenarı iki kez geçtiği için n düğümlü ağaçta 2(n − 1) kenar adımı sürer; maliyet O(n)'dir. Bu, üç gezinmenin de O(n) olduğunun bir başka açıklamasıdır.
Euler turu fikri gezinmeleri birleştirmekten fazlasına yarar. Bazı işler, bir düğümün hem öncesinde hem sonrasında bir şey yapmayı gerektirir. Örneğin bir ifade ağacını tam parantezli yazarken bir alt ağaca girerken “(” açılır, alttan geçerken işleç yazılır, çıkarken “)” kapanır. Bu, üç uğrayışın üçünün de kullanıldığı bir Euler turudur.
9.4.6Seviye sırası gezinme
Seviye sırası (level-order) gezinme kökten başlar, her seviyeyi soldan sağa tarar, sonra bir alt seviyeye iner. Çizgelerdeki genişlik öncelikli aramanın (breadth-first search, BFS) ağaçtaki hâlidir.
1 2 3 4 5 6 7.Derinlik öncelikli gezinmelerden farklı olarak seviye sırası, alt ağaçların yapısını izlemez. 2'yi ziyaret ettikten sonra onun çocuğu 4'e değil, kardeşi 3'e geçer. 2'nin çocukları ancak bütün 1. seviye bittikten sonra gelir. Bu yüzden seviye sırası doğal biçimde özyinelemeyle yazılamaz. Özyineleme, bir alt ağacı bitirmeden ötekine geçmez; seviye sırası ise iki alt ağaç arasında sürekli gidip gelir.
Çözüm bir kuyruktur (queue). Kuyruk, önce gelenin önce çıktığı FIFO (first-in, first-out) düzeniyle çalışır. Bir düğüm ziyaret edilirken çocukları kuyruğun arkasına eklenir. Önce eklenen düğümlerin çocukları da önce işlenir. Bir seviyenin bütün düğümleri, bir sonraki seviyenin düğümlerinden önce kuyruğa girdiği için seviyeler sırasıyla taranır.
Seviye sırasının kullanım alanları, seviyelerin anlam taşıdığı işlerdir. Ağacı seviye seviye yazdırmak, her seviyedeki değerlerin toplamını ya da en büyüğünü bulmak, köke en yakın yaprağı bulmak ve ağacın en geniş seviyesini (genişliğini) hesaplamak bunlara örnektir. Köke en yakın yaprak için seviye sırası en uygun yöntemdir. Kuyruktan çıkan ilk yaprak, köke en yakın olanlardan biridir; daha derindeki düğümlere hiç inmeden gezinme durdurulabilir.
Seviye sırasını kök başta ile karıştırmak. İki gezinme de kökle başlar ve Şekil 9.24'teki mükemmel ağaçta ilk üç öğeleri bile farklıdır: kök başta 1 2 4, seviye sırası 1 2 3. Kök başta bir dala derinlemesine iner; seviye sırası önce kardeşleri bitirir. Seviye sırasını yığınla yazmaya çalışmak da aynı karışıklığın kod hâlidir: yığın kullanılırsa çıktı bir derinlik öncelikli gezinmeye döner.
9.5Gezinmelerin kodu
Gezinmelerin tanımları özyinelemelidir ve en kısa kodları da özyinelemeli yazılır. Ama özyinelemenin arkasında görünmeyen bir yapı çalışır: çağrı yığını (call stack). Her metot çağrısı yığına bir çerçeve (stack frame) ekler; çerçeve, çağrının parametrelerini, yerel değişkenlerini ve metodun döndükten sonra nereden devam edeceğini tutar. Bu kesim önce üç özyinelemeli gezinmeyi çağrı yığını üzerinde izler. Ardından aynı gezinmeleri, çağrı yığınının yerine açık bir Deque yığını kullanarak özyinelemesiz yazar. Son olarak seviye sırasını bir kuyrukla kodlar.
Bu kesimdeki bütün kodlar bağlı gösterimdeki AgacDugumu sınıfını kullanır. Özyinelemeli izlemeler 9 kökündeki dört düğümlü ağaç üzerinde yapılır: 9'un çocukları 2 ve 3; 2'nin sol çocuğu 4. Bu ağacın yüksekliği 2'dir.
9.5.1Özyinelemeli kök başta ve çağrı yığını
public void kokBasta(AgacDugumu kok) {
if (kok == null) {
return;
}
System.out.print(kok.veri + " ");
kokBasta(kok.sol);
kokBasta(kok.sag);
}Metodun ilk iş olarak yaptığı şey, ağacın boş olup olmadığını denetlemektir. kok == null ise boş bir alt ağaçtayızdır ve yapılacak bir şey yoktur; metot hemen döner. Bu taban durumdur. Ağaç boş değilse önce kökün değeri yazdırılır (satır 5), sonra sol alt ağaç (satır 6), en son sağ alt ağaç (satır 7) için aynı metot çağrılır. Satır 8'deki kapanış parantezine gelindiğinde çağrı biter ve çerçevesi yığından çıkar.
Taban durumun null ile tanımlanması kodu sadeleştirir. Yaprak için özel bir durum yazılmaz: yaprağın iki çocuğu null'dır ve iki özyinelemeli çağrı hemen döner. Bunun bedeli, her null çocuk için bir çağrı daha yapılmasıdır. Teorem 9.5'e göre n düğümlü ağaçta n + 1 null referans vardır; toplam çağrı sayısı n + (n + 1) = 2n + 1'dir. Dört düğümlü ağaçta 9 çağrı yapılır.
| Adım | Çağrı yığını | Olay | Çıktı |
|---|---|---|---|
| 1 | kokBasta(9) | 9 yazdırılır | 9 |
| 2 | 9 · s.6 | kokBasta(2) | 2 yazdırılır | 9 2 |
| 3 | 9 · s.6 | 2 · s.6 | kokBasta(4) | 4 yazdırılır | 9 2 4 |
| 4 | … | 4 · s.6 | kokBasta(null) | taban durum, return | 9 2 4 |
| 5 | … | 4 · s.7 | kokBasta(null) | taban durum, return | 9 2 4 |
| 6 | 9 · s.6 | 2 · s.6 | kokBasta(4) | 4 biter (satır 8) | 9 2 4 |
| 7 | 9 · s.6 | 2 · s.7 | kokBasta(null) | 2'nin sağı boş | 9 2 4 |
| 8 | 9 · s.6 | kokBasta(2) | 2 biter | 9 2 4 |
| 9 | 9 · s.7 | kokBasta(3) | 3 yazdırılır | 9 2 4 3 |
| 10–11 | 9 · s.7 | 3 · s.6 / s.7 | kokBasta(null) | 3'ün iki çocuğu boş | 9 2 4 3 |
| 12 | 9 · s.7 | kokBasta(3) | 3 biter | 9 2 4 3 |
| 13 | kokBasta(9) | 9 biter, yığın boşalır | 9 2 4 3 |
kokBasta(4.sol) taban durumdadır; 9, 2 ve 4 çerçeveleri yığında bekler. Altta son adım: bütün düğümler yazdırılmıştır, çıktı 9 2 4 3.İzlemenin en önemli gözlemi, çağrı yığınının derinliğiyle ilgilidir. 4. ve 5. adımlarda yığında dört çerçeve vardır: 9, 2, 4 ve 4'ün null çocuğu. Bu, izlemedeki en büyük derinliktir. Yığın derinliği, kökten en derin null çocuğa inen yolla belirlenir. Yükseklik h ise bu yolda h + 1 düğüm ve bir null çağrısı vardır; yığında en çok h + 2 çerçeve bulunur. Burada h = 2 ve en çok 2 + 2 = 4 çerçeve görülür.
Zaman maliyeti O(n)'dir: her düğüm için bir çağrı ve her null referans için bir çağrı yapılır; her çağrı sabit iş yapar. Ek bellek, yığın derinliği kadardır: O(h). Dengeli ağaçta bu O(log n), çarpık ağaçta O(n)'dir.
9.5.2Özyinelemeli kök ortada
public void kokOrtada(AgacDugumu kok) {
if (kok == null) {
return;
}
kokOrtada(kok.sol);
System.out.print(kok.veri + " ");
kokOrtada(kok.sag);
}Kod kök başta ile aynıdır; tek fark yazdırma satırının iki özyinelemeli çağrının arasına alınmasıdır. Bu küçük değişiklik, değerin hangi anda yazılacağını değiştirir. Bir çağrı başladığında değer hemen yazılmaz; önce sol alt ağaç bütünüyle gezilir. Değer, sol çağrı döndükten sonra, satır 6'da yazılır.
| Adım | Çağrı yığını | Olay | Çıktı |
|---|---|---|---|
| 1–3 | 9 · s.5 | 2 · s.5 | kokOrtada(4) | 9, 2, 4 çağrılır; hiçbiri henüz yazılmaz | — |
| 4 | … | 4 · s.5 | kokOrtada(null) | 4'ün solu boş | — |
| 5 | 9 · s.5 | 2 · s.5 | kokOrtada(4) | sol bitti: 4 yazdırılır | 4 |
| 6–7 | … | 4 · s.7 | kokOrtada(null) | 4'ün sağı boş; 4 biter | 4 |
| 8 | 9 · s.5 | kokOrtada(2) | sol bitti: 2 yazdırılır | 4 2 |
| 9–10 | 9 · s.5 | 2 · s.7 | kokOrtada(null) | 2'nin sağı boş; 2 biter | 4 2 |
| 11 | kokOrtada(9) | sol bitti: 9 yazdırılır | 4 2 9 |
| 12–13 | 9 · s.7 | 3 · s.5 | kokOrtada(null) | 3 çağrılır; 3'ün solu boş | 4 2 9 |
| 14 | 9 · s.7 | kokOrtada(3) | 3 yazdırılır | 4 2 9 3 |
| 15–17 | kokOrtada(9) | 3'ün sağı boş; 3 ve 9 biter | 4 2 9 3 |
Çıktı 4 2 9 3'tür. Kök 9, sol alt ağacının (4 ve 2) arkasından, sağ alt ağacının (3) önünden gelir. İzlemede 9 numaralı çerçevenin ömrüne bakmak öğreticidir. Çerçeve ilk adımda oluşur, ama 9 ancak 11. adımda yazılır. Arada çerçeve, satır 5'teki çağrının dönmesini bekler. Yığındaki bir çerçeve, “burada yarım kalmış bir iş var” bilgisini taşır.
9.5.3Özyinelemeli kök sonda
public void kokSonda(AgacDugumu kok) {
if (kok == null) {
return;
}
kokSonda(kok.sol);
kokSonda(kok.sag);
System.out.print(kok.veri + " ");
}Bu kez yazdırma iki çağrının arkasına, satır 7'ye alınmıştır. Sol çağrı satır 5'ten, sağ çağrı satır 6'dan yapılır. Bir düğümün değeri, iki alt ağacının da tamamen gezilmesinden sonra yazılır.
| Adım | Çağrı yığını | Olay | Çıktı |
|---|---|---|---|
| 1–3 | 9 · s.5 | 2 · s.5 | kokSonda(4) | 9, 2, 4 çağrılır | — |
| 4 | … | 4 · s.5 | kokSonda(null) | 4'ün solu boş | — |
| 5 | … | 4 · s.6 | kokSonda(null) | 4'ün sağı boş | — |
| 6–7 | 9 · s.5 | 2 · s.5 | kokSonda(4) | iki alt ağaç bitti: 4 yazdırılır; 4 biter | 4 |
| 8 | 9 · s.5 | 2 · s.6 | kokSonda(null) | 2'nin sağı boş | 4 |
| 9–10 | 9 · s.5 | kokSonda(2) | 2 yazdırılır; 2 biter | 4 2 |
| 11–13 | 9 · s.6 | 3 · s.5 / s.6 | kokSonda(null) | 3 çağrılır; iki çocuğu boş | 4 2 |
| 14–15 | 9 · s.6 | kokSonda(3) | 3 yazdırılır; 3 biter | 4 2 3 |
| 16–17 | kokSonda(9) | 9 yazdırılır; yığın boşalır | 4 2 3 9 |
Çıktı 4 2 3 9'dur. Üç izleme yan yana konduğunda çağrı sırasının hiç değişmediği görülür. Üç metotta da çağrılar aynı sırayla yapılır ve aynı sırayla döner: 9, 2, 4, null, null, … Değişen tek şey, yazdırmanın bu sıranın hangi noktasında yapıldığıdır. Bu, Euler turu gözleminin kod düzeyindeki karşılığıdır. Çağrının başlangıcı düğümün solundan geçişe, iki çağrının arası altından geçişe, çağrının sonu sağından geçişe karşılık gelir.
Taban durumu unutmak. if (kok == null) return; satırı olmadan ilk yaprakta kokBasta(kok.sol) null ile çağrılır ve kok.veri okunurken NullPointerException fırlatılır. Taban durumu yazdırma satırından sonraya koymak da aynı hatayı verir. Her özyinelemeli ağaç metodu, düğümün alanlarına dokunmadan önce boş ağaç durumunu ele almalıdır.
9.5.4Yığınla kök başta
Özyineleme, çağrı yığınını sistemin kendisine bırakır. Çağrı yığınının boyutu ise sınırlıdır; Java'da varsayılan ayarlarla birkaç bin ile birkaç on bin çerçeve arasında bir derinlikte StackOverflowError alınır. Çok derin, örneğin yüz binlerce düğümlü bir zincir biçimindeki ağaçta özyinelemeli gezinme çöker. Çözüm, çağrı yığınının yaptığı işi java.util.Deque ile açık bir yığına yaptırmaktır. Bu yığın heap bellekte yaşar ve ancak bellek bitince dolar.
public void kokBastaYigin(AgacDugumu kok) {
if (kok == null) return;
Deque<AgacDugumu> yigin = new ArrayDeque<>();
yigin.push(kok);
while (!yigin.isEmpty()) {
AgacDugumu gecici = yigin.pop();
System.out.print(gecici.veri + " ");
if (gecici.sag != null)
yigin.push(gecici.sag); // önce sağ
if (gecici.sol != null)
yigin.push(gecici.sol); // sol üstte
}
}Kök başta, açık yığınla yazılması en kolay gezinmedir. Bir düğüm yığından çıkarıldığı anda yazdırılabilir, çünkü kök başta gezinmede düğüm çocuklarından önce gelir. Döngünün her turu üst düğümü çıkarır (pop), değerini yazdırır, sonra çocuklarını yığına iter (push). İtme sırası önemlidir: önce sağ, sonra sol. Yığın LIFO çalıştığı için en son itilen sol çocuk üstte kalır ve bir sonraki turda önce o çıkar. Böylece sol alt ağaç bütünüyle gezildikten sonra sağ çocuk sıraya gelir.
| Tur | pop | İtilenler | Tur sonunda yığın | Çıktı |
|---|---|---|---|---|
| başlangıç | — | 1 | [1] | — |
| 1 | 1 | 3, 2 | [3, 2] | 1 |
| 2 | 2 | 5, 4 | [3, 5, 4] | 1 2 |
| 3 | 4 | — | [3, 5] | 1 2 4 |
| 4 | 5 | — | [3] | 1 2 4 5 |
| 5 | 3 | 7, 6 | [7, 6] | 1 2 4 5 3 |
| 6 | 6 | — | [7] | 1 2 4 5 3 6 |
| 7 | 7 | — | [] | 1 2 4 5 3 6 7 |
1 2 4 5 3 6 7, özyinelemeli sürümle aynıdır.Yığının içeriği, özyinelemedeki “yarım kalmış işler”in listesidir. 2. turun sonunda yığında 3, 5, 4 vardır. Bu, “önce 4'ün alt ağacı, sonra 5'in alt ağacı, sonra 3'ün alt ağacı gezilecek” demektir. Özyinelemeli sürümde aynı bilgi, 2 ve 9 çerçevelerinin hangi satırda beklediğinde saklıydı.
Her düğüm yığına tam bir kez itilir ve bir kez çıkarılır; döngü n tur döner, zaman O(n)'dir. Yığının boyutu yükseklikle sınırlıdır: her seviyeden en çok bir sağ kardeş beklerken bir de üstteki düğüm bulunur. Bu ağaçta yığın en çok 3 öğe tutmuştur (2. turun sonunda). Ek bellek O(h)'dir.
Çocukları önce sol, sonra sağ sırasıyla itmek. Bu durumda sağ çocuk üstte kalır ve önce işlenir; çıktı “kök, sağ, sol” sırasına döner. Şekil 9.24'teki ağaçta yanlış sırayla itme 1 3 7 6 2 5 4 yazdırır. Doğru kök başta sırası için sağ çocuk önce itilmeli, sol çocuk üstte kalmalıdır.
9.5.5Yığınla kök ortada
Kök ortada gezinmede bir düğüm, yığına girdiği anda yazdırılamaz; önce sol alt ağacının bitmesi gerekir. Bu yüzden yığın burada “sonra yazdırılacak” düğümleri tutar. Algoritma iki hareketten oluşur. gecici bir düğümü gösterdiği sürece o düğüm yığına itilir ve sola inilir. gecici null'a ulaştığında sola gidecek yer kalmamıştır; yığının üstündeki düğümün sol alt ağacı bitmiştir. O düğüm çıkarılır, yazdırılır ve sağ alt ağacına geçilir.
public void kokOrtadaYigin(AgacDugumu kok) {
if (kok == null) return;
Deque<AgacDugumu> yigin = new ArrayDeque<>();
AgacDugumu gecici = kok;
while (!yigin.isEmpty() || gecici != null) {
if (gecici != null) {
yigin.push(gecici);
gecici = gecici.sol;
} else {
gecici = yigin.pop();
System.out.print(gecici.veri + " ");
gecici = gecici.sag;
}
}
}Döngü koşulu iki parçalıdır: !yigin.isEmpty() || gecici != null. Yalnız yığının boşluğuna bakmak yetmez. Kök yazdırıldıktan hemen sonra yığın boş olabilir; ama gecici kökün sağ çocuğunu gösterir ve sağ alt ağaç henüz gezilmemiştir. İzleme bu anı içerir. Örnek olarak 1'in çocukları 2 ve 3, 2'nin çocukları 4 ve 5, 3'ün yalnız sol çocuğu 6 olan ağacı kullanalım.
| Adım | Hareket | Yığın | gecici | Çıktı |
|---|---|---|---|---|
| 0 | başlangıç | [] | 1 | — |
| 1 | push 1, sola in | [1] | 2 | — |
| 2 | push 2, sola in | [1, 2] | 4 | — |
| 3 | push 4, sola in | [1, 2, 4] | null | — |
| 4 | pop 4, yaz, sağa geç | [1, 2] | null | 4 |
| 5 | pop 2, yaz, sağa geç | [1] | 5 | 4 2 |
| 6 | push 5, sola in | [1, 5] | null | 4 2 |
| 7 | pop 5, yaz, sağa geç | [1] | null | 4 2 5 |
| 8 | pop 1, yaz, sağa geç | [] | 3 | 4 2 5 1 |
| 9 | push 3, sola in | [3] | 6 | 4 2 5 1 |
| 10 | push 6, sola in | [3, 6] | null | 4 2 5 1 |
| 11 | pop 6, yaz, sağa geç | [3] | null | 4 2 5 1 6 |
| 12 | pop 3, yaz, sağa geç | [] | null | 4 2 5 1 6 3 |
gecici = null; sola gidecek yer kalmamıştır. Sağda 8. adım: 1 yazdırılmıştır, yığın boştur ama gecici 3'ü gösterir; döngü sürer.8. adım, döngü koşulunun neden iki parçalı olduğunu gösterir. Bu adımın sonunda yığın boştur; koşul yalnız !yigin.isEmpty() olsaydı döngü burada biter ve 3 ile 6 hiç yazdırılmazdı. gecici != null parçası, gezilmeyi bekleyen bir sağ alt ağaç olduğunu yakalar. Döngü ancak yığın boş ve gecici null olduğunda, 12. adımdan sonra biter. Çıktı 4 2 5 1 6 3'tür.
Her düğüm bir kez itilir ve bir kez çıkarılır. Ayrıca her null çocuğa bir kez ulaşılır. Toplam iş O(n)'dir. Yığın her an kökten gecici'ye inen yol üzerindeki, sol alt ağacı henüz bitmemiş düğümleri tutar; boyutu en çok h + 1'dir. Ek bellek O(h)'dir.
9.5.6Yığınla kök sonda
Kök sonda en zor yığınlı gezinmedir. Bir düğüm ancak iki alt ağacı da bittiğinde yazdırılabilir. Yığının üstünde bir düğüm görüldüğünde sol alt ağacının bittiği bilinir, ama sağ alt ağacın bitip bitmediği bilinmez. Kod bu yüzden yığındaki düğümü hemen çıkarmaz; önce peek ile bakar.
public void kokSondaYigin(AgacDugumu kok) {
AgacDugumu simdiki = kok;
Deque<AgacDugumu> yigin = new ArrayDeque<>();
while (simdiki != null || !yigin.isEmpty()) {
if (simdiki != null) {
yigin.push(simdiki);
simdiki = simdiki.sol;
} else {
AgacDugumu gecici = yigin.peek().sag;
if (gecici == null) {
gecici = yigin.pop();
System.out.print(gecici.veri + " ");
while (!yigin.isEmpty()
&& gecici == yigin.peek().sag) {
gecici = yigin.pop();
System.out.print(gecici.veri + " ");
}
} else {
simdiki = gecici;
}
}
}
}Kodun mantığı üç parçadır.
- Sola inme.
simdikinulldeğilse düğüm yığına itilir ve sola inilir. Bu, kök ortadaki sola inmenin aynısıdır. - Sağ alt ağaca geçme. Sola gidecek yer kalmayınca üstteki düğümün sağ çocuğuna bakılır (
yigin.peek().sag). Sağ çocuk varsa düğüm henüz yazdırılamaz;simdikisağ çocuğa atanır ve o alt ağaca girilir. Düğüm yığında kalır. - Yazdırma ve yukarı çıkma. Sağ çocuk yoksa üstteki düğümün iki alt ağacı da bitmiştir; düğüm çıkarılır ve yazdırılır. Ardından iç
whiledöngüsü yukarı bakar. Az önce yazdırılan düğüm, yığındaki yeni üst düğümün sağ çocuğuysa, o ebeveynin sağ alt ağacı da bitmiştir; o da çıkarılıp yazdırılır. Bu, ebeveyn zinciri boyunca sağ çocuk olmayı sürdüren düğümlerde tekrarlanır.
İç döngüdeki gecici == yigin.peek().sag karşılaştırması referans eşitliğidir: aynı nesne mi, diye sorar. Bu karşılaştırma, “bu ebeveynin sağ alt ağacından şimdi mi dönüyoruz?” sorusunun yanıtıdır. Yanıt evetse ebeveyn yazdırılır. Yanıt hayırsa, yani az önce yazdırılan düğüm sol çocuksa, ebeveynin sağ alt ağacına henüz girilmemiştir ve iç döngü durur. İzleme 1'in çocukları 2 ve 3, 2'nin çocukları 4 ve 5, 3'ün yalnız sağ çocuğu 6 olan ağaç üzerinde yapılır.
| Adım | Olay | Yığın | Çıktı |
|---|---|---|---|
| 1–3 | 1, 2, 4 itilir; simdiki = 4.sol = null | [1, 2, 4] | — |
| 4 | peek() = 4, sağ çocuk yok: 4 çıkarılıp yazdırılır | [1, 2] | 4 |
| 5 | iç döngü: 4, 2'nin sağ çocuğu değil (2.sag = 5); durur | [1, 2] | 4 |
| 6 | peek() = 2, sağ çocuk 5: simdiki = 5 | [1, 2] | 4 |
| 7 | 5 itilir; simdiki = null | [1, 2, 5] | 4 |
| 8 | peek() = 5, sağ çocuk yok: 5 yazdırılır | [1, 2] | 4 5 |
| 9 | iç döngü: 5, 2'nin sağ çocuğu: 2 yazdırılır | [1] | 4 5 2 |
| 10 | iç döngü: 2, 1'in sağ çocuğu değil (1.sag = 3); durur | [1] | 4 5 2 |
| 11 | peek() = 1, sağ çocuk 3: simdiki = 3 | [1] | 4 5 2 |
| 12 | 3 itilir; simdiki = 3.sol = null | [1, 3] | 4 5 2 |
| 13 | peek() = 3, sağ çocuk 6: simdiki = 6 | [1, 3] | 4 5 2 |
| 14 | 6 itilir; simdiki = null | [1, 3, 6] | 4 5 2 |
| 15 | peek() = 6, sağ çocuk yok: 6 yazdırılır | [1, 3] | 4 5 2 6 |
| 16 | iç döngü: 6, 3'ün sağ çocuğu: 3 yazdırılır | [1] | 4 5 2 6 3 |
| 17 | iç döngü: 3, 1'in sağ çocuğu: 1 yazdırılır | [] | 4 5 2 6 3 1 |
| 18–19 | yığın boş: iç döngü durur; simdiki null, dış döngü biter | [] | 4 5 2 6 3 1 |
4 5 2 6 3 1.İzlemenin son kısmı iç döngünün gücünü gösterir. 15. adımda 6 yazdırılınca iç döngü art arda iki düğümü daha yazdırır. 6, 3'ün sağ çocuğudur; 3'ün iki alt ağacı da bitmiştir. 3 de 1'in sağ çocuğudur; 1'in de iki alt ağacı bitmiştir. Tek bir yaprak, ebeveyn zinciri boyunca bir dizi düğümün “tamamlanmasını” tetiklemiştir. 3'ün sol çocuğunun olmaması da kodu bozmaz. 12. adımda simdiki = 3.sol doğrudan null olur ve 13. adımda sağ alt ağaca geçilir.
Maliyet yine O(n)'dir. Her düğüm bir kez itilir, bir kez çıkarılır; her peek ya bir çıkarma ya da bir sağ alt ağaca geçişle sonuçlanır. Yığın kökten o anki düğüme inen yolu tuttuğu için boyutu en çok h + 1'dir. Aynı sonuca daha kısa bir yoldan da varılır. Yığınlı kök başta kodunda çocuklar ters sırayla (önce sol, sonra sağ) itilirse “kök, sağ, sol” sırası çıkar. Bu çıktı ikinci bir yığınla ters çevrilir. Ters çevrilmiş “kök, sağ, sol” tam olarak “sol, sağ, kök”tür. Bu yöntem kolaydır, ama O(n) ek bellek ister.
9.5.7Kuyrukla seviye sırası
public void seviyeSirasi(AgacDugumu kok) {
if (kok == null) return;
Queue<AgacDugumu> kuyruk = new LinkedList<>();
kuyruk.add(kok);
while (!kuyruk.isEmpty()) {
AgacDugumu gecici = kuyruk.poll();
System.out.print(gecici.veri + " ");
if (gecici.sol != null)
kuyruk.add(gecici.sol);
if (gecici.sag != null)
kuyruk.add(gecici.sag);
}
}Seviye sırası kodu, yığınla kök başta koduna çok benzer. Üç fark vardır. Yığının yerinde bir Queue (burada LinkedList) bulunur. push ve pop yerine add (arkaya ekle) ve poll (önden çıkar) kullanılır. Çocuklar doğal sırayla eklenir: önce sol, sonra sağ. Kuyruk FIFO olduğu için önce eklenen sol çocuk önce çıkar; ters sırayla eklemeye gerek yoktur.
İzleme 4 kökündeki ağaç üzerinde yapılır: 4'ün çocukları 3 ve 7; 3'ün çocukları 5 ve 8; 7 yapraktır.
| Tur | poll | Eklenenler | Tur sonunda kuyruk | Çıktı |
|---|---|---|---|---|
| başlangıç | — | 4 | [4] | — |
| 1 | 4 | 3, 7 | [3, 7] | 4 |
| 2 | 3 | 5, 8 | [7, 5, 8] | 4 3 |
| 3 | 7 | — | [5, 8] | 4 3 7 |
| 4 | 5 | — | [8] | 4 3 7 5 |
| 5 | 8 | — | [] | 4 3 7 5 8 |
4 3 7 5 8.2. tur seviye sırasının özünü gösterir. 3 çıkarılırken çocukları 5 ve 8 kuyruğun arkasına, 7'nin arkasına eklenir. 7, 3 ile aynı seviyededir ve 3'ün çocuklarından önce işlenir. Kök başta olsaydı 3'ten sonra hemen 5 ve 8 gelirdi; çıktı 4 3 5 8 7 olurdu. Seviye sırası ise 4 3 7 5 8 verir.
Her düğüm kuyruğa bir kez girer ve bir kez çıkar; zaman O(n)'dir. Ek bellek ise yığınlı gezinmelerden farklı bir büyüklüğe bağlıdır. Kuyrukta aynı anda bulunan düğümler en çok iki ardışık seviyeye yayılır. Bu yüzden kuyruğun boyutu ağacın genişliğiyle, yani en kalabalık seviyedeki düğüm sayısı w ile sınırlanır; ek bellek O(w)'dir. Bu izlemede kuyruk en çok 3 düğüm tutmuştur. Mükemmel ağaçta son seviyede yaklaşık n/2 düğüm bulunur; seviye sırası bu ağaçlarda O(n) bellek harcar. Yığınlı kök ortada ise aynı ağaçta yalnız O(log n) bellek kullanır.
poll() yerine remove() ya da pop() kullanmanın farkını gözden kaçırmak. LinkedList hem kuyruk hem yığın arayüzünü gerçekler: pop() baştan çıkarır, push() ise başa ekler. Kodda add yerine yanlışlıkla push yazılırsa çocuklar başa eklenir ve kuyruk yığına dönüşür; çıktı seviye sırası olmaktan çıkar. Değişkeni Queue<AgacDugumu> tipiyle bildirmek, yığın metotlarının kullanılmasını derleme anında engeller.
9.6Gezinmelerin uygulamaları
Gezinmeler yalnız düğümleri yazdırmak için değildir. Ağaç üzerinde çalışan hemen her algoritma bir gezinmedir: düğümleri belirli bir sırayla ziyaret eder ve her ziyarette bir iş yapar. Bu kesim, gezinme kalıbının nasıl gerçek problemlere uygulandığını örneklerle gösterir.
9.6.1İfade ağacı
Bir aritmetik ifade doğal olarak bir ağaçtır. (3 + 4) × (5 − 2) ifadesinde en son yapılacak işlem çarpmadır; çarpmanın iki işleneni de birer alt ifadedir. Bu yapı bir ikili ağaçla doğrudan temsil edilir.
Bir ifade ağacı (expression tree), iç düğümleri işleç, yaprakları işlenen (sayı ya da değişken) olan bir ikili ağaçtır. İkili işleçler kullanıldığında her iç düğümün tam iki çocuğu vardır; ağaç doludur. Bir iç düğümün değeri, işlecinin sol ve sağ alt ağacın değerlerine uygulanmasıyla bulunur.
Ağaçta parantez yoktur; parantezlerin görevini ağacın yapısı üstlenir. Toplama ve çıkarma, çarpmadan daha derinde durduğu için önce hesaplanır. İşlem önceliği ve parantez bilgisi ağacın şekline gömülmüştür. Aynı ağaç, üç derinlik öncelikli gezinmeyle üç farklı gösterim verir.
| Gezinme | Çıktı | Gösterim |
|---|---|---|
| Kök başta | × + 3 4 − 5 2 | önek (prefix) |
| Kök ortada | 3 + 4 × 5 − 2; parantezle (3 + 4) × (5 − 2) | ara ek (infix) |
| Kök sonda | 3 4 + 5 2 − × → 7 × 3 = 21 | sonek (postfix) |
Önek ve sonek gösterimler parantez gerektirmez; işleçlerin ve işlenenlerin sırası ifadeyi tek anlamlı belirler. Kök ortada çıktısı ise parantezsiz yazıldığında anlam değiştirir. 3 + 4 × 5 − 2 olağan öncelik kurallarıyla 3 + 20 − 2 = 21'e eşit görünse de bu bir rastlantıdır; okunan ifade ağaçtaki ifade değildir. Doğru ara ek gösterimi elde etmek için her iç düğümün alt ağacı parantez içine alınmalıdır. Bu da düğüme girerken “(”, çıkarken “)” yazan bir Euler turudur.
Kök ortada çıktısını doğrudan ara ek ifade saymak. Parantezler kaybolduğu için ifade başka bir ağaca karşılık gelebilir. ((6 + 2) × 3) − (8 / 4) ifadesinin ağacı kök ortada gezilirse 6 + 2 × 3 − 8 / 4 çıkar; bu, olağan önceliklerle 6 + 6 − 2 = 10 verir, oysa ağacın değeri 22'dir.
9.6.2Kod izleme: hesapla()
İfade ağacının değeri kök sonda mantığıyla hesaplanır: bir işleç, iki işleneninin değeri bilinmeden uygulanamaz. Aşağıdaki kod, yaprakta bir sayı (sayi), iç düğümde bir işleç karakteri (islec) tutan bir IfadeDugumu sınıfı varsayar.
int hesapla(IfadeDugumu d) {
if (d.sol == null && d.sag == null)
return d.sayi; // yaprak: işlenen
int a = hesapla(d.sol);
int b = hesapla(d.sag);
switch (d.islec) { // iç düğüm: işleç
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
default: return a / b;
}
}Metot önce düğümün yaprak olup olmadığına bakar. İki çocuğu da yoksa düğüm bir işlenendir ve sayısı döndürülür; taban durum budur. Değilse sol alt ağacın değeri a'ya (satır 4), sağ alt ağacın değeri b'ye (satır 5) hesaplatılır. En son switch ile işleç uygulanır. Sol ve sağın sırası çıkarma ile bölmede önemlidir: a - b'de a sol işlenendir. İfade ağacı dolu olduğu için bir iç düğümün iki çocuğu da vardır; tek çocuklu düğüm durumu ele alınmaz.
| Adım | Çağrı yığını | Olay |
|---|---|---|
| 1 | hesapla(×) | iç düğüm; önce sol işlenen: hesapla(+) |
| 2 | × · s.4 | hesapla(+) | iç düğüm; önce sol işlenen: hesapla(3) |
| 3 | × · s.4 | + · s.4 | hesapla(3) | yaprak: 3 döner |
| 4–5 | × · s.4 | + · s.5 | hesapla(4) | a = 3; sağ işlenen yaprak: 4 döner |
| 6 | × · s.4 | hesapla(+) | b = 4; 3 + 4 = 7 döner |
| 7–8 | × · s.5 | hesapla(−) | a = 7; sağ işlenen hesapla(−) |
| 9–11 | × · s.5 | − · s.4 / s.5 | hesapla(5), hesapla(2) | yapraklar 5 ve 2 döner |
| 12 | × · s.5 | hesapla(−) | 5 − 2 = 3 döner |
| 13 | hesapla(×) | b = 3; 7 × 3 = 21 döner |
hesapla izlemesi. Solda 6. adım: 3 ve 4 yukarı taşınmış, + düğümü 7 döndürmüştür. Sağda son adım: − düğümü 3, kök 21 döndürmüştür. Oklu sayılar dönüş değerleridir.Değerler ağaçta aşağıdan yukarı akar: yapraklar sayılarını, iç düğümler hesapladıkları değeri ebeveynlerine verir. İşleçlerin uygulanma sırası +, −, ×'tir; bu tam olarak kök sonda sırasıdır ve sonek gösterimdeki 3 4 + 5 2 − × dizisinin okunuşuyla örtüşür. Sonek ifadeyi bir yığınla değerlendiren algoritma da aynı işi yapar; burada yığın, özyinelemenin çağrı yığınıdır. Her düğüm bir kez işlenir; zaman O(n), ek bellek O(h)'dir.
9.6.3En büyük değeri bulma
Sıralı olmayan bir ikili ağaçta en büyük değeri bulmak için her düğüme bakmak gerekir. Özyinelemeli çözüm şöyle düşünür: ağacın en büyüğü, kökün değeri, sol alt ağacın en büyüğü ve sağ alt ağacın en büyüğü arasındaki en büyüktür.
public int enBuyukBul(AgacDugumu kok) {
if (kok == null) {
return Integer.MIN_VALUE;
}
int sonuc = kok.veri;
int sol = enBuyukBul(kok.sol);
int sag = enBuyukBul(kok.sag);
if (sol > sonuc)
sonuc = sol;
if (sag > sonuc)
sonuc = sag;
return sonuc;
}Boş ağaç için hangi değerin döndürüleceği dikkat ister. Boş ağacın “en büyük değeri” yoktur; ama döndürülen değer karşılaştırmayı bozmamalıdır. Integer.MIN_VALUE (−231) hiçbir int değerden büyük olamaz; sol > sonuc karşılaştırmasında hiçbir zaman kazanmaz. Bu yüzden boş alt ağaç sonucu etkilemez. Metodun gövdesi kök sonda kalıbındadır: iki alt ağacın sonucu alınır, sonra kökle birleştirilir.
| Çağrı | sonuc başta | sol | sag | Döner |
|---|---|---|---|---|
enBuyukBul(5) | 5 | MIN | MIN | 5 |
enBuyukBul(8) | 8 | MIN | MIN | 8 |
enBuyukBul(3) | 3 | 5 (> 3: sonuc = 5) | 8 (> 5: sonuc = 8) | 8 |
enBuyukBul(7) | 7 | MIN | MIN | 7 |
enBuyukBul(4) | 4 | 8 (> 4: sonuc = 8) | 7 (≤ 8) | 8 |
enBuyukBul izlemesi. Solda 5'in iki boş alt ağacı MIN döndürmüş, 5 kendi değerini döndürmüştür. Sağda bütün değerler yukarı taşınmıştır: 3'ün alt ağacı 8, 7 kendisi, kök 8 döndürür.Tablo, çağrıların bitiş sırasına göre yazılmıştır: 5, 8, 3, 7, 4. Bu, kök sonda sırasıdır. Her düğüm bir kez ziyaret edilir ve her null çocuk için bir çağrı yapılır; beş düğümlü ağaçta toplam 11 çağrı vardır. Zaman O(n)'dir. Ağaç bir ikili arama ağacı olsaydı en büyük değer çok daha hızlı bulunurdu: kökten sağa sağa inmek yeterdi ve maliyet O(h) olurdu. Sıralı olmayan bir ağaçta ise bütün düğümlere bakmaktan kaçınılamaz.
Boş ağaç için 0 döndürmek. Bütün değerleri negatif olan bir ağaçta, örneğin −5 ve −3 değerli iki düğümde, 0 hiçbir düğümde bulunmadığı hâlde sonuç olarak döner. Taban durumun değeri, birleştirme işleminin etkisiz öğesi olmalıdır: en büyükte Integer.MIN_VALUE, toplamda 0, çarpımda 1.
9.6.4Özyinelemeli ağaç fonksiyonları
hesapla ve enBuyukBul aynı kalıbı izler. Boş ağaç için bir taban değer döndürülür; boş olmayan ağaçta iki alt ağacın sonucu özyinelemeyle alınır ve kökle birleştirilir. Bu kalıp, ağacın birçok temel özelliğini birkaç satırla hesaplamaya yeter.
int boyut(AgacDugumu k) { // düğüm sayısı
if (k == null) return 0;
return 1 + boyut(k.sol) + boyut(k.sag);
}
int yukseklik(AgacDugumu k) { // kenar sayısı
if (k == null) return -1; // boş ağaç: -1
return 1 + Math.max(yukseklik(k.sol),
yukseklik(k.sag));
}
int yaprakSayisi(AgacDugumu k) {
if (k == null) return 0;
if (k.sol == null && k.sag == null) return 1;
return yaprakSayisi(k.sol) + yaprakSayisi(k.sag);
}boyut(k)düğüm sayısını verir. Boş ağaçta 0 düğüm vardır. Boş olmayan ağaçta kök için 1, sol alt ağaçtakiler içinboyut(k.sol), sağdakiler içinboyut(k.sag)toplanır.yukseklik(k)yüksekliği kenar sayısıyla verir. Taban durumda −1 döndürülür. Bu seçim ilk bakışta tuhaf gelir, ama tanımı tutarlı kılar: yaprağın iki çocuğu da boştur ve yüksekliği 1 + max(−1, −1) = 0 çıkar. Taban durumda 0 döndürülseydi yaprağın yüksekliği 1 olurdu; bu da yüksekliği düğüm sayısıyla ölçmek demektir.yaprakSayisi(k)iki taban durum kullanır. Boş ağaçta 0 yaprak vardır. İki çocuğu danullolan düğüm bir yapraktır ve 1 döndürür. Diğer düğümlerde yaprak sayısı, iki alt ağacın yaprak sayılarının toplamıdır; düğümün kendisi yaprak olmadığı için eklenmez.
9 kökündeki dört düğümlü ağaçta (9'un çocukları 2 ve 3; 2'nin sol çocuğu 4) bu metotlar boyut = 4, yukseklik = 2 ve yaprakSayisi = 2 verir; yapraklar 4 ve 3'tür. Üç metot da her düğümü bir kez işler: O(n) zaman, çağrı yığını için O(h) alan.
9.6.5Kod izleme: boyut()
boyut'un izlemesi, değerlerin alt ağaçlardan yukarı nasıl taşındığını gösterir. Bütün özyinelemeli çağrılar tek satırdan, satır 3'ten yapılır; Java ifadeyi soldan sağa hesapladığı için önce boyut(k.sol), sonra boyut(k.sag) çağrılır.
| Adım | Çağrı | Olay | Döner |
|---|---|---|---|
| 1–3 | boyut(9), boyut(2), boyut(4) | her biri önce sol alt ağacı çağırır | — |
| 4 | boyut(4.sol) | boş ağaç | 0 |
| 5–6 | boyut(4.sag) | sol 0 geldi; sağ da boş | 0 |
| 7 | boyut(4) | 1 + 0 + 0 | 1 |
| 8–9 | boyut(2.sag) | sol 1 geldi; sağ boş | 0 |
| 10 | boyut(2) | 1 + 1 + 0 | 2 |
| 11–15 | boyut(3) | iki çocuğu boş; 1 + 0 + 0 | 1 |
| 16–17 | boyut(9) | 1 + 2 + 1 | 4 |
boyut izlemesi. Üstte 10. adım: 4'ün alt ağacı 1, 2'nin alt ağacı 2 döndürmüştür. Altta son adım: 3 de 1 döndürmüş, kök 1 + 2 + 1 = 4 vermiştir. null çocukların altındaki 0'lar, boş ağaçların dönüş değeridir.İzlemede dokuz çağrı yapılır: dört düğüm ve beş null çocuk için. Bu, 2n + 1 = 9'dur. Her düğümün döndürdüğü sayı, o düğümün alt ağacındaki düğüm sayısıdır: 4 için 1, 2 için 2, 3 için 1, 9 için 4. Böylece tek bir kök sonda gezinmesi bütün alt ağaçların boyutunu da hesaplamış olur. Bu bilgi düğümlerde saklanırsa, örneğin “ağaçtaki k. en küçük anahtar” gibi sorular hızla yanıtlanabilir.
9.6.6Kod izleme: yukseklik()
yukseklik'te sol çağrı satır 3'ten, sağ çağrı satır 4'ten yapılır: Math.max'ın iki argümanı iki ayrı satıra yazılmıştır. İzleme aynı ağaç üzerindedir.
| Adım | Çağrı | Hesap | Döner |
|---|---|---|---|
| 1–3 | yukseklik(9), (2), (4) | her biri önce sol alt ağacı çağırır | — |
| 4–6 | yukseklik(4.sol), yukseklik(4.sag) | boş ağaçlar | −1, −1 |
| 7 | yukseklik(4) | 1 + max(−1, −1) | 0 |
| 8–9 | yukseklik(2.sag) | boş ağaç | −1 |
| 10 | yukseklik(2) | 1 + max(0, −1) | 1 |
| 11–16 | yukseklik(3) | 1 + max(−1, −1) | 0 |
| 17 | yukseklik(9) | 1 + max(1, 0) | 2 |
yukseklik izlemesinin son adımı. Boş ağaçlar −1, yapraklar 4 ve 3 ise 0 döndürmüştür; 2'nin yüksekliği 1, kökün yüksekliği 1 + max(1, 0) = 2'dir.Sonuç 2'dir: kökten en derin yaprak 4'e giden 9–2–4 yolu iki kenarlıdır. Çağrı yapısı boyut ile aynıdır, birleştirme işlemi farklıdır: toplama yerine en büyük alınır. Kalıbın gücü buradadır. Taban değeri ve birleştirme işlemi değiştirilerek aynı iskeletle düğümlerin toplamı, en küçük değer, belirli bir değerin kaç kez geçtiği gibi birçok özellik hesaplanabilir.
9.6.7Kod izleme: yaprakSayisi()
yaprakSayisi bir yaprakta alt çağrı yapmadan döner. Bu yüzden izlemesi daha kısadır: yaprakların null çocukları hiç çağrılmaz.
| Adım | Çağrı | Olay | Döner |
|---|---|---|---|
| 1 | yaprakSayisi(9) | yaprak değil; önce sol | — |
| 2 | yaprakSayisi(2) | yaprak değil; önce sol | — |
| 3 | yaprakSayisi(4) | iki çocuğu da null: yaprak | 1 |
| 4 | yaprakSayisi(2.sag) | boş ağaç | 0 |
| 5 | yaprakSayisi(2) | 1 + 0 | 1 |
| 6 | yaprakSayisi(3) | yaprak | 1 |
| 7 | yaprakSayisi(9) | 1 + 1 | 2 |
yaprakSayisi izlemesinin son adımı. 4 ve 3 yaprak olarak 1 döndürmüş, 2'nin boş sağ alt ağacı 0 vermiştir; kökte 1 + 1 = 2 bulunur.Bu izlemede yalnız beş çağrı yapılır: dört düğüm ve 2'nin boş sağ çocuğu. boyut'taki dokuz çağrıdan azdır, çünkü yaprak denetimi yaprakların null çocuklarına inmeyi önler. İkinci taban durumun bir başka işlevi daha vardır. O satır olmasaydı metot boş ağaçta 0 döndürür ve hiçbir zaman 1 üretmezdi; her ağaç için sonuç 0 çıkardı. Yaprak, sayılacak şeyin kendisidir; bu yüzden ayrı bir taban durum ister.
yaprakSayisi'nde yalnız k == null taban durumunu yazıp yaprak denetimini unutmak ya da yaprak denetimini null denetiminden önce yazmak. Birincisinde sonuç her zaman 0'dır. İkincisinde k.sol okunurken k null olabilir ve NullPointerException fırlatılır. İki taban durum, kodda gösterilen sırayla yazılmalıdır.
9.6.8Gezinmelerden ağacı yeniden kurma
Bir ağacın tek bir gezinme çıktısı, ağacı belirlemez. Örneğin kök başta çıktısı 1 2 olan iki ağaç vardır: 2, 1'in sol çocuğu da olabilir sağ çocuğu da. Ama iki uygun gezinme birlikte verilirse ve düğüm değerleri birbirinden farklıysa ağaç tek biçimde kurulabilir.
Düğüm değerleri birbirinden farklı bir ikili ağaç, kök ortada çıktısı ile kök başta çıktısından (ya da kök ortada ile kök sonda çıktısından) tek biçimde kurulabilir. Kök başta ile kök sonda çıktıları ise, tek çocuklu düğüm bulunan ağaçlarda ağacı belirlemez.
Kurma yöntemi gezinmelerin tanımından çıkar. Kök başta çıktısının ilk öğesi köktür. Bu değer kök ortada çıktısında bulunur. Kök ortada çıktısında kökün solundaki öğeler sol alt ağacı, sağındakiler sağ alt ağacı oluşturur. Sol alt ağaçtaki öğe sayısı bilindiği için kök başta çıktısında da hangi öğelerin sol alt ağaca ait olduğu bulunur. Aynı işlem iki alt ağaca özyinelemeyle uygulanır. Kök sonda ile çalışırken tek fark, kökün çıktının son öğesi olmasıdır.
Kök başta ile kök sonda birlikte neden yetmez? İki gezinmede de kökün yeri bellidir, ama sol ve sağ alt ağaçları ayıran bir bilgi yoktur. Kök başta A B ve kök sonda B A çıktıları hem “B, A'nın sol çocuğu” hem de “B, A'nın sağ çocuğu” ağacına uyar. Ayrımı yalnız kök ortada çıktısı yapar: soldaki çocuk kökten önce, sağdaki sonra yazılır. Her düğümün 0 ya da 2 çocuğu olan dolu ağaçlarda ise belirsizlik kalkar; çünkü tek çocuklu düğüm yoktur.
int p = 0; // pre[] içinde sıradaki kök
AgacDugumu kur(int[] pre, int[] in, int bas, int son) {
if (bas > son) return null; // boş aralık
AgacDugumu k = new AgacDugumu(pre[p++]);
int m = bas; while (in[m] != k.veri) m++;
k.sol = kur(pre, in, bas, m - 1); // soldakiler
k.sag = kur(pre, in, m + 1, son); // sağdakiler
return k;
}kur(pre, in, bas, son), kök ortada dizisinin in[bas..son] aralığındaki öğelerden bir alt ağaç kurar ve kökünü döndürür. Aralık boşsa (bas > son) alt ağaç da boştur ve null döner. Değilse kök başta dizisinde sıradaki öğe, pre[p], bu alt ağacın köküdür; düğüm oluşturulur ve p bir artırılır. p bir alan olduğu için bütün çağrılar arasında paylaşılır ve kök başta dizisinde ileri doğru tek bir kez ilerler. Sonra kökün değeri kök ortada aralığında aranır ve indisi m'ye yazılır. in[bas..m−1] sol, in[m+1..son] sağ alt ağaçtır. Önce sol alt ağaç kurulur. Bu sıra zorunludur, çünkü kök başta dizisinde sol alt ağacın öğeleri sağdakilerden önce gelir.
pre = [1, 2, 4, 5, 3, 6] ve in = [4, 2, 5, 1, 3, 6] dizilerinden ağacı kuralım. İlk çağrı kur(pre, in, 0, 5)'tir.
pre[0] = 1. Kök 1'dir.in'de 1'in indisi m = 3'tür.in[0..2] = 4, 2, 5sol alt ağaç,in[4..5] = 3, 6sağ alt ağaçtır.pre[1] = 2. Sol alt ağacın kökü 2'dir; m = 1. Sol aralığı[0..0](4), sağ aralığı[2..2](5).pre[2] = 4. 2'nin sol çocuğu 4'tür; m = 0. İki aralık da boştur; iki çağrınulldöner. 4 yapraktır.pre[3] = 5. 2'nin sağ çocuğu 5'tir; m = 2; o da yapraktır. 2'nin alt ağacı tamamlanır ve1.sol = 2yapılır.pre[4] = 3. Sağ alt ağacın kökü 3'tür; m = 4. Sol aralık[4..3]boştur: 3'ün sol çocuğu yoktur. Sağ aralık[5..5]'tir.pre[5] = 6. 3'ün sağ çocuğu 6'dır; m = 5; yapraktır.1.sag = 3yapılır.- Sonuç. Ağaç 1(2(4, 5), 3(—, 6))'dır. Denetim: bu ağacın kök başta çıktısı
1 2 4 5 3 6, kök ortada çıktısı4 2 5 1 3 6'dır.
kur izlemesinin üç anı. Üstte kök 1 oluşturulmuş ve in dizisi 1'in iki yanında bölünmüştür. Ortada sol alt ağaç (2, 4, 5) kurulmuş, sağ alt ağacın kökü 3 için in[4..5] aralığına geçilmiştir. Altta ağaç tamamlanmıştır.Maliyeti belirleyen, m'yi bulan doğrusal aramadır. Her düğüm için kendi aralığında bir arama yapılır. Dengeli ağaçta aralıklar her seviyede yarıya iner ve toplam O(n log n) olur. Çarpık ağaçta ise aralıklar birer birer küçülür ve toplam O(n2)'ye çıkar. İyileştirme kolaydır: in dizisindeki her değerin indisi önceden bir eşlemeye (HashMap) yazılırsa m O(1)'de bulunur ve kurma O(n)'e iner.
Kök sonda ile kurarken sol alt ağacı önce kurmak. Kök sonda dizisi sondan başa okunduğunda önce kök, sonra sağ alt ağacın kökü gelir. Bu yüzden kök sonda ile kurulurken dizi sondan başa okunur ve önce sağ alt ağaç kurulmalıdır. Sıra karıştırılırsa sağ alt ağacın öğeleri sol alt ağaca yerleştirilir.
9.6.9Gezinmelerin maliyeti
Bütün gezinmeler her düğümü bir kez ziyaret eder; ziyaret başına iş sabittir. Zaman maliyetleri bu yüzden aynıdır: O(n). Gezinmeler ek bellekte ayrışır.
| Yöntem | Zaman | Ek bellek | Neden? |
|---|---|---|---|
| Özyinelemeli kök başta / ortada / sonda | O(n) | O(h) | Çağrı yığınında en çok h + 2 çerçeve (null çağrısı dahil) |
| Yığınla kök başta / ortada / sonda | O(n) | O(h) | Açık yığın, özyinelemenin yerini tutar |
| Kuyrukla seviye sırası | O(n) | O(w) | w: en geniş seviyedeki düğüm sayısı |
Derinlik öncelikli gezinmelerde belleği yükseklik belirler. Dengeli ağaçta h ≈ log2 n olduğundan bellek küçüktür. Çarpık ağaçta ise h = n − 1'dir ve bellek O(n)'e çıkar. Bu durumda özyinelemeli sürüm, çağrı yığınının sınırına takılıp StackOverflowError verebilir; yığınlı sürüm aynı belleği heap'te kullandığı için çalışmayı sürdürür.
Seviye sırasında belleği genişlik belirler. Mükemmel ağaçta son seviyede yaklaşık n/2 düğüm vardır; kuyruk bu seviyeyi tutarken O(n) bellek ister. Çarpık ağaçta ise her seviyede bir düğüm vardır ve kuyruk hiçbir zaman bir iki öğeden fazlasını tutmaz. İki aile böylece birbirinin tersi davranır. Derin ve dar ağaçta seviye sırası, geniş ve sığ ağaçta derinlik öncelikli gezinme daha az bellek harcar. Örneğin n düğümlü mükemmel ağaçta seviye sırasının ek belleği O(n), özyinelemeli kök ortadanın ek belleği O(log n)'dir.
9.7Sık yapılan hatalar
Ağaç kodlarındaki hataların çoğu birkaç tipe ayrılır. Aşağıdaki tablo, notun içinde ayrı ayrı anlatılan hataları bir arada verir; bir ağaç kodunu gözden geçirirken kontrol listesi olarak kullanılabilir.
| Hata | Ne olur? | Doğrusu |
|---|---|---|
| Taban durumu unutmak | k.sol'a erişmeden önce k == null denetlenmezse NullPointerException. | Her özyinelemeli ağaç metodu boş ağaç durumuyla başlar. |
| Yükseklik tanımını karıştırmak | Bütün değerler bir kayar. | Kenar sayısıyla: yaprak 0, boş ağaç −1. Düğüm sayısıyla: yaprak 1, boş ağaç 0. Bir tanım seçilip tutarlı kullanılmalıdır. |
| Dolu ile tamı aynı saymak | Yanlış sınıflandırma. | Dolu: her düğümde 0 ya da 2 çocuk. Tam: son seviye dışındakiler dolu, son seviye soldan dolar. Biri ötekini gerektirmez. |
| Tek gezinmeden ağaç kurmak | Birden çok ağaç aynı çıktıyı verir. | Kök ortada ile kök başta ya da kök sonda gerekir. Kök başta ile kök sonda, tek çocuklu düğümlerde belirsizdir. |
| İndis formüllerini karıştırmak | Yanlış çocuk ya da ebeveyn; kök kendini gösterebilir. | 1 tabanlı: 2i, 2i + 1, ⌊i/2⌋. 0 tabanlı: 2i + 1, 2i + 2, ⌊(i − 1)/2⌋. |
| Derin ağaçta özyineleme | Çarpık ağaçta h = n − 1; yüz binlerce çerçeve StackOverflowError verir. | Yığınlı ya da kuyruklu sürüm. |
| Yığınlı kök başta itme sırası | Önce sol itilirse çıktı kök, sağ, sol olur. | Önce sağ, sonra sol itilir. |
| Yanlış taban değeri | Boş ağaçta en büyük için 0, negatif değerlerde yanlış sonuç verir. | Taban değer birleştirme işleminin etkisiz öğesi olmalıdır (Integer.MIN_VALUE, 0, 1). |
Tablodaki hataların ortak bir kökü vardır: ağacın özyinelemeli yapısını kodda tam yansıtmamak. Bir ağaç ya boştur ya da bir kök ve iki alt ağaçtan oluşur. Doğru bir ağaç metodu bu iki durumu da açıkça ele alır. Boş ağaç için anlamlı bir değer döndürür. Boş olmayan ağaçta alt ağaçların sonuçlarını, kökün nerede yer alacağına dikkat ederek birleştirir. Gezinmeler arasındaki fark da yalnız bu son noktadadır: kök, alt ağaçların önünde mi, arasında mı, arkasında mı?
9.8Özet
- Ağaç, düğümler ve onları ebeveyn–çocuk ilişkisiyle bağlayan kenarlardan oluşan hiyerarşik bir yapıdır. Tek bir kök vardır; kök dışındaki her düğümün tam bir ebeveyni vardır. Bu yüzden döngü yoktur, n düğümlü ağaçta n − 1 kenar bulunur ve iki düğüm arasında tek bir yol vardır.
- İç düğüm en az bir çocuğu olan, yaprak hiç çocuğu olmayan düğümdür. Ebeveyn, çocuk, kardeş, ata ve soy aile ağacındaki anlamlarıyla kullanılır; aynı seviyede olmak kardeş olmak demek değildir.
- Derece çocuk sayısıdır. Derinlik kökten düğüme, yükseklik düğümden en uzak yaprağa kenar sayısıdır. Ağacın yüksekliği kökün yüksekliği, yani en büyük derinliktir. Düğüm sayısıyla ölçen kaynaklarda bütün değerler bir fazladır.
- İkili ağaçta her düğümün en çok iki çocuğu vardır ve sol ile sağ ayrılır. Dolu: her düğümde 0 ya da 2 çocuk. Tam: son seviye dışındakiler dolu, son seviye soldan dolar. Mükemmel: bütün yapraklar aynı derinlikte; hem dolu hem tam.
- İkili ağaçta d derinliğinde en çok 2d, yüksekliği h olan ağaçta en çok 2h+1 − 1 düğüm bulunur; ⌊log2 n⌋ ≤ h ≤ n − 1. Dolu ikili ağaçta yaprak sayısı iç düğüm sayısından bir fazladır.
- Dizi gösteriminde 1 tabanlı indis i'nin çocukları 2i ve 2i + 1, ebeveyni ⌊i/2⌋'dir; tam ağaçlarda (heap) idealdir, çarpık ağaçlarda 2n − 1 hücreye kadar israf eder. Bağlı gösterimde her düğüm sol ve sağ referans taşır; n + 1 referans
null'dır. Genel ağaçlar çocuk listesiyle ya da ilk çocuk – sonraki kardeş gösterimiyle saklanır. - Kök başta K-S-S, kök ortada S-K-S, kök sonda S-S-K sırasıyla gezer; üçü de aynı Euler turunun farklı anlarıdır. Seviye sırası ağacı seviye seviye bir kuyrukla tarar.
- Özyinelemeli gezinmeler çağrı yığınında en çok h + 2 çerçeve kullanır. Açık yığınlı sürümler aynı işi heap bellekte yapar: kök başta için önce sağ, sonra sol itilir; kök ortada sola iner ve çıkarırken yazar; kök sonda
peekile sağ alt ağacın bitip bitmediğine bakar. - İfade ağacında kök başta önek, kök sonda sonek, parantezli kök ortada ara ek gösterimi verir; değer kök sonda mantığıyla hesaplanır.
boyut,yukseklik,yaprakSayisiveenBuyukBulaynı kalıbı izler: boş ağaç için taban değer, sonra alt ağaç sonuçlarının birleştirilmesi. - Kök ortada ile kök başta (ya da kök sonda) çıktıları, değerleri farklı bir ikili ağacı tek biçimde belirler. Bütün gezinmeler O(n) zaman ister; ek bellek derinlik öncelikli gezinmelerde O(h), seviye sırasında O(w)'dir.
| Konu | Akılda kalması gerekenler |
|---|---|
| Terimler | kök, ebeveyn/çocuk, yaprak, iç düğüm; n düğüm → n − 1 kenar |
| Ölçüler | derece = çocuk sayısı; derinlik = kökten kenar sayısı; yükseklik = en uzun aşağı yol |
| İkili ağaç | dolu: 0/2 çocuk · tam: son seviye soldan dolar · mükemmel: bütün seviyeler dolu |
| Gösterim | dizi: indis bağıntıları · bağlı: sol/sağ referansları |
| Gezinme | kök başta K-S-S · kök ortada S-K-S · kök sonda S-S-K · seviye sırası kuyrukla |
| Maliyet | bütün gezinmeler O(n); ek bellek O(h) (yığın) ya da O(w) (kuyruk) |
9.9Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- ağaç tree
- düğüm node
- kenar edge
- kök root
- ebeveyn parent
- çocuk child
- kardeş sibling
- ata ancestor
- soy descendant
- yaprak leaf
- iç düğüm internal node
- derece degree
- derinlik depth
- seviye level
- yükseklik height
- yol path
- alt ağaç subtree
- döngü cycle
- konum position
- soyut veri tipi abstract data type (ADT)
- ikili ağaç binary tree
- dolu ikili ağaç full binary tree
- tam ikili ağaç complete binary tree
- mükemmel ikili ağaç perfect binary tree
- ikili arama ağacı binary search tree (BST)
- heap heap
- gezinme traversal
- kök başta preorder
- kök ortada inorder
- kök sonda postorder
- seviye sırası level-order
- genişlik öncelikli arama breadth-first search (BFS)
- derinlik öncelikli arama depth-first search (DFS)
- ifade ağacı expression tree
- özyineleme recursion
- taban durum base case
- çağrı yığını call stack
- çerçeve stack frame
- kuyruk queue
9.10Kendini Dene
Bir genel ağaçta A köktür; A'nın çocukları B ve C, B'nin çocukları D, E, F, C'nin çocuğu G, G'nin çocukları H ve I'dır. Yapraklar ve iç düğümler hangileridir? Ağacın derecesi, kenar sayısı ve yüksekliği kaçtır? G'nin derinliği ve yüksekliği nedir?
Cevap
Yapraklar D, E, F, H, I; iç düğümler A, B, C, G'dir (5 + 4 = 9 düğüm). En büyük derece B'nindir (3); ağacın derecesi 3'tür. Kenar sayısı 9 − 1 = 8'dir. En uzun yol A→C→G→H'dir; ağacın yüksekliği 3'tür. G'nin derinliği 2 (A→C→G), yüksekliği 1'dir (G→H).
Yüksekliği 5 olan bir ikili ağaçta en az ve en çok kaç düğüm bulunabilir? 100 düğümlü bir ikili ağacın yüksekliği en az kaç olabilir?
Cevap
En az h + 1 = 6 (zincir), en çok 26 − 1 = 63 düğüm (mükemmel ağaç). 100 düğüm için yükseklik 5 yetmez (en çok 63 düğüm), 6 yeter (en çok 127 düğüm); en az yükseklik ⌊log2 100⌋ = 6'dır.
21 düğümlü dolu bir ikili ağaçta kaç yaprak ve kaç iç düğüm vardır? Aynı ağaç bağlı gösterimde saklanırsa kaç
nullçocuk referansı bulunur?Cevap
n = 2i + 1'den i = 10, y = i + 1 = 11'dir.
nullreferans sayısı n + 1 = 22'dir. Denetim: her yaprağın ikinullreferansı vardır, 2 · 11 = 22; iç düğümlerin iki çocuğu da dolu olduğu için başkanullyoktur.0 tabanlı dizi gösteriminde indis 9'daki düğümün ebeveyninin ve çocuklarının indisleri nedir? Aynı düğüm 1 tabanlı dizide hangi indiste durur ve bu indislerin karşılıkları nedir?
Cevap
0 tabanlı: ebeveyn ⌊(9 − 1)/2⌋ = 4, sol çocuk 2 · 9 + 1 = 19, sağ çocuk 20. 1 tabanlıda aynı düğüm indis 10'dadır: ebeveyn ⌊10/2⌋ = 5, çocuklar 20 ve 21. Her 1 tabanlı indis, 0 tabanlı karşılığından tam bir fazladır.
Kök 10; 10'un çocukları 5 ve 15; 5'in çocukları 2 ve 7; 7'nin yalnız sol çocuğu 6; 15'in yalnız sağ çocuğu 20 olan ağacın kök başta, kök ortada, kök sonda ve seviye sırası çıktıları nedir? Bu ağaç bir ikili arama ağacı mıdır?
Cevap
Kök başta:
10 5 2 7 6 15 20. Kök ortada:2 5 6 7 10 15 20. Kök sonda:2 6 7 5 20 15 10. Seviye sırası:10 5 15 2 7 20 6. Kök ortada çıktısı küçükten büyüğe sıralı olduğu için ağaç bir ikili arama ağacıdır.Bir önceki sorudaki ağaçta
kokBastaYiginçalıştırılıyor. Her turun sonunda yığında (alttan üste) neler vardır? Yığın en çok kaç öğe tutar?Cevap
Başta
[10]. 10 çıkar, 15 ve 5 itilir:[15, 5]. 5 çıkar, 7 ve 2 itilir:[15, 7, 2]. 2 çıkar:[15, 7]. 7 çıkar, 6 itilir:[15, 6]. 6 çıkar:[15]. 15 çıkar, 20 itilir:[20]. 20 çıkar:[]. Yığın en çok 3 öğe tutar; çıktı kök başta sırasıdır.((9 − 3) / (1 + 2)) × 4 ifadesinin ağacında kök hangi işleçtir? Önek ve sonek gösterimleri nedir,
hesaplahangi değeri döndürür?Cevap
En son yapılan işlem çarpma olduğu için kök
×'tir; sol alt ağacın kökü/, sağ çocuğu 4'tür. Önek:× / − 9 3 + 1 2 4. Sonek:9 3 − 1 2 + / 4 ×.hesaplaönce 9 − 3 = 6, sonra 1 + 2 = 3, 6 / 3 = 2, en son 2 × 4 = 8 hesaplar ve 8 döndürür.Kök ortada çıktısı
8 4 9 2 5 1 3 7, kök sonda çıktısı8 9 4 5 2 7 3 1olan ağaç hangisidir? Kök başta çıktısı nedir?Cevap
Kök sonda çıktısının son öğesi kök: 1. Kök ortada 1'in solu
8 4 9 2 5, sağı3 7'dir. Sağ alt ağaçta kök sonda sırasının sondan bir önceki öğesi 3 köktür; 7 onun sağındadır. Sol alt ağaçta kök 2'dir; solunda8 4 9, sağında 5 vardır.8 4 9'un kökü 4'tür; 8 sol, 9 sağ çocuktur. Ağaç 1(2(4(8, 9), 5), 3(—, 7))'dir. Kök başta:1 2 4 8 9 5 3 7.Kök başta çıktısı
A B, kök sonda çıktısıB Aolan kaç farklı ikili ağaç vardır? Hangi ek bilgi ağacı belirler?Cevap
İki ağaç vardır: B, A'nın sol çocuğu ya da sağ çocuğu olabilir; iki ağacın kök başta ve kök sonda çıktıları aynıdır. Kök ortada çıktısı ayrımı yapar: B sol çocuksa
B A, sağ çocuksaA Bçıkar. Belirsizliğin nedeni A'nın tek çocuklu olmasıdır.yukseklikmetodunda taban durumreturn -1yerinereturn 0yazılmış olsun. 9 kökündeki dört düğümlü ağaçta (9'un çocukları 2 ve 3; 2'nin sol çocuğu 4) metot ne döndürür? Bir yaprak için ne döndürür?Cevap
Yaprak için 1 + max(0, 0) = 1 döner. 2 için 1 + max(1, 0) = 2, 3 için 1, kök için 1 + max(2, 1) = 3 döner. Metot artık yüksekliği düğüm sayısıyla ölçer: her değer kenar tanımındakinden bir fazladır (doğru yanıt 2'dir).
9.11İleri okuma
- M. T. Goodrich, R. Tamassia ve M. H. Goldwasser, Data Structures and Algorithms in Java (6. baskı, 2014), Bölüm 8: genel ve ikili ağaç ADT'lerini konum soyutlamasıyla tanımlar, bağlı ve dizi gerçeklemelerini verir; gezinmeleri, Euler turunu ve ifade ağaçlarını bu nottakine yakın bir Java koduyla anlatır.
- D. E. Knuth, The Art of Computer Programming, Vol. 1: Fundamental Algorithms (3. baskı, 1997), Kesim 2.3: ağaç gezinmelerinin klasik incelemesidir; özyinelemesiz gezinme algoritmalarını, iplikli ikili ağaçları ve genel ağaçların ikili ağaçlarla gösterimini ayrıntılı işler.
- T. H. Cormen, C. E. Leiserson, R. L. Rivest ve C. Stein, Introduction to Algorithms (4. baskı, 2022), Bölüm 10'daki köklü ağaçların gösterimi kesimi ve Ek B.5: ilk çocuk – sonraki kardeş gösterimini ve ağaçlarla ilgili tanım ve sayma sonuçlarını daha biçimsel bir dille verir.
9.12Kaynaklar
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022.
- M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6. baskı, Wiley, 2014.
- D. E. Knuth, The Art of Computer Programming, Vol. 1: Fundamental Algorithms, 3. baskı, Addison-Wesley, 1997.
- Oracle, Java SE 17 API Specification:
java.util.Deque,java.util.ArrayDeque,java.util.Queuevejava.util.LinkedListbelgeleri. - R. Sedgewick, K. Wayne, Algorithms, 4. baskı, Addison-Wesley, 2011.
İlgili sayfalar: Bölüm 9 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.