go
Veri Yapıları dersleri
Veri Yapıları/Ağaçlar

Ağaçlar ve İkili Ağaçlar

Ağaç terminolojisi, ikili ağaçlar ve gezinme yöntemleri.

Ders 7 / 1430 dkOrta
Bu derste öğreneceklerin
  • Kök, yaprak, derinlik ve yükseklik
  • İkili ağaç türleri: tam, dolu, mükemmel
  • Preorder, inorder ve postorder gezinme
  • Seviye sıralı (level-order) gezinme
  • Özyinelemeli ve iteratif gezinme
  • Yükseklik, düğüm sayısı ve ayna ağaç

Şimdiye kadar gördüğün veri yapıları doğrusaldı: Diziler, bağlı listeler, yığınlar ve kuyruklarda her elemanın en fazla bir öncesi ve bir sonrası vardı. Ağaç bu kısıtı kırar: Bir elemanın birden çok "sonraki"si olabilir. Bu küçük değişiklik, tamamen yeni bir problem sınıfını çözmeyi mümkün kılar.

Aslında ağaçlar her gün kullandığın pek çok şeyin altında durur. Dosya sistemi bir ağaçtır: Klasörler alt klasörler içerir. Bir web sayfasının yapısı bir ağaçtır. Bir programın sözdizimi, derleyicinin ürettiği soyut sözdizimi ağacıdır. Şirket organizasyon şemaları, aile soy ağaçları, oyunlardaki karar ağaçları, veritabanı indeksleri — hepsi aynı fikrin farklı yüzleridir.

Ağaçların algoritmik cazibesi ise şudur: Dengeli bir ağaçta yükseklik, düğüm sayısının logaritması kadardır. Bir milyon düğümlü dengeli bir ikili ağaçta kökten yaprağa yalnızca yirmi adım vardır. Bu, arama ve güncelleme işlemlerini O(log n)'e indirir ve dizilerin O(n) ile hash tablolarının sırasızlığı arasındaki boşluğu doldurur. Bu derste ağaç terminolojisini, ikili ağaç türlerini, dört temel gezinme yöntemini ve özyinelemeli/iteratif uygulamaları öğreneceksin.

Terminoloji

                 ┌─────┐
                 │  A  │  ← kök (root), derinlik 0
                 └──┬──┘
          ┌─────────┴─────────┐
       ┌──▼──┐             ┌──▼──┐
       │  B  │             │  C  │  ← A'nın çocukları, derinlik 1
       └──┬──┘             └──┬──┘
      ┌───┴───┐               │
   ┌──▼──┐ ┌──▼──┐         ┌──▼──┐
   │  D  │ │  E  │         │  F  │  ← yapraklar, derinlik 2
   └─────┘ └─────┘         └─────┘

kök (root)      : ebeveyni olmayan tek düğüm (A)
ebeveyn (parent): bir düğümün bir üstü (B'nin ebeveyni A)
çocuk (child)   : bir düğümün bir altı (D ve E, B'nin çocukları)
kardeş (sibling): aynı ebeveyni paylaşan düğümler (D ve E)
yaprak (leaf)   : hiç çocuğu olmayan düğüm (D, E, F)
iç düğüm        : en az bir çocuğu olan düğüm (A, B, C)
derinlik (depth): kökten o düğüme kadar olan kenar sayısı
yükseklik       : o düğümden en uzak yaprağa kadar olan kenar sayısı
ağacın yüksekliği: kökün yüksekliği (burada 2)
alt ağaç        : bir düğüm ve altındaki her şey

İki tanımın karıştırılması çok yaygındır: Derinlik yukarıdan aşağıya, yükseklik aşağıdan yukarıya ölçülür. Kökün derinliği 0, yaprakların yüksekliği 0'dır.

Bir ağacın tanımı gereği iki özelliği vardır: Döngü içermez ve n düğümlü bir ağaçta tam olarak n−1 kenar bulunur. Döngü içeren bir yapı artık ağaç değil, graf olur.

İkili ağaçlar ve türleri

İkili ağaç (binary tree), her düğümün en fazla iki çocuğu olan ağaçtır. Çocuklara sol ve sağ denir ve bu sıra anlamlıdır — sol ile sağı değiştirirsen farklı bir ağaç elde edersin.

Belirli özellikleri sağlayan ikili ağaçların özel adları vardır:

TürTanımNeden önemli
Dolu (full)Her düğümün ya 0 ya 2 çocuğu varİfade ağaçlarında doğal
Tam (complete)Son seviye hariç tüm seviyeler dolu, son seviye soldan sağa dolduruluyorDiziyle temsil edilebilir → heap
Mükemmel (perfect)Tüm iç düğümlerin 2 çocuğu var, tüm yapraklar aynı seviyedeTeorik en iyi durum
DengeliHer düğümde sol ve sağ alt ağaç yükseklikleri en fazla 1 fark ederO(log n) garantisi
DejenereHer düğümün tek çocuğu varBağlı listeye eşdeğer: O(n)
   Mükemmel          Tam              Dolu           Dejenere
      A               A                A                A
    ┌─┴─┐           ┌─┴─┐            ┌─┴─┐              │
    B   C           B   C            B   C              B
   ┌┴┐ ┌┴┐         ┌┴┐               ┌─┴─┐              │
   D E F G         D E               D   E              C

                                                        D

Yükseklik ile düğüm sayısı arasındaki ilişki, ağaçların tüm gücünün kaynağıdır:

h yükseklikli bir ikili ağaçta:
  en az  h + 1 düğüm  (dejenere)
  en çok 2^(h+1) − 1 düğüm  (mükemmel)

n düğümlü bir ikili ağaçta:
  en iyi durum yükseklik: ⌊log₂ n⌋      → dengeli
  en kötü durum yükseklik: n − 1        → dejenere

Bu yüzden ağaç tabanlı yapıların hepsinde temel mesele dengeyi korumaktır. Dengesiz bir ağaç, tüm logaritmik vaadini kaybeder. Dengelemenin nasıl yapıldığını AVL Ağaçları dersinde göreceksin.

Ağacı Go'da temsil etmek

En doğal temsil, her düğümün çocuklarına işaretçi tuttuğu yapıdır:

main.go
package main

import "fmt"

type Node struct {
	Value       int
	Left, Right *Node
}

// build, dilimden tam (complete) bir ikili ağaç kurar.
// Dizi temsilinde: i. düğümün çocukları 2i+1 ve 2i+2'dir.
func build(values []int, i int) *Node {
	if i >= len(values) {
		return nil
	}
	return &Node{
		Value: values[i],
		Left:  build(values, 2*i+1),
		Right: build(values, 2*i+2),
	}
}

// size: düğüm sayısı
func size(n *Node) int {
	if n == nil {
		return 0
	}
	return 1 + size(n.Left) + size(n.Right)
}

// height: kenar cinsinden yükseklik (tek düğümlü ağaç için 0)
func height(n *Node) int {
	if n == nil {
		return -1
	}
	return 1 + max(height(n.Left), height(n.Right))
}

// leaves: yaprak sayısı
func leaves(n *Node) int {
	if n == nil {
		return 0
	}
	if n.Left == nil && n.Right == nil {
		return 1
	}
	return leaves(n.Left) + leaves(n.Right)
}

// isBalanced: her düğümde alt ağaç yükseklik farkı ≤ 1 mi
func isBalanced(n *Node) bool {
	var check func(*Node) (int, bool)
	check = func(n *Node) (int, bool) {
		if n == nil {
			return -1, true
		}
		lh, lok := check(n.Left)
		rh, rok := check(n.Right)
		if !lok || !rok || lh-rh > 1 || rh-lh > 1 {
			return 0, false
		}
		return 1 + max(lh, rh), true
	}
	_, ok := check(n)
	return ok
}

func main() {
	tree := build([]int{1, 2, 3, 4, 5, 6, 7}, 0)

	fmt.Println("düğüm sayısı:", size(tree))
	fmt.Println("yükseklik:", height(tree))
	fmt.Println("yaprak sayısı:", leaves(tree))
	fmt.Println("dengeli mi:", isBalanced(tree))

	// Dejenere ağaç: her düğümün tek çocuğu
	degenerate := &Node{Value: 1,
		Right: &Node{Value: 2,
			Right: &Node{Value: 3,
				Right: &Node{Value: 4}}}}

	fmt.Println()
	fmt.Println("dejenere — düğüm:", size(degenerate), "yükseklik:", height(degenerate))
	fmt.Println("dejenere dengeli mi:", isBalanced(degenerate))
	fmt.Println("boş ağaç yüksekliği:", height(nil))
}
Çıktı
düğüm sayısı: 7
yükseklik: 2
yaprak sayısı: 4
dengeli mi: true

dejenere — düğüm: 4 yükseklik: 3
dejenere dengeli mi: false
boş ağaç yüksekliği: -1

isBalanced fonksiyonundaki yaklaşım dikkate değer: Yükseklik hesabıyla denge kontrolünü tek bir geçişte birleştirir. Her düğüm için ayrı ayrı yükseklik hesaplasaydın O(n²) olurdu; böyle yazınca O(n)'de kalır. Bu, ağaç problemlerinde sık kullanılan bir tekniktir — özyinelemeden birden fazla bilgi döndür.

Gezinme (traversal) yöntemleri

Bir ağacın tüm düğümlerini ziyaret etmenin dört standart yolu vardır. İlk üçü derinlik öncelikli (DFS), dördüncüsü genişlik önceliklidir (BFS).

        A
      ┌─┴─┐
      B   C
    ┌─┴─┐  ┌┴┐
    D   E  F G

Preorder  (kök → sol → sağ) : A B D E C F G
Inorder   (sol → kök → sağ) : D B E A F C G
Postorder (sol → sağ → kök) : D E B F G C A
Level     (seviye seviye)   : A B C D E F G

Hangisini seçeceğin, ne yapmak istediğine bağlıdır:

  • Preorder: Kökü çocuklarından önce işlemek gerektiğinde. Ağacı kopyalamak, dizinleri listelemek, ifadeyi ön ek gösterimine çevirmek.
  • Inorder: İkili arama ağacında elemanları sıralı vermek. Bu, BST'nin tanımlayıcı özelliğidir.
  • Postorder: Çocukları kökten önce işlemek gerektiğinde. Ağacı silmek, dizin boyutunu hesaplamak, ifadeyi değerlendirmek.
  • Level-order: Seviye seviye işlem, en kısa yol, ağacı seviyelere göre yazdırmak.

Aşağıdaki görselleştirmede farklı gezinme türlerini seçip ziyaret sırasını adım adım izleyebilirsin:

main.go
package main

import (
	"fmt"
	"strings"
)

type Node struct {
	Value       string
	Left, Right *Node
}

func build() *Node {
	return &Node{"A",
		&Node{"B", &Node{"D", nil, nil}, &Node{"E", nil, nil}},
		&Node{"C", &Node{"F", nil, nil}, &Node{"G", nil, nil}},
	}
}

func preorder(n *Node, visit func(string)) {
	if n == nil {
		return
	}
	visit(n.Value) // önce kök
	preorder(n.Left, visit)
	preorder(n.Right, visit)
}

func inorder(n *Node, visit func(string)) {
	if n == nil {
		return
	}
	inorder(n.Left, visit)
	visit(n.Value) // ortada kök
	inorder(n.Right, visit)
}

func postorder(n *Node, visit func(string)) {
	if n == nil {
		return
	}
	postorder(n.Left, visit)
	postorder(n.Right, visit)
	visit(n.Value) // sonda kök
}

// levelOrder: kuyruk kullanır — BFS
func levelOrder(root *Node) [][]string {
	if root == nil {
		return nil
	}
	var levels [][]string
	queue := []*Node{root}

	for len(queue) > 0 {
		var level []string
		var next []*Node
		for _, n := range queue {
			level = append(level, n.Value)
			if n.Left != nil {
				next = append(next, n.Left)
			}
			if n.Right != nil {
				next = append(next, n.Right)
			}
		}
		levels = append(levels, level)
		queue = next
	}
	return levels
}

func collect(walk func(*Node, func(string)), root *Node) string {
	var out []string
	walk(root, func(v string) { out = append(out, v) })
	return strings.Join(out, " ")
}

func main() {
	tree := build()

	fmt.Println("preorder :", collect(preorder, tree))
	fmt.Println("inorder  :", collect(inorder, tree))
	fmt.Println("postorder:", collect(postorder, tree))

	fmt.Println()
	for i, level := range levelOrder(tree) {
		fmt.Printf("seviye %d: %v\n", i, level)
	}
}
Çıktı
preorder : A B D E C F G
inorder  : D B E A F C G
postorder: D E B F G C A

seviye 0: [A]
seviye 1: [B C]
seviye 2: [D E F G]
ZamanO(n)AlanO(h) DFS / O(w) BFS

Alan karmaşıklığındaki fark önemlidir. DFS, çağrı yığınında en fazla ağacın yüksekliği kadar çerçeve tutar: dengeli ağaçta O(log n), dejenere ağaçta O(n). BFS ise kuyruğunda en geniş seviyenin genişliği kadar düğüm tutar: mükemmel bir ağaçta bu, son seviyedeki düğüm sayısı yani yaklaşık n/2'dir. Yani geniş ve sığ ağaçlarda DFS, derin ve dar ağaçlarda BFS daha az bellek kullanır.

İteratif gezinme

Özyineleme zariftir ama çağrı yığınını kullanır ve derin ağaçlarda taşabilir. Aynı gezinmeleri kendi yığınını yöneterek iteratif de yazabilirsin:

main.go
package main

import (
	"fmt"
	"strings"
)

type Node struct {
	Value       string
	Left, Right *Node
}

func build() *Node {
	return &Node{"A",
		&Node{"B", &Node{"D", nil, nil}, &Node{"E", nil, nil}},
		&Node{"C", &Node{"F", nil, nil}, &Node{"G", nil, nil}},
	}
}

// iterativePreorder: yığına önce SAĞ çocuğu koy ki sol önce çıksın
func iterativePreorder(root *Node) []string {
	if root == nil {
		return nil
	}
	var out []string
	stack := []*Node{root}

	for len(stack) > 0 {
		n := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		out = append(out, n.Value)

		if n.Right != nil {
			stack = append(stack, n.Right)
		}
		if n.Left != nil {
			stack = append(stack, n.Left)
		}
	}
	return out
}

// iterativeInorder: sola in, düğümü işle, sağa geç
func iterativeInorder(root *Node) []string {
	var out []string
	var stack []*Node
	cur := root

	for cur != nil || len(stack) > 0 {
		for cur != nil { // sola git, yol boyunca yığına koy
			stack = append(stack, cur)
			cur = cur.Left
		}
		cur = stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		out = append(out, cur.Value)
		cur = cur.Right
	}
	return out
}

// iterativePostorder: preorder'ın aynasını alıp ters çevir
func iterativePostorder(root *Node) []string {
	if root == nil {
		return nil
	}
	var out []string
	stack := []*Node{root}

	for len(stack) > 0 {
		n := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		out = append(out, n.Value)

		if n.Left != nil { // sol önce: sonuç ters çevrilecek
			stack = append(stack, n.Left)
		}
		if n.Right != nil {
			stack = append(stack, n.Right)
		}
	}
	// kök-sağ-sol → ters → sol-sağ-kök
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out
}

func main() {
	tree := build()

	fmt.Println("iteratif preorder :", strings.Join(iterativePreorder(tree), " "))
	fmt.Println("iteratif inorder  :", strings.Join(iterativeInorder(tree), " "))
	fmt.Println("iteratif postorder:", strings.Join(iterativePostorder(tree), " "))

	fmt.Println("boş ağaç:", iterativePreorder(nil) == nil)
}
Çıktı
iteratif preorder : A B D E C F G
iteratif inorder  : D B E A F C G
iteratif postorder: D E B F G C A
boş ağaç: true

Preorder'daki sıraya dikkat: Yığın LIFO olduğu için sağ çocuğu önce koyarsın ki sol çocuk önce çıksın. Postorder'daki numara ise daha zariftir: Kök-sağ-sol sırasında gezip sonucu ters çevirirsen sol-sağ-kök elde edersin.

Bu dönüşümler, Yığın (Stack) dersinde gördüğün "çağrı yığınını elle yönetme" fikrinin doğrudan uygulamasıdır.

Klasik ağaç problemleri

Ağaç problemlerinin çoğu aynı kalıba uyar: Problemi alt ağaçlar için çöz, sonuçları birleştir. Bu, özyinelemenin en doğal kullanım alanıdır.

main.go
package main

import (
	"fmt"
	"strings"
)

type Node struct {
	Value       int
	Left, Right *Node
}

func build(values []int, i int) *Node {
	if i >= len(values) || values[i] == -1 {
		return nil
	}
	return &Node{values[i], build(values, 2*i+1), build(values, 2*i+2)}
}

// mirror: ağacın aynasını üretir (özgün ağacı değiştirmez)
func mirror(n *Node) *Node {
	if n == nil {
		return nil
	}
	return &Node{Value: n.Value, Left: mirror(n.Right), Right: mirror(n.Left)}
}

// isSymmetric: ağaç kendi aynası mı
func isSymmetric(root *Node) bool {
	var same func(a, b *Node) bool
	same = func(a, b *Node) bool {
		if a == nil || b == nil {
			return a == b
		}
		return a.Value == b.Value && same(a.Left, b.Right) && same(a.Right, b.Left)
	}
	if root == nil {
		return true
	}
	return same(root.Left, root.Right)
}

// maxDepth: en derin yolun düğüm sayısı
func maxDepth(n *Node) int {
	if n == nil {
		return 0
	}
	return 1 + max(maxDepth(n.Left), maxDepth(n.Right))
}

// sumPaths: kökten yaprağa giden tüm yolları toplar
func sumPaths(n *Node, current int, paths *[]string, sum int) {
	if n == nil {
		return
	}
	current = current*10 + n.Value
	if n.Left == nil && n.Right == nil {
		*paths = append(*paths, fmt.Sprint(current))
		return
	}
	sumPaths(n.Left, current, paths, sum)
	sumPaths(n.Right, current, paths, sum)
}

// diameter: ağaçtaki en uzun yolun kenar sayısı (kökten geçmek zorunda değil)
func diameter(root *Node) int {
	best := 0
	var depth func(*Node) int
	depth = func(n *Node) int {
		if n == nil {
			return 0
		}
		l, r := depth(n.Left), depth(n.Right)
		best = max(best, l+r) // bu düğümden geçen en uzun yol
		return 1 + max(l, r)
	}
	depth(root)
	return best
}

func inorderString(n *Node) string {
	if n == nil {
		return ""
	}
	parts := []string{inorderString(n.Left), fmt.Sprint(n.Value), inorderString(n.Right)}
	return strings.TrimSpace(strings.Join(parts, " "))
}

func main() {
	tree := build([]int{1, 2, 3, 4, 5, 6, 7}, 0)
	fmt.Println("özgün inorder:", inorderString(tree))
	fmt.Println("ayna inorder :", inorderString(mirror(tree)))

	fmt.Println("maksimum derinlik:", maxDepth(tree))
	fmt.Println("çap (kenar):", diameter(tree))

	var paths []string
	sumPaths(tree, 0, &paths, 0)
	fmt.Println("kökten yaprağa yollar:", paths)

	// Simetrik ağaç
	sym := build([]int{1, 2, 2, 3, 4, 4, 3}, 0)
	fmt.Println()
	fmt.Println("simetrik ağaç mı:", isSymmetric(sym))
	fmt.Println("özgün ağaç simetrik mi:", isSymmetric(tree))
	fmt.Println("boş ağaç simetrik mi:", isSymmetric(nil))
}
Çıktı
özgün inorder: 4 2 5 1 6 3 7
ayna inorder : 7 3 6 1 5 2 4
maksimum derinlik: 3
çap (kenar): 4
kökten yaprağa yollar: [124 125 136 137]

simetrik ağaç mı: true
özgün ağaç simetrik mi: false
boş ağaç simetrik mi: true

diameter fonksiyonundaki desen özellikle öğreticidir: Özyineleme bir şey döndürürken (alt ağacın derinliği) yan etki olarak başka bir şeyi günceller (şimdiye kadarki en uzun yol). Bu ikili yapı, ağaç problemlerinin belki de yarısında karşına çıkar.

Ağaçları anlamanın anahtarı

Ağaç problemlerine yeni başlayanların en çok zorlandığı yer, özyinelemenin nereden başlayacağını kestirememektir. Birkaç düşünme alışkanlığı bu süreci belirgin biçimde kolaylaştırır.

Temel durumu önce yaz. Neredeyse her ağaç fonksiyonu if n == nil ile başlar. Boş ağaç için doğru cevabın ne olduğunu düşünmek, çoğu zaman problemin geri kalanını da netleştirir: Düğüm sayısı için sıfır, yükseklik için eksi bir, "hepsi şu koşulu sağlıyor mu" için doğru.

Alt ağaçların cevabını hazır varsay. Özyinelemeli düşünmenin püf noktası, alt problemlerin nasıl çözüldüğünü kafanda canlandırmaya çalışmamaktır. "Sol alt ağacın cevabını biliyorum, sağ alt ağacınkini de biliyorum — bu düğümün cevabını nasıl üretirim?" sorusunu yanıtlamak yeterlidir. Bu soruya yanıt verebiliyorsan fonksiyon zaten yazılmış demektir.

Ne döndürmen gerektiğine dikkatlice karar ver. Bazı problemlerde tek bir sayı yeterlidir; bazılarında iki bilgiyi birden taşımak gerekir. Denge kontrolünde hem yüksekliği hem dengeli olup olmadığını döndürmek, iki ayrı geçişi tek geçişe indirir. Cevabı bir işaretçi ya da closure ile dışarıda biriktirmek de yaygın bir çözümdür.

Hangi gezinme sırasının uygun olduğunu sor. Cevabın çocuklara bağlıysa postorder düşün: Önce alt ağaçları çöz, sonra birleştir. Kökten aşağıya bilgi taşıman gerekiyorsa preorder düşün: Bilgiyi parametre olarak aşağı geçir. Seviyeler önemliyse BFS kullan.

Sınır durumlarını sına. Boş ağaç, tek düğüm, yalnızca sola uzayan ağaç, tam ağaç. Ağaç kodundaki hataların neredeyse tamamı bu dört durumdan birinde ortaya çıkar.

Bu alışkanlıkları edindiğinde, ağaç problemleri şaşırtıcı derecede tekdüze hâle gelir. Aradaki fark genelde yalnızca birleştirme adımındadır.

Sık yapılan hatalar

  • Yükseklik ile derinliği karıştırmak. Derinlik yukarıdan, yükseklik aşağıdan ölçülür.
  • Boş ağaç durumunu atlamak. nil kontrolü yapılmayan özyineleme panik oluşturur.
  • Yüksekliği kenar mı düğüm mü saydığını belirtmemek. İki tanım da kullanılır; kod içinde tutarlı ol.
  • Her düğümde yüksekliği yeniden hesaplamak. Denge kontrolü O(n²)'ye çıkar; tek geçişte birlikte hesapla.
  • İteratif preorder'da çocukları yanlış sırada yığına koymak. Sağ önce konmalıdır ki sol önce çıksın.
  • Dejenere ağaç riskini göz ardı etmek. Sıralı veri eklenirse ağaç bağlı listeye döner ve O(n)'e düşer.
  • Derin ağaçlarda özyineleme sınırını unutmak. Girdi kontrolsüzse iteratif sürümü tercih et.

Alıştırmalar

Alıştırma·Seviye toplamları
Kolay

Bir ikili ağacın her seviyesindeki düğüm değerlerinin toplamını hesaplayan bir fonksiyon yaz. Ayrıca en büyük toplama sahip seviyenin numarasını da bul.

İpucu

Seviye sıralı gezinme (BFS) kullan: Her turda o seviyedeki tüm düğümleri işle ve çocuklarını bir sonraki tur için biriktir.

Çözümü göster
main.go
package main

import "fmt"

type Node struct {
	Value       int
	Left, Right *Node
}

func build(values []int, i int) *Node {
	if i >= len(values) || values[i] == -1 {
		return nil
	}
	return &Node{values[i], build(values, 2*i+1), build(values, 2*i+2)}
}

func levelSums(root *Node) []int {
	if root == nil {
		return nil
	}
	var sums []int
	queue := []*Node{root}

	for len(queue) > 0 {
		sum := 0
		var next []*Node
		for _, n := range queue {
			sum += n.Value
			if n.Left != nil {
				next = append(next, n.Left)
			}
			if n.Right != nil {
				next = append(next, n.Right)
			}
		}
		sums = append(sums, sum)
		queue = next
	}
	return sums
}

func maxLevel(sums []int) (level, value int) {
	for i, s := range sums {
		if i == 0 || s > value {
			level, value = i, s
		}
	}
	return level, value
}

func main() {
	tree := build([]int{5, 3, 8, 1, 4, -1, 12, 9, -1, 2}, 0)

	sums := levelSums(tree)
	for i, s := range sums {
		fmt.Printf("seviye %d toplam: %d\n", i, s)
	}

	level, value := maxLevel(sums)
	fmt.Printf("en büyük toplam seviye %d'de: %d\n", level, value)

	fmt.Println("boş ağaç:", levelSums(nil))
	fmt.Println("tek düğüm:", levelSums(&Node{Value: 42}))
}
Çıktı
seviye 0 toplam: 5
seviye 1 toplam: 11
seviye 2 toplam: 17
seviye 3 toplam: 11
en büyük toplam seviye 2'de: 17
boş ağaç: []
tek düğüm: [42]
ZamanO(n)AlanO(w)

BFS'te seviyeleri ayırmanın anahtarı, bir turda o anki kuyruğun tamamını işleyip çocukları ayrı bir dilimde biriktirmektir. Alternatif olarak kuyruk uzunluğunu tur başında kaydedip o kadar eleman işleyebilirsin; ikisi de aynı sonucu verir.

Alıştırma·Ağaç eşitliği ve alt ağaç kontrolü
Orta

İki ağacın yapısal olarak aynı olup olmadığını kontrol eden bir fonksiyon yaz. Ardından bir ağacın diğerinin alt ağacı olup olmadığını kontrol eden bir fonksiyon ekle.

İpucu

Alt ağaç kontrolü: Büyük ağacın her düğümü için "buradan başlayan ağaç, aranan ağaca eşit mi?" sorusunu sor.

Çözümü göster
main.go
package main

import "fmt"

type Node struct {
	Value       int
	Left, Right *Node
}

func build(values []int, i int) *Node {
	if i >= len(values) || values[i] == -1 {
		return nil
	}
	return &Node{values[i], build(values, 2*i+1), build(values, 2*i+2)}
}

// sameTree: yapı ve değerler birebir aynı mı
func sameTree(a, b *Node) bool {
	if a == nil || b == nil {
		return a == b // ikisi de nil ise eşit
	}
	return a.Value == b.Value &&
		sameTree(a.Left, b.Left) &&
		sameTree(a.Right, b.Right)
}

// isSubtree: sub, root'un bir alt ağacı mı
func isSubtree(root, sub *Node) bool {
	if sub == nil {
		return true
	}
	if root == nil {
		return false
	}
	if sameTree(root, sub) {
		return true
	}
	return isSubtree(root.Left, sub) || isSubtree(root.Right, sub)
}

func main() {
	a := build([]int{1, 2, 3, 4, 5}, 0)
	b := build([]int{1, 2, 3, 4, 5}, 0)
	c := build([]int{1, 2, 3, 4, 9}, 0) // son değer farklı
	d := build([]int{1, 2, 3}, 0)       // yapı farklı

	fmt.Println("a == b:", sameTree(a, b))
	fmt.Println("a == c:", sameTree(a, c))
	fmt.Println("a == d:", sameTree(a, d))
	fmt.Println("nil == nil:", sameTree(nil, nil))

	fmt.Println()
	sub := &Node{2, &Node{4, nil, nil}, &Node{5, nil, nil}}
	fmt.Println("sub, a'nın alt ağacı mı:", isSubtree(a, sub))

	notSub := &Node{2, &Node{4, nil, nil}, nil}
	fmt.Println("eksik dallı ağaç alt ağaç mı:", isSubtree(a, notSub))
	fmt.Println("nil her zaman alt ağaçtır:", isSubtree(a, nil))
	fmt.Println("büyük ağaç küçüğün alt ağacı mı:", isSubtree(d, a))
}
Çıktı
a == b: true
a == c: false
a == d: false
nil == nil: true

sub, a'nın alt ağacı mı: true
eksik dallı ağaç alt ağaç mı: false
nil her zaman alt ağaçtır: true
büyük ağaç küçüğün alt ağacı mı: false
ZamanO(n × m)AlanO(h)

Alt ağaç kontrolünün karmaşıklığı, en kötü durumda büyük ağacın her düğümü için küçük ağacın tamamını karşılaştırmayı gerektirir. Daha hızlı bir yaklaşım, her iki ağacı da benzersiz bir metne dönüştürüp alt dize araması yapmaktır — bu, String Algoritmaları dersindeki KMP algoritmasıyla O(n + m)'e iner.

sameTree içindeki return a == b satırına dikkat: İkisi de nil ise eşit, biri nil diğeri değilse eşit değil. İşaretçi karşılaştırmasıyla iki durumu tek satırda hallediyoruz.

Alıştırma·Ağacı serileştirme ve geri yükleme
Zor

Bir ikili ağacı metne çeviren (Serialize) ve o metinden ağacı geri kuran (Deserialize) fonksiyonlar yaz. Boş çocukları da kodla ki yapı tam olarak geri gelsin. Geri yüklenen ağacın özgünle aynı olduğunu doğrula.

İpucu

Preorder gezinme + nil işaretleyicisi yeterlidir: 1,2,#,#,3,#,#. Geri yüklerken aynı sırayla tüket ve # gördüğünde nil döndür.

Çözümü göster
main.go
package main

import (
	"fmt"
	"strconv"
	"strings"
)

type Node struct {
	Value       int
	Left, Right *Node
}

func build(values []int, i int) *Node {
	if i >= len(values) || values[i] == -1 {
		return nil
	}
	return &Node{values[i], build(values, 2*i+1), build(values, 2*i+2)}
}

// Serialize: preorder + nil işaretleyicisi
func Serialize(root *Node) string {
	var sb strings.Builder
	var walk func(*Node)
	walk = func(n *Node) {
		if sb.Len() > 0 {
			sb.WriteByte(',')
		}
		if n == nil {
			sb.WriteByte('#')
			return
		}
		sb.WriteString(strconv.Itoa(n.Value))
		walk(n.Left)
		walk(n.Right)
	}
	walk(root)
	return sb.String()
}

// Deserialize: aynı sırayla tüketerek ağacı geri kurar
func Deserialize(data string) (*Node, error) {
	tokens := strings.Split(data, ",")
	pos := 0

	var build func() (*Node, error)
	build = func() (*Node, error) {
		if pos >= len(tokens) {
			return nil, fmt.Errorf("beklenmeyen son: konum %d", pos)
		}
		token := tokens[pos]
		pos++
		if token == "#" {
			return nil, nil
		}
		v, err := strconv.Atoi(token)
		if err != nil {
			return nil, fmt.Errorf("geçersiz belirteç %q: %w", token, err)
		}
		left, err := build()
		if err != nil {
			return nil, err
		}
		right, err := build()
		if err != nil {
			return nil, err
		}
		return &Node{Value: v, Left: left, Right: right}, nil
	}

	root, err := build()
	if err != nil {
		return nil, err
	}
	if pos != len(tokens) {
		return nil, fmt.Errorf("fazla belirteç var: %d/%d tüketildi", pos, len(tokens))
	}
	return root, nil
}

func sameTree(a, b *Node) bool {
	if a == nil || b == nil {
		return a == b
	}
	return a.Value == b.Value && sameTree(a.Left, b.Left) && sameTree(a.Right, b.Right)
}

func main() {
	tree := build([]int{1, 2, 3, -1, 4, 5, -1, -1, -1, 6}, 0)

	data := Serialize(tree)
	fmt.Println("serileştirilmiş:", data)
	fmt.Println("uzunluk:", len(data), "bayt")

	restored, err := Deserialize(data)
	if err != nil {
		fmt.Println("hata:", err)
		return
	}
	fmt.Println("geri yüklendi, aynı mı:", sameTree(tree, restored))
	fmt.Println("tekrar serileştirince aynı metin mi:", Serialize(restored) == data)

	// Sınır durumları
	fmt.Println()
	fmt.Println("boş ağaç:", Serialize(nil))
	empty, _ := Deserialize("#")
	fmt.Println("boş ağaç geri yüklendi:", empty == nil)

	single, _ := Deserialize("42,#,#")
	fmt.Println("tek düğüm:", single.Value)

	_, err = Deserialize("1,2")
	fmt.Println("eksik veri hatası:", err != nil)
	_, err = Deserialize("1,#,#,9")
	fmt.Println("fazla veri hatası:", err != nil)
}
Çıktı
serileştirilmiş: 1,2,#,4,6,#,#,#,3,5,#,#,#
uzunluk: 25 bayt
geri yüklendi, aynı mı: true
tekrar serileştirince aynı metin mi: true

boş ağaç: #
boş ağaç geri yüklendi: true
tek düğüm: 42
eksik veri hatası: true
fazla veri hatası: true
ZamanO(n)AlanO(n)

Boş çocukları kodlamak neden zorunlu? Çünkü yalnızca değerler yazılırsa yapı belirsiz kalır: 1,2,3 dizisi birden çok farklı ağaca karşılık gelebilir. # işaretleyicisi bu belirsizliği ortadan kaldırır ve geri yükleme tek bir doğru sonuç üretir.

Bu teknik gerçek sistemlerde ağaç yapılarını diske yazmak, ağ üzerinden göndermek ya da önbelleğe almak için kullanılır. Üretimde genelde metin yerine ikili bir biçim tercih edilir; fikir aynı kalır.

Bir alternatif, inorder ve preorder dizilerini birlikte saklamaktır — ikisi birlikte ağacı benzersiz biçimde belirler, ancak yalnızca değerler benzersizse çalışır.

Kısa sınav

Kısa sınav

Bir düğümün derinliği ile yüksekliği arasındaki fark nedir?

n düğümlü bir ikili ağacın en kötü durumdaki yüksekliği nedir?

İkili arama ağacında hangi gezinme elemanları sıralı verir?

Level-order (seviye sıralı) gezinme hangi veri yapısını kullanır?

İteratif preorder gezinmede çocuklar yığına hangi sırayla konmalıdır?

DFS ile BFS'in alan karmaşıklığı nasıl karşılaştırılır?

Özet

  • Ağaç, döngü içermeyen hiyerarşik bir yapıdır; n düğümlü bir ağaçta n−1 kenar bulunur.
  • Derinlik kökten aşağıya, yükseklik yapraktan yukarıya ölçülür.
  • İkili ağaçlarda her düğümün en fazla iki çocuğu vardır ve sol/sağ ayrımı anlamlıdır.
  • Dengeli bir ağacın yüksekliği O(log n), dejenere bir ağacınki O(n)'dir; tüm performans bu farka bağlıdır.
  • Preorder kökü önce, inorder ortada, postorder sonda ziyaret eder; level-order seviye seviye ilerler.
  • Inorder gezinme, ikili arama ağacında elemanları sıralı verir.
  • DFS yığın (veya özyineleme), BFS kuyruk kullanır; alan kullanımları sırasıyla yükseklik ve genişlikle orantılıdır.
  • Ağaç problemlerinde kalıp nettir: temel durumu yaz, alt ağaçların cevabını hazır varsay, birleştirme adımını tasarla.
  • Özyineleme derin ağaçlarda yığın taşırabilir; iteratif sürümlerde yığını kendin yönetirsin.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.