Bölüm 11: Arama Ağaçları
Giriş
Öğrenme Çıktıları
İkili arama ağacının dört özelliğini bir ağaçta denetlemek.
arama_agacina_ekle ve arama_agacinda_ara yordamlarını döngüsel ve özyinelemeli yazmak.
Derinliği özyinelemeli yazmak; ek parametre ve global değişkenden kaçınmak.
Çocuksuz, tek çocuklu ve iki çocuklu düğüm silmenin dört yöntemini karşılaştırmak.
Dengesizliğin maliyetini hesaplamak; AVL'de LL, RR, LR, RL döndürmelerini uygulamak.
BST, AVL, kırmızı-siyah ve B-ağacını kullanım yerine göre ayırmak.
İkili Arama Ağacı
İkili arama ağacı (binary search tree)
- Her öğenin değeri farklıdır; değerler tektir (unique).
- Boş olmayan sol alt ağaçtaki değerler, alt ağacın kökündekinden küçüktür.
- Boş olmayan sağ alt ağaçtaki değerler kökündekinden büyüktür.
- Sol ve sağ alt ağaçlar da ikili arama ağacıdır.
İkili Arama Ağacı
Hangisi ikili arama ağacı?
22, 15'in sağında olduğu için 15'ten büyük (doğru) ama 18'in sol alt ağacında olduğu hâlde 18'den büyük → tanıma uymaz.
95'in solundaki her değer (80, 50, 2) 95'ten küçük; 80'in solu 50, 50'nin solu 2: sıralama bozulmamış.
35'in sağındaki her değer büyük; 40'ın solunda 38 ve 39 (40'tan küçük, 35'ten büyük): geçerli.
Ekleme
Ekleme: yeri ağaç belirler
SORU: 3, 5, 7, 9, 11 kümesiyle derinliği 3, 4 ve 5 olan ikili arama ağaçları çiziniz. 2 derinliğinde çizilebilir mi?
Çözüm
Lemma: k derinlikte en çok 2k−1 düğüm; 2 derinlikte en çok 3 → 5 düğüm sığmaz. Yanda 3, 4, 5 derinlikli örnekler.
Ekleme
Verilerin ağaca eklenme sırası önemli midir?
47 (sol) ve 26 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
55 (sol) ve 17 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
49 (sol) ve 47 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
26 (sol) ve 93 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
34 (sol) ve 84 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
17 (sol) ve 52 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
52 (sol) ve 34 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
84 (sol) ve 55 (sağ) eklenir: kökten başlanıp küçükse sola, büyükse sağa inilir, ilk NULL'a konur.
Sonuç: derinlik 4 ve 6. Kök ortada gezinme ikisinde de aynı: 17 26 34 47 49 52 55 84 93.
Ekleme
Çarpıklık, dengeli ağaçlar ve sık kullanılan düğümler
- Veriler sıralı gelirse ağaç sağa ya da sola çarpık olur, derinliği n olur: liste gibi.
- Derinlik ≈ lg n ise dengeli ikili arama ağacı: ekleme, silme, arama O(lg n).
- Dengeli ağaçlar (self-balancing): veri geldikçe ağacı yeniden dengeleyen algoritmalar — kırmızı-siyah, AVL.
- Sık kullanılan düğümler: arama sıklığı yüksek düğümleri köke yaklaştıran algoritmalar (“zig-zag”; literatürdeki adıyla splay tree, Sleator–Tarjan).
Ekleme
??? — düşündüren iki soru
??? Ağaç görüntüsünden verilerin eklenme sırası elde edilebilir mi? Gelen veriye göre oluşan ağaç unique mi?
Cevap
Aynı sıra her zaman aynı ağacı verir (ekleme deterministiktir). Ama tersi tek değildir: 2,1,3 ve 2,3,1 aynı ağacı kurar. Her atanın torunlarından önce gelmesi yeter; ağacın kök önde sırası daima geçerli bir ekleme sırasıdır.
??? Kök önde, kök ortada, kök sonda gezinme sırasından ağaç görüntüsü elde edilebilir mi?
Cevap
Genel ikili ağaçta tek gezinme yetmez; kök ortada + (kök önde ya da kök sonda) tek ağaç verir. İkili arama ağacında kök ortada zaten sıralı hâldir; bu yüzden yalnızca kök önde (ya da kök sonda) sırası yeterlidir.
Artık ekleme yordamıyla örnek ağaçlar kurup ikili ağaç yordamlarını (gezinme, kopyalama, eşitlik) gerçekten çalıştırabiliriz.
Ekleme
Ekleme nasıl yapılır?
- Her iki çocuğu da olan düğüme eklenemez; en az bir çocuğu olmayan düğüme eklenir.
- Gelen veri kökten itibaren aranır: daha büyük değerli düğümde sola, daha küçük değerlide sağa ilerlenir.
- Aynı değer bulunursa tanıma aykırı olacağından ekleme yapılmaz.
- Arama işaretçisinin ilk rastladığı NULL'un yerine yeni öğe eklenir: bunun için bir adım arkadan gelen işaretçi (q) gerekir.
typedef enum { FALSE, TRUE } boolean; typedef struct dugum *agac_gost; struct dugum { agac_gost solcocuk; int veri; agac_gost sagcocuk; }; /* ogeal(): malloc + NULL denetimi, çocukları NULL yapar */
Ekleme · Kodu adım adım çalıştır
arama_agacina_ekle(50, &L) adım adım
p kökü gösteriyor (47), q (arkadaki işaretçi) NULL.
q = p = 47: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.
50 > 47 → yeri sağ alt ağaçta: p = 47->sagcocuk = 55.
q = p = 55: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.
50 < 55 → yeni öğenin yeri sol alt ağaçta: p = 55->solcocuk = 49.
q = p = 49: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.
50 > 49 → yeri sağ alt ağaçta: p = 49->sagcocuk = 52.
q = p = 52: arka işaretçi bir adım geriden gelir; eklemeyi q'ya yapacağız.
50 < 52 → yeni öğenin yeri sol alt ağaçta: p = 52->solcocuk = NULL.
p NULL oldu: döngü biter. Yeni öğe q = 52'nin boş çocuğunun yerine gelecek.
Yeni düğüm ayrılır (çocukları NULL) ve verisi 50 yapılır.
Ağaç boş değil; 50 < 52 → q->solcocuk = r. Bağ kuruldu.
TRUE döner. Yalnızca bir işaretçi değişti; ağacın geri kalanına dokunulmadı.
boolean arama_agacina_ekle(int x, agac_gost *L) { agac_gost q = NULL, p = *L, r; // q arka, p ön while (p) { // NULL'a dek in q = p; // arkayı günle if (x < p->veri) p = p->solcocuk; else if (x > p->veri) p = p->sagcocuk; else { printf("Benzer deger var.\n"); return FALSE; } } r = ogeal(); // çocuklar NULL r->veri = x; if (*L == NULL) *L = r; // boş ağaç: kök else if (x < q->veri) q->solcocuk = r; else q->sagcocuk = r; return TRUE; }
Ekleme
Neden agac_gost *L? Maliyet?
- Ağaç boşsa (
*L == NULL) eklenen öğe kök olur: çağıranın kök işaretçisi değişmelidir. - İşaretçinin kendisini değiştirmek için adresi gönderilir (call by reference):
arama_agacina_ekle(x, &kok). - Maliyet: derinlik kadar karşılaştırma → Ω(lg n) – O(n).
- C99 düzeltmesi:
q = NULLilk değeri.
/* Özyinelemeli alternatif: yeni kökü döndürür */ agac_gost ekle(agac_gost L, int x) { if (!L) { L = ogeal(); L->veri = x; } else if (x < L->veri) L->solcocuk = ekle(L->solcocuk, x); else if (x > L->veri) L->sagcocuk = ekle(L->sagcocuk, x); return L; /* kok = ekle(kok, x); */ }
Arama · Kodu adım adım çalıştır
arama_agacinda_ara(52, L) — döngüsel
52 > 47 → sağa. Sol alt ağaç elenir.
52 < 55 → sola. Sağ alt ağaç (soluk) hiç incelenmez: aramanın gücü buradan gelir.
52 > 49 → sağa. Sol alt ağaç elenir.
52 == 52: bulundu, düğümün adresi döndürülür. Toplam 4 karşılaştırma.
agac_gost arama_agacinda_ara(int x, agac_gost L) { while (L) { // uygun alt ağaçta in if (x == L->veri) return L; if (x < L->veri) L = L->solcocuk; else L = L->sagcocuk; } return NULL; // bulunamadı }
“Memura görevi tanımlarken müdür çok doğru tanımlamalı. Adam bir daha gelip kapıyı çalmamalı.”
Arama · Kodu adım adım çalıştır
Özyinelemeli arama: ara(34, L)
34 < 47: return ara(x, L->solcocuk) — sonucu ne olursa olsun aynen yukarı iletilecek.
34 > 26: return ara(x, L->sagcocuk).
34 == 34: bulundu, bu çerçeve L'yi döndürür.
Alt çağrı &düğüm(34) döndürdü; ara(34, 26) da onu döndürür.
Alt çağrı &düğüm(34) döndürdü; ara(34, 47) da onu döndürür.
agac_gost ara(int x, agac_gost L) { if (!L) return NULL; // yok if (x == L->veri) return L; // bulundu if (x < L->veri) return ara(x, L->solcocuk); return ara(x, L->sagcocuk); }
Arama
Örnek: “Bu örnek çalışır mı?”
if (L) { if (x == L->veri) return L; if (x < L->veri) return arama(x, L->sol); return arama(x, L->sag); } return NULL; /* “Buraya hiç girmez, ama olsun, biz yazalım da… ☺” */
3. satırdaki if bloğuna girilirse 4. satırdaki sağ alt ağaç araması asla yapılmaz: alt çağrı ne döndürürse o döner.
- İkili arama ağacında bu davranış tam istenendir: x küçükse sağda olamaz; kod doğrudur ve O(h)'dir.
- Sıralı olmayan genel ikili ağaçta ise x sağ alt ağaçta olabilir; bu kod onu kaçırır.
- Genel ağaç için çözüm iki tarafı da arar → O(n).
Arama
Örnek çözüm: her ikili ağaçta arama
agac_gost ara_ozyineli(int veri, agac_gost L) { if (!L) return NULL; /* ağaç boş mu */ if (L->veri == veri) return L; /* aranan bulundu */ agac_gost T = ara_ozyineli(veri, L->solcocuk); return T ? T : ara_ozyineli(veri, L->sagcocuk); }
- Önce solda arar; bulamazsa (T == NULL) sağda arar.
- Sıralama bilgisi kullanılmaz → en kötü O(n); BST'de gereksiz yere yavaş.
- Ağırlık bulma sınav sorusunun çözümünde bu yordam kullanılır.
Derinlik
SORU: derinliği döngüsel yazınız
/* global değişkensiz döngüsel çözüm: düzey düzey gezin, düzeyleri say */ int derinlik_d(agac_gost L) { agac_gost k[MAX_SIZE]; int on = 0, arka = 0, d = 0; if (L) k[arka++] = L; while (on < arka) { /* bir düzey */ int son = arka; d++; for (; on < son; on++) { if (k[on]->solcocuk) k[arka++] = k[on]->solcocuk; if (k[on]->sagcocuk) k[arka++] = k[on]->sagcocuk; } } return d; /* MAX_SIZE >= n olmalı */ }
Derinlik
Neden global değişken yok?
Girdi parametreden, çıktı dönüş değerinden; gizli durum yok.
Yordamın içinde başka bir yordamın prototipi yerel olarak bildirilip kullanılabilir; tanım dışarıda kalır (standart C iç içe yordam tanımına izin vermez).
Her çağrının kendi yerel sol/sağ değişkeni vardır; global sayaç gerekmez.
Derinlik · Kodu adım adım çalıştır
Özyinelemeli derinlik: {sol, sağ} çerçeveleri
L = A: önce sol alt ağacın derinliği: derinlik(B) çağrılır.
L = B: önce sol alt ağacın derinliği: derinlik(D) çağrılır.
L = D: sol = 0, sağ = 0 (NULL → 0) → MAX + 1 = 1 döner; yerel sol/sağ silinir.
L = B: sol = 1, sağ = 0 (NULL → 0) → MAX + 1 = 2 döner; yerel sol/sağ silinir.
sol = 2; şimdi derinlik(C).
L = C: önce sol alt ağacın derinliği: derinlik(E) çağrılır.
L = E: önce sol alt ağacın derinliği: derinlik(F) çağrılır.
L = F: sol = 0, sağ = 0 (NULL → 0) → MAX + 1 = 1 döner; yerel sol/sağ silinir.
L = E: sol = 1, sağ = 0 (NULL → 0) → MAX + 1 = 2 döner; yerel sol/sağ silinir.
L = C: sol = 2, sağ = 0 (NULL → 0) → MAX + 1 = 3 döner; yerel sol/sağ silinir.
L = A: sol = 2, sağ = 3 → MAX + 1 = 4 döner; yerel sol/sağ silinir.
int derinlik(agac_gost L) { int sol, sag; if (L) { // işaretçi NULL mı sol = derinlik(L->solcocuk); sag = derinlik(L->sagcocuk); return MAX(sol, sag) + 1; } else return 0; // uç yaprağa ulaşıldı }
Derinlik
Derinlik: problem yeterince küçüldü mü?
- Ağacın derinliği, sol ve sağ alt ağaçlarının en derin olanının 1 fazlasıdır:
MAX(sol, sağ) + 1. - Her düğümün kendi 2 yerel değişkeni vardır: sol ve sağ derinlik. NULL ile yapılan çağrıların yerelleri kullanılmadan silinir.
- Zaman O(n), bellek O(h).
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int derinlik(agac_gost L) { /* tek satır */ return !L ? 0 : MAX(derinlik(L->solcocuk), derinlik(L->sagcocuk)) + 1; }
MAX makrosu argümanını iki kez değerlendirir; bu tek satırlıkta özyinelemeli çağrılar iki kez yapılır → üstel süre! Bu yüzden makro yerine max() işlevi kullanılır (sınav çözümündeki gibi).
Derinlik
Ek parametreler koyma mevzusu
— Alan J. Perlis
Soru: “Hocam, int derinlik(agac_gost L, int a) diye fazladan bir parametre ekleyip derinlik(L, 0) diye çağırsak olur mu?”
Silme
İkili arama ağacından öğe silme
- Silerken ağacın ikili arama ağacı özelliği bozulmamalı.
- Çocuksuz (yaprak): babanın ilgili işaretçisi NULL yapılır.
- Tek çocuklu: babanın işaretçisi silinenin tek çocuğunu gösterir.
- İki çocuklu: farklı yöntemler var; örnek ağaçtan 60'ı silelim.
Örnek ağaç: 40, 20, 60, 10, 30, 50, 70, 42, 55, 65, 73, 52, 57, 66, 56 sırasıyla eklenmiştir (derinlik 6).
Silme
1. yöntem: sol alt ağacın en büyüğü
Sol alt ağaçtaki en büyük değer: sol alt ağaçta sağ işaret alanları üzerinden ilerlenir; sağ çocuğu olmayan öğe (57) en büyüktür.
Bu öğenin bir sol alt ağacı olabilir (57'nin 56'sı). Bu sol çocuk, taşınacak öğenin atasının sağ bağına aktarılmalı (55 → 56).
İkinci yol: 60 free edilmez; veri alanına 57 yazılır, yalnızca 55'in sağ bağı 56'ya günlenir → 1 bağ. Birinci yol (düğümü taşımak) 4 bağ ister: 40→57, 55→56, 57.sol→50, 57.sağ→70.
Silme · Kodu adım adım çalıştır
sil(60, &L): iki çocuklu düğüm
p kökü, bag kök işaretçisinin adresini tutar: silinecek düğüme giden bağı değiştirebilmek için.
60 > 40 → bag = &40->sagcocuk, p = 60.
60 bulundu ve iki çocuğu var (50, 70) → sol alt ağacın en büyüğü aranır.
b = &60->solcocuk → *b = 50.
*b'nin sağı var → b = &50->sagcocuk, *b = 55.
*b'nin sağı var → b = &55->sagcocuk, *b = 57.
57'nin sağ çocuğu yok: sol alt ağacın en büyüğü 57.
60'ın veri alanına 57 yazılır (düğüm taşınmaz, 1. yöntemin 2. yolu).
Artık silinecek olan eski 57 düğümü; bag = &55->sagcocuk.
*bag = 57'nin tek çocuğu (56): 55'in sağ bağı artık 56'yı gösterir.
Eski düğüm free edilir. Ağaç yine ikili arama ağacı; derinlik 6 → 5.
void sil(int x, agac_gost *L) { agac_gost p = *L, *bag = L; // bag: p'ye giden while (p && p->veri != x) { // x'i ara bag = (x < p->veri) ? &p->solcocuk : &p->sagcocuk; p = *bag; } if (!p) return; // x yok if (p->solcocuk && p->sagcocuk) { // 2 çocuk agac_gost *b = &p->solcocuk; // sol alt while ((*b)->sagcocuk) // ağacın en b = &(*b)->sagcocuk; // büyüğü p->veri = (*b)->veri; // veriyi taşı bag = b; p = *b; // onu sileceğiz } *bag = p->solcocuk ? p->solcocuk : p->sagcocuk; free(p); }
Silme
Dört yöntem ve derinliğe etkisi
1. yöntem: 57 yukarı, 56 55'in sağına. Derinlik 5'e iner.
2. yöntem: sağ alt ağacın en küçüğü 65 taşınır; 65'in sağ çocuğu 66, 70'in soluna bağlanır. Derinlik 6.
3. yöntem: silinenin yerine sağ çocuğu (70) kayar; 50 alt ağacı, 70 alt ağacının en küçüğünün (65) soluna asılır. Derinlik 7.
4. yöntem: yerine sol çocuğu (50) kayar; 70 alt ağacı, 50 alt ağacının en büyüğünün (57) sağına asılır. Derinlik 7 (40-50-55-57-70-65-66 yolu 7 düğümdür).
Dengeli Ağaçlar
Dengesizliğin maliyeti
| n | dengeli: ⌈lg(n+1)⌉ | çarpık: n |
|---|---|---|
| 1.000 | 10 | 1.000 |
| 1.000.000 | 20 | 1.000.000 |
| 2.000.000 | 21 | 2.000.000 |
| 109 | 30 | 109 |
- Rastgele sırada eklenen n anahtarlı BST'nin ortalama derinliği Θ(lg n)'dir; ortalama arama ≈ 1,39 lg n karşılaştırma (Sedgewick; CLRS 12.4).
- Ama en kötü durum (sıralı veya neredeyse sıralı veri, ki gerçekte sık görülür) Θ(n)'dir.
- Çözüm: her ekleme/silmeden sonra yerel döndürmelerle derinliği O(lg n) tutan ağaçlar: AVL, kırmızı-siyah, B-ağacı.
Dengeli Ağaçlar
AVL ağacı (Adelson-Velski ve Landis, 1962)
- Her düğümde denge faktörü = yük(sol) − yük(sağ) ∈ {−1, 0, +1}.
- Yükseklik en çok ≈ 1,44 lg(n+2): arama, ekleme, silme O(lg n).
- Ekleme sıradan BST eklemesiyle yapılır; sonra eklenen yoldan köke doğru denge faktörleri güncellenir.
- Faktörü ±2 olan ilk ata bulunur; yeni düğümün o atanın hangi torununda olduğuna göre 4 durum: LL, RR, LR, RL.
- Eklemede tek (LL/RR) ya da çift (LR/RL) döndürme dengeyi yeniden kurar.
typedef struct avl *avl_gost; struct avl { int veri; int yuk; /* alt ağaç yüksekliği */ avl_gost sol, sag; }; int yuk(avl_gost t) { return t ? t->yuk : 0; } int denge(avl_gost t) { return yuk(t->sol) - yuk(t->sag); }
Dengeli Ağaçlar
Tek döndürme: LL ve RR
LL: yeni düğüm z'nin sol çocuğunun (y) sol alt ağacına girdi, z'nin faktörü +2. z sağa döndürülür: y yukarı çıkar, y'nin sağ alt ağacı T3 z'nin soluna geçer. Sıra korunur: T1 x T2 y T3 z T4.
RR ayna durumdur: z sola döndürülür; y'nin sol alt ağacı (T2) z'nin sağına geçer. Her iki durumda alt ağacın yüksekliği eklemeden önceki değerine döner.
Dengeli Ağaçlar
Çift döndürme: LR ve RL
LR: yeni düğüm z'nin sol çocuğunun (y) sağ alt ağacında. Tek sağa döndürme işe yaramaz; önce y sola döndürülür → LL biçimi.
Sonra z sağa döndürülür: torun x iki atasının üstüne çıkar. RL bunun aynasıdır: önce sağ çocuk sağa, sonra z sola.
Dengeli Ağaçlar · Kodu adım adım çalıştır
sag_dondur(): LL durumunda iki işaretçi
5 eklendi: 30'un faktörü yük(20)=3 − yük(40)=1 = +2, sol-sol ağır → LL: sag_dondur(30).
k1 = k2->sol = 20: yeni kök olacak düğüm.
k2->sol = k1->sag: 25, 30'un soluna geçer (20 < 25 < 30, sıra korunur). 25'e şimdilik iki işaretçi var.
k1->sag = k2: 30, 20'nin sağ çocuğu olur. Bağlar bitti; çizimi düzeltelim →
Yükseklikler aşağıdan yukarı: önce k2 (30): 1 + MAX(1, 1) = 2.
Sonra k1 (20): 1 + MAX(yük(10)=2, 2) = 3. Alt ağacın yüksekliği eklemeden önceki gibi 3.
Yeni kök k1 döner; çağıran, babanın işaretçisini buna bağlar. Yalnızca 2 işaretçi değişti: O(1).
avl_gost sag_dondur(avl_gost k2) { // LL
avl_gost k1 = k2->sol;
k2->sol = k1->sag;
k1->sag = k2;
k2->yuk = 1 + MAX(yuk(k2->sol), yuk(k2->sag));
k1->yuk = 1 + MAX(yuk(k1->sol), k2->yuk);
return k1; // yeni kök
}Dengeli Ağaçlar
AVL'ye 3, 2, 1, 4, 5, 6, 7, 16, 15 ekleme
3 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.
2 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.
1 eklendi. Dengesizlik → LL (3'te sağa döndürme).
4 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.
5 eklendi. Dengesizlik → RR (3'te sola döndürme).
6 eklendi. Dengesizlik → RR (2'de sola döndürme).
7 eklendi. Dengesizlik → RR (5'te sola döndürme).
16 eklendi. Tüm faktörler −1..+1: döndürme gerekmedi.
15 eklendi. Dengesizlik → RL (16'da sağa, 7'de sola).
Aynı sıra sıradan BST'ye eklenseydi 3–4–5–6–7–16–15 zinciri yüzünden derinlik 7 olurdu; AVL'de 4. (Örnek: Weiss, Data Structures and Algorithm Analysis in C, 4.4)
Dengeli Ağaçlar
AVL ekleme: BST eklemesi + dönüşte denge
avl_gost avl_ekle(avl_gost t, int x) { if (!t) return yeni_avl(x); /* malloc, yuk = 1 */ if (x < t->veri) t->sol = avl_ekle(t->sol, x); else if (x > t->veri) t->sag = avl_ekle(t->sag, x); else return t; /* tekrar eden değer yok */ t->yuk = 1 + MAX(yuk(t->sol), yuk(t->sag)); int d = denge(t); if (d > 1 && x < t->sol->veri) return sag_dondur(t); /* LL */ if (d < -1 && x > t->sag->veri) return sol_dondur(t); /* RR */ if (d > 1) { t->sol = sol_dondur(t->sol); return sag_dondur(t); } /* LR */ if (d < -1) { t->sag = sag_dondur(t->sag); return sol_dondur(t); } /* RL */ return t; }
- Özyinelemeden dönerken her ata güncellenir; eklemede en çok bir (tek ya da çift) döndürme yeter.
- Kullanım:
kok = avl_ekle(kok, x);— yeni kök döndüğü için*Lgerekmez. Silme de benzer ama O(lg n) döndürme gerekebilir.
Dengeli Ağaçlar
Kısa bakış: kırmızı-siyah ağaç ve B-ağacı
- Her düğüm kırmızı ya da siyah; kök ve NULL yapraklar siyah.
- Kırmızı düğümün çocukları siyah.
- Her düğümden NULL'lara giden her yolda aynı sayıda siyah düğüm.
- Sonuç: yükseklik ≤ 2 lg(n+1); ekleme/silmede en çok 2–3 döndürme + renk değişimi.
- C++
std::map, JavaTreeMap, Linux çekirdeği.
- m yollu dengeli arama ağacı: düğüm başına ⌈m/2⌉−1 … m−1 anahtar.
- Tüm yapraklar aynı düzeyde.
- Dolan düğüm ortadaki anahtarı babaya çıkararak bölünür; ağaç kökten büyür.
- Bir düğüm = bir disk bloğu → çok az disk erişimi: veritabanı dizinleri, dosya sistemleri (B+ ağacı).
Dengeli Ağaçlar
Hangi arama ağacı?
| yapı | arama / ekleme / silme (en kötü) | denge koşulu | nerede? |
|---|---|---|---|
| ikili arama ağacı | O(n) (ortalama O(lg n)) | yok | küçük, rastgele veri; öğretim |
| AVL | O(lg n) | |yük(sol) − yük(sağ)| ≤ 1 | arama ağırlıklı işler |
| kırmızı-siyah | O(lg n) | siyah yükseklik eşit | kütüphane eşlemeleri (map) |
| B-ağacı / B+ | O(logm n) disk erişimi | yapraklar aynı düzeyde | veritabanı, dosya sistemi |
| splay (“zig-zag”) | O(lg n) amortize | yok; erişilen köke çıkar | sık erişilen öğeler |
Sınav Soruları
Sınav sorusu: bir öğenin ağırlığı
Değerlerin tekrarlı olmadığı bir ikili ağaçta x değerini içeren öğenin ağırlığı: “uç düğümlere kadar olan yolların en uzunu”. x yoksa ya da L = NULL ise 0. int AgirlikBul(agac_gost L, int x) yordamını yazınız. Global değişken kullanmayın.
Örnek çözüm
int AgirlikBul(agac_gost L, int x) { /* agacta_ara: x'i içeren öğe ya da NULL */ agac_gost agacta_ara(agac_gost L, int x); int derinlik(agac_gost L); return derinlik(agacta_ara(L, x)); } agac_gost agacta_ara(agac_gost L, int x) { if (!L) return NULL; if (L->veri == x) return L; agac_gost T = agacta_ara(L->solcocuk, x); return T ? T : agacta_ara(L->sagcocuk, x); } int derinlik(agac_gost L) { int max(int a, int b); /* a,b'den küçük olmayan */ return L ? max(derinlik(L->solcocuk), derinlik(L->sagcocuk)) + 1 : 0; }
Tasarım: iki iyi tanımlı yordamın bileşimi; derinlik(NULL) = 0 sayesinde “bulunamadı” ayrıca ele alınmaz.
Sınav Soruları
Sınav sorusu: AgirlikBul kaç kez işletilir?
int AgirlikBul(oge_gost L, int x) { if (!L) return 0; else if (L->veri == x) return agirlik(L); if (AgirlikBul(L->solcocuk, x) > AgirlikBul(L->sagcocuk, x)) AgirlikBul(L->solcocuk, x); if (AgirlikBul(L->sagcocuk, x) > AgirlikBul(L->solcocuk, x)) AgirlikBul(L->sagcocuk, x); return 0; }
Ağaç: kök 2, sol 10, sağ 20. i) x = 20 ve x = 30 için kaç çağrı? ii) En kötü durum için özyinelemeli ilişkiyi yazıp çözünüz.
Çözüm
i) x = 20: 14, x = 30: 21. Her yaprak çağrısı 1 + 4 NULL çağrısı = 5 çağrı yapar; x = 30'da kök 1 + 4·5 = 21.
ii) x ağaçta yoksa her çağrı her alt ağacı iki kez çağırır. Çarpık ağaçta T(n) = 2T(n−1) + c → Θ(2n); dengeli ağaçta T(n) = 4T(n/2) + c → Θ(n2). Doğru tasarım (ağırlık bulma sorusunun çözümü) O(n)'dir.
Sınav Soruları
Sınav sorusu: SWAP iki özyinelemeli çağrının arasında
{44, 30, 15, 18, 86, 97, 65, 113, 74, 129} ile kurulan BST için yordam'ı işletiniz; SWAP satırından her geçişte ağaç görünümü değişmişse yeniden çiziniz; son hâli yorumlayınız.
void yordam(agac_gost L) { agac_gost p; if (L) { yordam(L->solcocuk); SWAP(L->solcocuk, L->sagcocuk, p); yordam(L->sagcocuk); } }
Çözüm (adım adım)
Başlangıç: 44, 30, 15, 18, 86, 97, 65, 113, 74, 129 ile kurulan ikili arama ağacı.
SWAP 15'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.
SWAP 30'da: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.
SWAP 15'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.
SWAP 44'te: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.
SWAP 30'da: sol ve sağ yer değiştirdi. Sonra “sağ” (az önce sol olan) alt ağaç işlenir.
Son hâl: kökün iki alt ağacı yer değiştirdi ama sağa geçen alt ağaçlar (eski sol) iki kez işlendiği için tam ayna olmadı.
Yorum: SWAP öncesine ya da sonrasına yazılsaydı ağacın tam aynası elde edilirdi; arada olunca sol çocuk SWAP ile sağa geçip aynı düğüm için bir kez daha işlenir.
Özet
Karmaşıklık özeti
| işlem | en iyi / dengeli | en kötü (çarpık) | ek bellek |
|---|---|---|---|
| arama_agacinda_ara (döngüsel) | O(lg n) | O(n) | O(1) |
| ara (özyinelemeli) | O(lg n) | O(n) | O(h) yığın |
| arama_agacina_ekle | Ω(lg n) | O(n) | O(1) |
| sil | O(lg n) | O(n) | O(1) |
| derinlik | O(n) | O(n) | O(h) |
| genel ikili ağaçta ara | O(n) | O(n) | O(h) |
| AVL arama / ekleme / silme | O(lg n) | O(lg n) | O(lg n) özyineleme |
| tek döndürme | O(1) | O(1) | — |
Özet
Sık yapılan hatalar
agac_gost L yerine *L gerekir).return etmemek.MAX makrosuna özyinelemeli çağrı koyup üstel süreye düşmek.Özet
Alıştırmalar
- Boş bir BST'ye sırasıyla 50, 30, 70, 20, 40, 60, 80, 35 ekleyin; kök önde sırasını yazın.
- Aynı ağaçtan 30'u 1. ve 2. yöntemle silin; derinlikleri karşılaştırın.
- En küçük ve en büyük değeri döndüren döngüsel yordamları yazın.
- Bir ikili ağacın BST olup olmadığını O(n)'de denetleyen yordamı yazın.
- Boş AVL'ye 10, 20, 30, 25, 28 ekleyin; hangi döndürmeler yapılır?
- x'ten küçük anahtar sayısını bulan yordamı yazın (kök ortada gezinmeden yararlanılabilir).
İpuçları / cevaplar
- Kök önde: 50 30 20 40 35 70 60 80.
- 1. yöntem: 20 yukarı (derinlik 4 kalır); 2. yöntem: 35 yukarı, 40'ın solu NULL (derinlik 3).
- En küçük: sürekli
solcocuk; en büyük: süreklisagcocuk. - Her düğüme izin verilen (alt, üst) aralığı parametre olarak geçirilir; yalnız çocukla karşılaştırmak yetmez.
- 30'da RR (20 kök); 28'de LR: 30'un solu 25, önce 25'te sola, sonra 30'da sağa → 20(10, 28(25, 30)).
- Kök ortada gezinirken x'e kadar sayılır ya da düğümlerde alt ağaç boyutu tutulur.
Özet
Bölüm özeti
Sol alt ağaç küçük, sağ büyük, değerler tek; kök ortada = sıralı.
İlk NULL'a eklenir; arka işaretçi q ve *L ile boş ağaç.
Her karşılaştırma bir alt ağacı eler: O(h).
MAX(sol, sağ) + 1; global değişken ve gereksiz parametre yok.
0/1 çocuk kolay; 2 çocukta öncel/ardıl ile veri değiştir (1 bağ).
Sıralı veri O(n) yapar; AVL/kırmızı-siyah O(lg n) garanti eder.
Özet
Kaynaklar ve devam
- Horowitz–Sahni–Anderson-Freed, Fundamentals of Data Structures in C, 5.7 (BST), 10.2 (AVL).
- Cormen vd., Introduction to Algorithms, Bölüm 12 (BST), 13 (kırmızı-siyah), 18 (B-ağacı).
- M. A. Weiss, Data Structures and Algorithm Analysis in C, 4.4 (AVL); R. Sedgewick, Algorithms in C, Bölüm 12–13.