Bir önceki derste ağaçların yapısını ve nasıl gezileceğini öğrendin. Ama o ağaçlarda değerler rastgele yerleştirilmişti; bir değeri aramak için tüm düğümleri dolaşman gerekiyordu, yani O(n). İkili arama ağacı (BST — Binary Search Tree), ağaca tek bir kural ekleyerek bunu O(log n)'e indirir.
Kural şudur: Her düğüm için, sol alt ağaçtaki tüm değerler o düğümden küçük, sağ alt ağaçtaki tüm değerler o düğümden büyüktür. Bu tek cümle, sıralı bir dizide ikili arama yapmanın ağaç hâlidir: Aradığın değeri kökle karşılaştırırsın, küçükse sola gidersin, büyükse sağa. Her adımda kalan olasılıkların yarısını elersin.
BST'nin dizilere göre avantajı ekleme ve silmenin de O(log n) olmasıdır — sıralı bir dizide araya eleman eklemek O(n) kaydırma gerektirirdi. Hash tablolarına göre avantajı ise sıralı gezinme ve aralık sorgularıdır. Ama BST'nin bir de kırılgan yanı vardır ve bu ders o kırılganlığı da anlatacak: Yanlış sırada veri eklersen ağaç bağlı listeye dönüşür ve tüm avantajını kaybeder.
BST özelliği
┌────┐
│ 50 │
└─┬──┘
┌───────┴───────┐
┌──▼──┐ ┌──▼──┐
│ 30 │ │ 70 │
└──┬──┘ └──┬──┘
┌───┴───┐ ┌───┴───┐
┌──▼─┐ ┌──▼─┐ ┌──▼─┐ ┌──▼─┐
│ 20 │ │ 40 │ │ 60 │ │ 80 │
└────┘ └────┘ └────┘ └────┘
50'nin solundaki her şey < 50 : {20, 30, 40} ✓
50'nin sağındaki her şey > 50 : {60, 70, 80} ✓
30'un solundaki her şey < 30 : {20} ✓
30'un sağındaki her şey > 30 : {40} ✓Dikkat edilecek ince nokta şudur: Kural yalnızca doğrudan çocuklar için değil, tüm alt ağaç için geçerlidir. Aşağıdaki ağaç, her düğüm kendi çocuklarıyla uyumlu görünse bile BST değildir:
50
┌──┴──┐
30 70
┌─┴─┐
20 60 ← 60 > 50 ama 50'nin SOL alt ağacında!Bu, BST doğrulamanın neden basit bir yerel kontrol olmadığını gösterir; birazdan doğru yöntemi göreceğiz.
Aşağıdaki görselleştirmede değer ekleyip arayarak yolun nasıl dallandığını izleyebilirsin. Sıralı değerler eklediğinde ağacın nasıl dejenere olduğunu denemeyi unutma:
Arama ve ekleme
Her iki işlem de aynı yolu izler: Kökten başla, karşılaştır, sola veya sağa in.
package main
import (
"fmt"
"strings"
)
type Node struct {
Value int
Left, Right *Node
}
type BST struct {
root *Node
size int
}
// Insert: O(h) — h, ağacın yüksekliği
func (t *BST) Insert(v int) bool {
if t.root == nil {
t.root = &Node{Value: v}
t.size++
return true
}
cur := t.root
for {
switch {
case v < cur.Value:
if cur.Left == nil {
cur.Left = &Node{Value: v}
t.size++
return true
}
cur = cur.Left
case v > cur.Value:
if cur.Right == nil {
cur.Right = &Node{Value: v}
t.size++
return true
}
cur = cur.Right
default:
return false // zaten var
}
}
}
// Contains: O(h) — her adımda kalanın yarısı elenir (dengeli ağaçta)
func (t *BST) Contains(v int) (found bool, steps int) {
for cur := t.root; cur != nil; {
steps++
switch {
case v < cur.Value:
cur = cur.Left
case v > cur.Value:
cur = cur.Right
default:
return true, steps
}
}
return false, steps
}
// InOrder: sıralı çıktı — BST'nin tanımlayıcı özelliği
func (t *BST) InOrder() []int {
out := make([]int, 0, t.size)
var walk func(*Node)
walk = func(n *Node) {
if n == nil {
return
}
walk(n.Left)
out = append(out, n.Value)
walk(n.Right)
}
walk(t.root)
return out
}
func (t *BST) Height() int {
var h func(*Node) int
h = func(n *Node) int {
if n == nil {
return -1
}
return 1 + max(h(n.Left), h(n.Right))
}
return h(t.root)
}
// String: ağacı yatay olarak çizer (sağ üstte, sol altta)
func (t *BST) String() string {
var sb strings.Builder
var draw func(*Node, string, string)
draw = func(n *Node, prefix, branch string) {
if n == nil {
return
}
draw(n.Right, prefix+" ", "┌── ")
fmt.Fprintf(&sb, "%s%s%d\n", prefix, branch, n.Value)
draw(n.Left, prefix+" ", "└── ")
}
draw(t.root, "", "")
return sb.String()
}
func main() {
var t BST
for _, v := range []int{50, 30, 70, 20, 40, 60, 80} {
t.Insert(v)
}
fmt.Print(t.String())
fmt.Println("eleman sayısı:", t.size, "| yükseklik:", t.Height())
fmt.Println("sıralı:", t.InOrder())
fmt.Println()
for _, v := range []int{40, 80, 45} {
found, steps := t.Contains(v)
fmt.Printf("%d ara → bulundu=%-5t adım=%d\n", v, found, steps)
}
fmt.Println("tekrar ekleme kabul edildi mi:", t.Insert(40))
}┌── 80
┌── 70
└── 60
50
┌── 40
└── 30
└── 20
eleman sayısı: 7 | yükseklik: 2
sıralı: [20 30 40 50 60 70 80]
40 ara → bulundu=true adım=3
80 ara → bulundu=true adım=3
45 ara → bulundu=false adım=3
tekrar ekleme kabul edildi mi: falseArama adımlarına dikkat: Yedi elemanlı dengeli bir ağaçta en fazla üç adım gerekiyor. Bin elemanlı dengeli bir ağaçta bu on adım, bir milyon elemanlıda yirmi adım olur. Logaritmanın gücü tam olarak budur.
Karmaşıklığı O(log n) değil O(h) olarak yazdığımıza dikkat et. Bu ikisi yalnızca ağaç dengeliyse aynıdır ve dengeyi hiçbir şey garanti etmez.
Minimum, maksimum ve ardıl
BST'nin yapısı, bazı soruları çok ucuz hâle getirir. En küçük eleman her zaman en soldaki düğümdür, en büyük eleman ise en sağdaki:
package main
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
switch {
case v < n.Value:
n.Left = insert(n.Left, v)
case v > n.Value:
n.Right = insert(n.Right, v)
}
return n
}
// Min: sürekli sola in — O(h)
func minNode(n *Node) *Node {
if n == nil {
return nil
}
for n.Left != nil {
n = n.Left
}
return n
}
// Max: sürekli sağa in — O(h)
func maxNode(n *Node) *Node {
if n == nil {
return nil
}
for n.Right != nil {
n = n.Right
}
return n
}
// successor: v'den büyük en küçük değer (inorder ardılı)
func successor(root *Node, v int) (int, bool) {
var best *Node
for cur := root; cur != nil; {
if v < cur.Value {
best = cur // aday: daha küçüğünü sol tarafta ara
cur = cur.Left
} else {
cur = cur.Right
}
}
if best == nil {
return 0, false
}
return best.Value, true
}
// predecessor: v'den küçük en büyük değer
func predecessor(root *Node, v int) (int, bool) {
var best *Node
for cur := root; cur != nil; {
if v > cur.Value {
best = cur
cur = cur.Right
} else {
cur = cur.Left
}
}
if best == nil {
return 0, false
}
return best.Value, true
}
// rangeQuery: [lo, hi] aralığındaki değerleri sıralı verir.
// Aralık dışındaki alt ağaçlara hiç inmez — budama (pruning).
func rangeQuery(n *Node, lo, hi int, out *[]int) {
if n == nil {
return
}
if n.Value > lo {
rangeQuery(n.Left, lo, hi, out)
}
if n.Value >= lo && n.Value <= hi {
*out = append(*out, n.Value)
}
if n.Value < hi {
rangeQuery(n.Right, lo, hi, out)
}
}
func main() {
var root *Node
for _, v := range []int{50, 30, 70, 20, 40, 60, 80, 35, 45} {
root = insert(root, v)
}
fmt.Println("minimum:", minNode(root).Value)
fmt.Println("maksimum:", maxNode(root).Value)
fmt.Println()
for _, v := range []int{40, 45, 80, 10} {
s, sok := successor(root, v)
p, pok := predecessor(root, v)
fmt.Printf("%-3d → ardıl=%v(%t) önce gelen=%v(%t)\n", v, s, sok, p, pok)
}
fmt.Println()
var inRange []int
rangeQuery(root, 35, 65, &inRange)
fmt.Println("35-65 aralığı:", inRange)
var all []int
rangeQuery(root, -1000, 1000, &all)
fmt.Println("tüm değerler:", all)
}minimum: 20 maksimum: 80 40 → ardıl=45(true) önce gelen=35(true) 45 → ardıl=50(true) önce gelen=40(true) 80 → ardıl=0(false) önce gelen=70(true) 10 → ardıl=20(true) önce gelen=0(false) 35-65 aralığı: [35 40 45 50 60] tüm değerler: [20 30 35 40 45 50 60 70 80]
rangeQuery fonksiyonu, BST'nin hash tablolarına karşı en büyük üstünlüğünü gösterir: Aralık sorgusu. Kod, aralığın dışında kalacağı kesin olan alt ağaçlara hiç inmez; bu budama sayesinde maliyet, ağacın yüksekliği artı sonuç sayısı kadardır. Hash tablosunda aynı sorgu tüm anahtarları taramayı gerektirirdi.
Silme: üç durum
Silme, BST'nin en çok dikkat isteyen işlemidir çünkü üç ayrı durum vardır.
DURUM 1 — Yaprak: doğrudan sil
50 50
┌──┴──┐ 20 sil ┌─┴──┐
30 70 ───► 30 70
┌─┘
20
DURUM 2 — Tek çocuk: çocuğu yerine geçir
50 50
┌──┴──┐ 30 sil ┌─┴──┐
30 70 ───► 20 70
┌─┘
20
DURUM 3 — İki çocuk: sağ alt ağacın MİNİMUMU ile değiştir
50 60
┌──┴──┐ 50 sil ┌──┴──┐
30 70 ───► 30 70
┌─┴─┐ ┌─┴─┐
60 80 80
↑ ardıl (successor)Üçüncü durumdaki mantık şudur: Silinen düğümün yerine, BST kuralını bozmayacak tek aday sağ alt ağacın en küçüğü (ardıl) ya da sol alt ağacın en büyüğüdür (öncel). Ardıl, silinen değerden büyük ama sağ alt ağaçtaki her şeyden küçüktür; tam olarak o boşluğa uyar.
package main
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
switch {
case v < n.Value:
n.Left = insert(n.Left, v)
case v > n.Value:
n.Right = insert(n.Right, v)
}
return n
}
func minValue(n *Node) int {
for n.Left != nil {
n = n.Left
}
return n.Value
}
// remove: üç durumu da ele alır, yeni kökü döndürür
func remove(n *Node, v int) *Node {
if n == nil {
return nil
}
switch {
case v < n.Value:
n.Left = remove(n.Left, v)
case v > n.Value:
n.Right = remove(n.Right, v)
default:
// DURUM 1 ve 2: sıfır veya tek çocuk
if n.Left == nil {
return n.Right
}
if n.Right == nil {
return n.Left
}
// DURUM 3: iki çocuk — ardılı yerine koy, ardılı sağdan sil
n.Value = minValue(n.Right)
n.Right = remove(n.Right, n.Value)
}
return n
}
func inorder(n *Node, out *[]int) {
if n == nil {
return
}
inorder(n.Left, out)
*out = append(*out, n.Value)
inorder(n.Right, out)
}
func collect(n *Node) []int {
var out []int
inorder(n, &out)
return out
}
func contains(n *Node, v int) bool {
for n != nil {
switch {
case v < n.Value:
n = n.Left
case v > n.Value:
n = n.Right
default:
return true
}
}
return false
}
func main() {
var root *Node
for _, v := range []int{50, 30, 70, 20, 40, 60, 80, 35} {
root = insert(root, v)
}
fmt.Println("başlangıç:", collect(root))
root = remove(root, 20) // durum 1: yaprak
fmt.Println("20 (yaprak) silindi: ", collect(root))
root = remove(root, 30) // durum 2: tek çocuk (35)
fmt.Println("30 (tek çocuk) silindi:", collect(root))
root = remove(root, 50) // durum 3: iki çocuk
fmt.Println("50 (iki çocuk) silindi:", collect(root))
fmt.Println("yeni kök:", root.Value)
root = remove(root, 999) // olmayan değer
fmt.Println("olmayan değer sonrası:", collect(root))
fmt.Println()
fmt.Println("50 hâlâ var mı:", contains(root, 50))
fmt.Println("60 var mı:", contains(root, 60))
// Her şeyi sil
for _, v := range collect(root) {
root = remove(root, v)
}
fmt.Println("hepsi silindi, kök nil mi:", root == nil)
}başlangıç: [20 30 35 40 50 60 70 80] 20 (yaprak) silindi: [30 35 40 50 60 70 80] 30 (tek çocuk) silindi: [35 40 50 60 70 80] 50 (iki çocuk) silindi: [35 40 60 70 80] yeni kök: 60 olmayan değer sonrası: [35 40 60 70 80] 50 hâlâ var mı: false 60 var mı: true hepsi silindi, kök nil mi: true
Silme kodundaki özyinelemeli yazım — n.Left = remove(n.Left, v) — kendini yeniden bağlama (reconstruction) deseni olarak bilinir. Alt ağaçtan dönen yeni kökü ebeveyne geri bağlayarak, ebeveyn işaretçisini ayrıca takip etmekten kurtulursun. Ağaç kodunu belirgin biçimde sadeleştirir.
BST doğrulama
Bir ağacın gerçekten BST olup olmadığını kontrol ederken en sık yapılan hata, yalnızca düğümü kendi çocuklarıyla karşılaştırmaktır. Doğru yaklaşım, her düğüm için geçerli bir aralık taşımaktır:
package main
import (
"fmt"
"math"
)
type Node struct {
Value int
Left, Right *Node
}
// isBSTNaive: YANLIŞ — yalnızca yerel kontrol yapar
func isBSTNaive(n *Node) bool {
if n == nil {
return true
}
if n.Left != nil && n.Left.Value >= n.Value {
return false
}
if n.Right != nil && n.Right.Value <= n.Value {
return false
}
return isBSTNaive(n.Left) && isBSTNaive(n.Right)
}
// isBST: DOĞRU — her düğüme izin verilen aralığı taşır
func isBST(n *Node, lo, hi int) bool {
if n == nil {
return true
}
if n.Value <= lo || n.Value >= hi {
return false
}
return isBST(n.Left, lo, n.Value) && isBST(n.Right, n.Value, hi)
}
// isBSTInorder: alternatif — inorder gezinme artan olmalı
func isBSTInorder(root *Node) bool {
var prev *int
var walk func(*Node) bool
walk = func(n *Node) bool {
if n == nil {
return true
}
if !walk(n.Left) {
return false
}
if prev != nil && *prev >= n.Value {
return false
}
v := n.Value
prev = &v
return walk(n.Right)
}
return walk(root)
}
func main() {
// Geçerli BST
valid := &Node{50,
&Node{30, &Node{20, nil, nil}, &Node{40, nil, nil}},
&Node{70, &Node{60, nil, nil}, &Node{80, nil, nil}},
}
// Geçersiz: 60, kökün SOL alt ağacında olmasına rağmen 50'den büyük
invalid := &Node{50,
&Node{30, &Node{20, nil, nil}, &Node{60, nil, nil}},
&Node{70, nil, &Node{80, nil, nil}},
}
fmt.Println("geçerli ağaç:")
fmt.Println(" naif kontrol :", isBSTNaive(valid))
fmt.Println(" aralık kontrolü:", isBST(valid, math.MinInt, math.MaxInt))
fmt.Println(" inorder kontrol:", isBSTInorder(valid))
fmt.Println()
fmt.Println("geçersiz ağaç:")
fmt.Println(" naif kontrol :", isBSTNaive(invalid), "← yanlış cevap!")
fmt.Println(" aralık kontrolü:", isBST(invalid, math.MinInt, math.MaxInt))
fmt.Println(" inorder kontrol:", isBSTInorder(invalid))
fmt.Println()
fmt.Println("boş ağaç geçerli mi:", isBST(nil, math.MinInt, math.MaxInt))
}geçerli ağaç: naif kontrol : true aralık kontrolü: true inorder kontrol: true geçersiz ağaç: naif kontrol : true ← yanlış cevap! aralık kontrolü: false inorder kontrol: false boş ağaç geçerli mi: true
Naif kontrolün geçersiz ağaç için true döndürdüğüne dikkat et — klasik bir tuzaktır. Aralık yaklaşımı doğru çalışır: Sola inerken üst sınır daralır, sağa inerken alt sınır yükselir. Inorder yaklaşımı da doğrudur ve BST'nin tanımını doğrudan kullanır: Inorder gezinme kesin artan olmalıdır.
Dejenere ağaçlar: BST'nin aşil topuğu
Şimdi BST'nin en ciddi sorununa geliyoruz. Ağaca sıralı veri eklersen ne olur?
1, 2, 3, 4, 5 sırayla eklenirse:
1
└── 2
└── 3
└── 4
└── 5
Yükseklik = n − 1 = 4
Arama = O(n) ← bağlı listeden farkı yok!Bu, teorik bir endişe değil; gerçek hayatta çok sık karşılaşılan bir durumdur. Veritabanından sıralı çekilen kayıtlar, artan kimlik numaraları, zaman damgaları, alfabetik listeler — hepsi ağacı dejenere eder.
package main
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
if v < n.Value {
n.Left = insert(n.Left, v)
} else if v > n.Value {
n.Right = insert(n.Right, v)
}
return n
}
func height(n *Node) int {
if n == nil {
return -1
}
return 1 + max(height(n.Left), height(n.Right))
}
func searchSteps(n *Node, v int) int {
steps := 0
for n != nil {
steps++
switch {
case v < n.Value:
n = n.Left
case v > n.Value:
n = n.Right
default:
return steps
}
}
return steps
}
// balancedInsert: sıralı diliminden dengeli ağaç kurar (orta elemanı kök yap)
func balancedInsert(values []int) *Node {
if len(values) == 0 {
return nil
}
mid := len(values) / 2
return &Node{
Value: values[mid],
Left: balancedInsert(values[:mid]),
Right: balancedInsert(values[mid+1:]),
}
}
func main() {
const n = 1023
sorted := make([]int, n)
for i := range sorted {
sorted[i] = i + 1
}
// 1) Sıralı ekleme: dejenere ağaç
var degenerate *Node
for _, v := range sorted {
degenerate = insert(degenerate, v)
}
// 2) Dengeli kurulum
balanced := balancedInsert(sorted)
fmt.Printf("%-22s %10s %14s\n", "ağaç", "yükseklik", "arama adımı")
fmt.Printf("%-22s %10d %14d\n", "sıralı eklenmiş", height(degenerate), searchSteps(degenerate, n))
fmt.Printf("%-22s %10d %14d\n", "dengeli kurulmuş", height(balanced), searchSteps(balanced, n))
fmt.Println()
fmt.Println("teorik en iyi yükseklik (log₂ 1023 ≈ 9):", height(balanced))
fmt.Println("dejenere ağaç kaç kat daha yavaş:",
searchSteps(degenerate, n)/searchSteps(balanced, n))
}ağaç yükseklik arama adımı sıralı eklenmiş 1022 1023 dengeli kurulmuş 9 10 teorik en iyi yükseklik (log₂ 1023 ≈ 9): 9 dejenere ağaç kaç kat daha yavaş: 102
Sonuç çarpıcı: Aynı 1023 eleman için dengeli ağaçta 10 adım, dejenere ağaçta 1023 adım. Yüz kattan fazla fark.
Çözüm ikiye ayrılır. Veriyi önceden biliyorsan, yukarıdaki balancedInsert gibi orta elemanı kök yaparak dengeli kurabilirsin. Ama veri akış hâlinde geliyorsa ağacın kendini dengelemesi gerekir. İşte bu, AVL Ağaçları dersinin konusudur ve kırmızı-siyah ağaçlar, B-ağaçları gibi tüm dengeli ağaç ailelerinin varlık sebebidir.
BST'yi ne zaman seçmeli?
Bir veri yapısı seçerken en yararlı soru şudur: "Hash tablosunun veremediği ne gerekiyor?" Çünkü yalnızca anahtarla arama yapacaksan hash tablosu neredeyse her zaman daha hızlıdır. Arama ağaçlarını tercih etmenin gerçek sebepleri şunlardır.
Sıralı gezinme gerekiyorsa. Elemanları küçükten büyüğe dolaşmak, arama ağacında ek bir maliyet olmadan mümkündür. Hash tablosunda her seferinde sıralama yapman gerekir ve bu O(n log n) demektir.
Aralık sorguları varsa. "Şu iki değer arasındaki kayıtlar" sorusu, arama ağacında yalnızca ilgili alt ağaçlara inerek yanıtlanır. Bu, veritabanı indekslerinin neden ağaç tabanlı olduğunun temel sebebidir.
En küçük veya en büyüğe sık erişiliyorsa. Bunlar ağacın uçlarındadır ve ağacın yüksekliği kadar adımda bulunur.
Sıralamada komşuluk önemliyse. Bir değerden hemen sonraki ya da hemen önceki elemanı bulmak, otomatik tamamlamadan zamanlayıcılara kadar pek çok yerde gerekir.
Sıralı çıktı sürekli isteniyorsa. Verinin her an sıralı hâlde bulunması gerekiyorsa, her eklemede yeniden sıralamak yerine ağaçta tutmak çok daha ucuzdur.
Buna karşılık, yalnızca "var mı, yok mu" ve "değerini getir" sorularını soruyorsan hash tablosu daha az bellek kullanır ve sabit süreli erişim verir. Go'da sıralı bir yapıya ihtiyacın olduğunda çoğu zaman en pratik çözüm, verileri bir dilimde tutup slices.BinarySearch ile aramaktır — ekleme sık değilse bu yaklaşım hem daha basit hem de önbellek dostu olduğu için daha hızlıdır.
Sık yapılan hatalar
- BST doğrulamayı yerel kontrolle yapmak. Her düğüm için aralık taşınmalı ya da inorder gezinme kontrol edilmelidir.
- Silmede iki çocuklu durumu yanlış ele almak. Yerine yalnızca ardıl (sağın minimumu) veya öncel (solun maksimumu) geçebilir.
- Dengesizliği görmezden gelmek. Sıralı veri, ağacı bağlı listeye çevirir ve tüm avantajı yok eder.
- Karmaşıklığı O(log n) sanmak. Doğrusu O(h)'dir; h ancak ağaç dengeliyse log n olur.
- Tekrarlı değerler için kural belirlememek. Eşitlik durumunda ne yapılacağına (yok say, sola koy, sayaç tut) baştan karar ver.
- Özyinelemeli silmede dönen kökü bağlamayı unutmak.
n.Left = remove(n.Left, v)yazımındaki atama zorunludur. - Derin ağaçlarda özyineleme sınırını unutmak. Dejenere bir ağaçta özyineleme derinliği eleman sayısına eşittir.
Alıştırmalar
Bir BST'de k'ıncı en küçük elemanı bulan bir fonksiyon yaz. Tüm ağacı bir dilime toplamadan, inorder gezinme sırasında sayarak çöz ve bulunca erken çık.
İpucu
Inorder gezinme değerleri sıralı verir. Bir sayaç tut; k'ya ulaşınca sonucu kaydet ve özyinelemeden çık.
Çözümü göster
package main
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
if v < n.Value {
n.Left = insert(n.Left, v)
} else if v > n.Value {
n.Right = insert(n.Right, v)
}
return n
}
// kthSmallest: inorder gezinme sırasında sayar, bulunca erken çıkar
func kthSmallest(root *Node, k int) (int, bool) {
if k < 1 {
return 0, false
}
count := 0
result := 0
found := false
var walk func(*Node)
walk = func(n *Node) {
if n == nil || found {
return
}
walk(n.Left)
if found {
return
}
count++
if count == k {
result, found = n.Value, true
return
}
walk(n.Right)
}
walk(root)
return result, found
}
// kthSmallestIterative: yığınla, gereksiz düğümlere hiç inmez
func kthSmallestIterative(root *Node, k int) (int, bool) {
var stack []*Node
cur := root
count := 0
for cur != nil || len(stack) > 0 {
for cur != nil {
stack = append(stack, cur)
cur = cur.Left
}
cur = stack[len(stack)-1]
stack = stack[:len(stack)-1]
count++
if count == k {
return cur.Value, true
}
cur = cur.Right
}
return 0, false
}
func main() {
var root *Node
for _, v := range []int{50, 30, 70, 20, 40, 60, 80, 35} {
root = insert(root, v)
}
for _, k := range []int{1, 3, 8, 9, 0} {
a, ok1 := kthSmallest(root, k)
b, ok2 := kthSmallestIterative(root, k)
fmt.Printf("k=%-2d → özyinelemeli=%v(%t) iteratif=%v(%t) aynı=%t\n",
k, a, ok1, b, ok2, a == b && ok1 == ok2)
}
}k=1 → özyinelemeli=20(true) iteratif=20(true) aynı=true k=3 → özyinelemeli=35(true) iteratif=35(true) aynı=true k=8 → özyinelemeli=80(true) iteratif=80(true) aynı=true k=9 → özyinelemeli=0(false) iteratif=0(false) aynı=true k=0 → özyinelemeli=0(false) iteratif=0(false) aynı=true
Erken çıkış önemlidir: k küçükse ağacın büyük kısmına hiç dokunmazsın. Tüm değerleri toplayıp sorted[k-1] almak da doğru sonucu verirdi ama O(n) zaman ve O(n) bellek harcardı.
Bu sorgu çok sık yapılacaksa, her düğümde alt ağacının boyutunu saklayabilirsin; o zaman k'ıncı elemanı bulmak doğrudan O(h) olur. Buna "sıra istatistiği ağacı" denir.
Dejenere hâle gelmiş bir BST'yi dengeleyen bir fonksiyon yaz: Önce inorder gezinmeyle sıralı diliye çevir, sonra orta elemanı kök yaparak yeniden kur. Önceki ve sonraki yükseklikleri karşılaştır.
İpucu
Sıralı bir dilimden dengeli ağaç kurmak için özyinelemeli olarak orta elemanı kök seç, sol yarıyı sol alt ağaç, sağ yarıyı sağ alt ağaç yap.
Çözümü göster
package main
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
if v < n.Value {
n.Left = insert(n.Left, v)
} else if v > n.Value {
n.Right = insert(n.Right, v)
}
return n
}
func inorder(n *Node, out *[]int) {
if n == nil {
return
}
inorder(n.Left, out)
*out = append(*out, n.Value)
inorder(n.Right, out)
}
func height(n *Node) int {
if n == nil {
return -1
}
return 1 + max(height(n.Left), height(n.Right))
}
// fromSorted: sıralı dilimden dengeli ağaç kurar
func fromSorted(values []int) *Node {
if len(values) == 0 {
return nil
}
mid := len(values) / 2
return &Node{
Value: values[mid],
Left: fromSorted(values[:mid]),
Right: fromSorted(values[mid+1:]),
}
}
// Balance: mevcut ağacı dengeli bir kopyaya dönüştürür
func Balance(root *Node) *Node {
var values []int
inorder(root, &values)
return fromSorted(values)
}
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() {
// Sıralı ekleme → dejenere ağaç
var degenerate *Node
for i := 1; i <= 31; i++ {
degenerate = insert(degenerate, i)
}
var before []int
inorder(degenerate, &before)
balanced := Balance(degenerate)
var after []int
inorder(balanced, &after)
fmt.Println("eleman sayısı:", len(before))
fmt.Println("dengeleme öncesi yükseklik:", height(degenerate))
fmt.Println("dengeleme sonrası yükseklik:", height(balanced))
fmt.Println("dengeli mi (önce):", isBalanced(degenerate))
fmt.Println("dengeli mi (sonra):", isBalanced(balanced))
fmt.Println("yeni kök:", balanced.Value)
fmt.Println("içerik korundu mu:", fmt.Sprint(before) == fmt.Sprint(after))
}eleman sayısı: 31 dengeleme öncesi yükseklik: 30 dengeleme sonrası yükseklik: 4 dengeli mi (önce): false dengeli mi (sonra): true yeni kök: 16 içerik korundu mu: true
Bu yöntem — "düzleştir ve yeniden kur" — tek seferlik bir toparlama için idealdir ve uygulaması çok basittir. Ancak her eklemede çalıştıramazsın: O(n) maliyeti, eklemenin O(log n) avantajını yok eder.
Sürekli dengeli kalması gereken ağaçlar için dönüşlerle yerel düzeltme yapan yapılar kullanılır; onları bir sonraki derste göreceksin. Yine de bu teknik pratikte işe yarar: Toplu veri yüklemesinden sonra ağacı bir kez dengelemek yaygın bir stratejidir.
Bir BST'de iki düğümün en yakın ortak atasını (lowest common ancestor) bulan bir fonksiyon yaz. BST özelliğinden yararlanarak O(h) sürede çöz. Ayrıca iki düğüm arasındaki yolu da yazdır.
İpucu
Kökten başla: Her iki değer de kökten küçükse sola, ikisi de büyükse sağa in. Değerler kökün iki yanına düşüyorsa (veya biri köke eşitse) o düğüm en yakın ortak atadır.
Çözümü göster
package main
import (
"fmt"
"strings"
)
type Node struct {
Value int
Left, Right *Node
}
func insert(n *Node, v int) *Node {
if n == nil {
return &Node{Value: v}
}
if v < n.Value {
n.Left = insert(n.Left, v)
} else if v > n.Value {
n.Right = insert(n.Right, v)
}
return n
}
func contains(n *Node, v int) bool {
for n != nil {
switch {
case v < n.Value:
n = n.Left
case v > n.Value:
n = n.Right
default:
return true
}
}
return false
}
// lca: BST özelliğiyle O(h) — yol ayrıldığı ilk düğüm cevaptır
func lca(root *Node, a, b int) (int, bool) {
if !contains(root, a) || !contains(root, b) {
return 0, false
}
lo, hi := min(a, b), max(a, b)
for cur := root; cur != nil; {
switch {
case hi < cur.Value:
cur = cur.Left // ikisi de solda
case lo > cur.Value:
cur = cur.Right // ikisi de sağda
default:
return cur.Value, true // yollar burada ayrılıyor
}
}
return 0, false
}
// pathTo: kökten hedefe giden yolu döndürür
func pathTo(root *Node, v int) []int {
var path []int
for cur := root; cur != nil; {
path = append(path, cur.Value)
switch {
case v < cur.Value:
cur = cur.Left
case v > cur.Value:
cur = cur.Right
default:
return path
}
}
return nil
}
// distance: iki düğüm arasındaki kenar sayısı
func distance(root *Node, a, b int) (int, bool) {
ancestor, ok := lca(root, a, b)
if !ok {
return 0, false
}
pa, pb := pathTo(root, a), pathTo(root, b)
pl := len(pathTo(root, ancestor))
return (len(pa) - pl) + (len(pb) - pl), true
}
func join(path []int) string {
parts := make([]string, len(path))
for i, v := range path {
parts[i] = fmt.Sprint(v)
}
return strings.Join(parts, " → ")
}
func main() {
var root *Node
for _, v := range []int{50, 30, 70, 20, 40, 60, 80, 35, 45, 75} {
root = insert(root, v)
}
cases := [][2]int{{20, 45}, {35, 45}, {60, 80}, {20, 80}, {30, 35}, {20, 99}}
for _, c := range cases {
a, b := c[0], c[1]
ancestor, ok := lca(root, a, b)
if !ok {
fmt.Printf("%d ve %d → düğümlerden biri ağaçta yok\n", a, b)
continue
}
dist, _ := distance(root, a, b)
fmt.Printf("%-3d ve %-3d → ortak ata: %-3d mesafe: %d\n", a, b, ancestor, dist)
}
fmt.Println()
fmt.Println("45'e giden yol:", join(pathTo(root, 45)))
fmt.Println("75'e giden yol:", join(pathTo(root, 75)))
}20 ve 45 → ortak ata: 30 mesafe: 3 35 ve 45 → ortak ata: 40 mesafe: 2 60 ve 80 → ortak ata: 70 mesafe: 2 20 ve 80 → ortak ata: 50 mesafe: 4 30 ve 35 → ortak ata: 30 mesafe: 2 20 ve 99 → düğümlerden biri ağaçta yok 45'e giden yol: 50 → 30 → 40 → 45 75'e giden yol: 50 → 70 → 80 → 75
BST özelliği bu problemi şaşırtıcı derecede kolaylaştırır. Sıradan bir ikili ağaçta aynı soru, her iki düğümü de bulup yolları karşılaştırmayı ya da alt ağaçları özyinelemeli olarak taramayı gerektirir. Burada ise tek bir aşağı doğru yürüyüş yeterlidir: Yolun ayrıldığı ilk düğüm, tanım gereği en yakın ortak atadır.
Fonksiyonun başındaki varlık kontrolüne dikkat et: Bu kontrol olmasaydı, ağaçta bulunmayan değerler için de anlamsız bir sonuç dönerdi. Sınır durumlarını açıkça ele almak, ağaç kodunda her zaman iyi bir yatırımdır.
Kısa sınav
BST özelliği tam olarak nedir?
İki çocuğu olan bir düğüm silindiğinde yerine ne geçer?
BST'ye 1, 2, 3, 4, 5 değerleri bu sırayla eklenirse ne olur?
Hangi gezinme BST'de değerleri sıralı verir?
BST'nin hash tablosuna göre en önemli avantajı nedir?
BST işlemlerinin karmaşıklığı neden O(log n) değil O(h) olarak yazılır?
Özet
- BST kuralı: Sol alt ağaçtaki tüm değerler düğümden küçük, sağ alt ağaçtakiler büyüktür.
- Arama, ekleme ve silme O(h) sürer; h dengeli ağaçta log n, dejenere ağaçta n−1'dir.
- Minimum en solda, maksimum en sağdadır; ardıl ve öncel tek bir aşağı yürüyüşle bulunur.
- Aralık sorguları budama sayesinde yalnızca ilgili alt ağaçlara iner — hash tablolarına karşı en büyük üstünlük.
- Silme üç durumludur: yaprak doğrudan silinir, tek çocuklu düğümün yerine çocuğu geçer, iki çocukluda ardıl kullanılır.
- BST doğrulama yerel kontrolle yapılamaz; aralık taşımak veya inorder artışını kontrol etmek gerekir.
- Sıralı veri eklemek ağacı dejenere eder ve tüm logaritmik avantajı yok eder.
- Veri önceden biliniyorsa orta elemanı kök yaparak dengeli kurulabilir; akış hâlinde gelen veri için kendini dengeleyen ağaçlar gerekir.