Ş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:
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
│
DYü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 → dejenereBu 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:
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))
}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 GHangisini 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:
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)
}
}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]
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:
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)
}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.
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))
}ö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.
nilkontrolü 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
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
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}))
}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]
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.
İ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
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))
}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
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.
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
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)
}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
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
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.