Veri Yapıları · Ders Notu

Bölüm 10: Arama Ağaçları

Sıralı bir dizide ikili arama çok hızlıdır, ama araya yeni bir öğe koymak bütün öğeleri kaydırmayı gerektirir. Bağlı listeye eklemek kolaydır, ama orada arama baştan sona yürümek demektir. Arama ağaçları bu iki dünyanın iyi yanlarını birleştirir. Öğeler anahtarlarına göre bir ağaçta öyle yerleştirilir ki her düğümde “sola mı, sağa mı?” sorusunun tek bir cevabı olur. Böylece arama, ekleme ve silme kökten bir yaprağa inen tek bir yol üzerinde yürür ve maliyet ağacın yüksekliğine bağlı kalır. Not, ikili arama ağacının (BST) tanımı ve temel işlemleriyle başlar. Ardından yüksekliğin neden her şeyi belirlediğini gösterir ve yüksekliği kendiliğinden küçük tutan AVL, kırmızı-siyah ve splay ağaçlarını anlatır. Son kesim, disk üzerindeki büyük veriler için geliştirilen B-ağacı ve B+ ağacıdır.

Öğrenme çıktıları

BST işlemleri
Arama, ekleme ve silme işlemlerini ikili arama ağacında uygulamak; ardıl, öncül ve aralık sorgularını yapmak.
Denge gereksinimi
Ağaç yüksekliğinin işlem maliyetlerine etkisini açıklamak; aynı anahtarların farklı sıralarla eklenince neden çok farklı ağaçlar verdiğini göstermek.
Dengeleme
AVL döndürmelerini ve kırmızı-siyah ağaç kurallarını yorumlamak; bir ekleme dizisinden sonra oluşan dengeli ağacı adım adım bulmak.
Karşılaştırma
BST, AVL, kırmızı-siyah, splay ve B-ağaçlarını kullanım amacına göre seçmek.

10.1Arama ağaçları

Bir telefon rehberinde “Yılmaz” soyadını arayan kimse rehberi ilk sayfadan okumaya başlamaz. Rehberi ortadan açar, soyadın o sayfadan önce mi sonra mı geldiğine bakar ve aramayı yarısına indirir. Bu yöntem rehber sıralı olduğu için işler. Bilgisayarda aynı yöntemin adı ikili aramadır ve sıralı bir dizide n öğe arasında aranan öğeyi yaklaşık log2 n karşılaştırmada bulur.

Sıralı dizinin zayıf yanı güncellemedir. Rehbere yeni bir kişi eklemek için ondan sonra gelen bütün kayıtların bir sıra kaydırılması gerekir; bu, en kötü durumda n kaydırma demektir. Bağlı listede ekleme ve silme ucuzdur, ama listenin ortasına doğrudan atlanamaz; arama baştan başlayıp öğe öğe ilerler. Arama ağaçları bu iki yapının arasında durur. Bağlı yapıları sayesinde ekleme ve silme yalnız birkaç bağı değiştirir. Sıralı düzenleri sayesinde de arama her adımda kalan adayların bir kısmını eler.

10.1.1Arama ağacı nedir?

Ağaçla ilgili temel kavramları kısaca hatırlayalım. Bir ağaç, düğümlerden ve düğümleri birbirine bağlayan kenarlardan oluşur. En üstteki düğüme kök (root), çocuğu olmayan düğümlere yaprak (leaf) denir. Bir düğümün derinliği (depth), kökten o düğüme inen yoldaki kenar sayısıdır; kökün derinliği 0'dır. Ağacın yüksekliği (height) ise en derin düğümün derinliğidir. Yükseklik genellikle h ile gösterilir. Tek düğümlü ağacın yüksekliği 0, boş ağacınki ise −1 kabul edilir.

Arama ağacında her düğüm bir öğe taşır ve her öğenin bir anahtarı (key) vardır. Anahtar, öğeyi ötekilerden ayıran ve karşılaştırılabilen değerdir: öğrenci kaydında öğrenci numarası, sözlükte kelimenin kendisi, rehberde soyadı. Öğenin geri kalanı (ad, adres, not ortalaması) anahtara bağlı olarak saklanır. Bu notta örnekleri sade tutmak için anahtar çoğunlukla bir tamsayıdır ve düğümde yalnız o tamsayı vardır.

Tanım 10.1

Arama ağacı (search tree), her düğümü bir öğe taşıyan ve öğeleri anahtarlarına göre sıralı bir düzende tutan ağaçtır. Düzen öyle kurulur ki bir anahtarı ararken her düğümde yalnız bir alt ağaca inmek yeter. Bu yüzden arama kökten başlar, her düzeyde tek bir dal seçer ve en çok h + 1 düğüm ziyaret eder.

Tanımın son cümlesi bölümün bütün hikâyesini özetler. Arama, ekleme ve silme kökten aşağı tek bir yol izler. Yolun uzunluğu en çok ağacın yüksekliği kadardır. Öyleyse bu işlemlerin maliyeti O(h)'dir. Yükseklik küçük kalırsa işlemler hızlı olur, büyürse yavaşlar.

6 2 1 4 3 8derinlik 0derinlik 1derinlik 2derinlik 3
Şekil 10.1: Kökü 6 olan altı düğümlü bir arama ağacı. Kökün derinliği 0, 2 ile 8'in derinliği 1, 1 ile 4'ün derinliği 2, 3'ün derinliği 3'tür. En derin düğüm 3 olduğu için ağacın yüksekliği h = 3'tür.

n düğümlü bir ağacın yüksekliği ne kadar olabilir? İki uç durum vardır. En kötü durumda her düğümün tek çocuğu olur ve ağaç bir zincire dönüşür; bu durumda yükseklik n − 1'dir. En iyi durumda her düzey tamamen doludur. Derinliği d olan düzeyde en çok 2d düğüm bulunabilir. Öyleyse yüksekliği h olan ikili ağaçta en çok 1 + 2 + 4 + … + 2h = 2h+1 − 1 düğüm vardır. n ≤ 2h+1 − 1 eşitsizliğinden h ≥ log2(n + 1) − 1 çıkar; yani en iyi durumda yükseklik yaklaşık log2 n'dir.

Örnek 10.1

Bir milyon (n = 106) anahtar bir arama ağacında tutulsun. Ağaç tam dengeliyse yükseklik yaklaşık log2 106 ≈ 19,93, yani 20 civarındadır; bir arama en çok 20 küsur karşılaştırma yapar. Ağaç bir zincire dönüşmüşse yükseklik 999.999'dur ve en kötü arama bir milyon karşılaştırma yapar. Aynı anahtarlar, aynı işlem; tek fark ağacın şeklidir. Aradaki oran yaklaşık 50.000 kattır.

Bu fark, bölümün geri kalanındaki bütün yapıların varlık nedenidir. Düz ikili arama ağacı yükseklik için hiçbir söz vermez. AVL ve kırmızı-siyah ağaçlar, her güncellemeden sonra ağacı küçük yerel değişikliklerle düzelterek h = O(log n) garantisi verir. Splay ağacı tek tek işlemler için söz vermez, ama uzun işlem dizilerinin ortalamasında aynı sınıra ulaşır. B-ağacı ise düğüm başına çok anahtar tutarak ağacı hem kısa hem geniş yapar.

Sık yapılan hata

Yüksekliği düğüm sayısıyla saymak. Yükseklik, kökten en derin yaprağa inen yoldaki kenar sayısıdır. Şekil 10.1'deki ağaçta 6 → 2 → 4 → 3 yolunda dört düğüm, üç kenar vardır; yükseklik 3'tür. Bazı kitaplar yüksekliği düğüm sayısıyla tanımlar; o zaman bütün formüller bir kayar. Bu notta yükseklik her yerde kenar sayısıdır ve boş ağacın yüksekliği −1'dir.

10.1.2Temel işlemler

Bir arama ağacından beklenen işlemler, sıralı bir koleksiyondan beklenenlerle aynıdır. Tablo 10.1 bunları ve bu notta kullanılan metot adlarını verir.

Tablo 10.1: Arama ağacının temel işlemleri. Örnekler sunumdaki ağaçlara aittir.
İşlemNe yapar?Örnek
Arama (search)Verilen anahtar ağaçta varsa true, yoksa false döndürür.ara(kok, 5) → true
Ekleme (insertion)Yeni öğeyi sıralama düzenini bozmadan uygun yaprak konumuna ekler.ekle(kok, 7)
Silme (deletion)Verilen anahtarlı öğeyi çıkarır ve düzeni korur.sil(kok, 40)
Gezinme (traversal)Bütün öğeleri belirli bir sırayla ziyaret eder. Kök ortada gezinme anahtarları sıralı verir.inorder(kok) → 1 2 3 4 6 8

Sıralı düzen birkaç sorguyu daha mümkün kılar. En küçük ve en büyük anahtar bunların ilk ikisidir. Bir anahtarın ardılı kendisinden büyük en küçük anahtar, öncülü ise kendisinden küçük en büyük anahtardır. Son sorgu, belli bir aralığa düşen bütün anahtarları ister. Bu sorguların ortak noktası, hepsinin de kökten tek bir yol izlemesidir. Gezinme dışındaki bütün işlemler bu yüzden O(h) sürer. Gezinme ise her düğümü bir kez ziyaret ettiği için O(n)'dir.

Hash tablosu da arama ve eklemeyi ortalama sabit sürede yapar. Arama ağacını ondan ayıran şey bu sıralı sorgulardır. Hash tablosu anahtarları dağıtarak saklar. “50'den büyük en küçük anahtar hangisi?” ya da “20 ile 45 arasındaki anahtarları sırayla ver” gibi sorular için bütün tabloyu taraması gerekir. Arama ağacı ise bu soruları da O(h) ya da O(h + s) sürede cevaplar; s burada döndürülen anahtar sayısıdır.

10.1.3Arama ağacı türleri

Bölümde dört ikili arama ağacı türü ve iki çok yollu ağaç işlenir. İkili türlerin hepsinde aynı sıralama kuralı geçerlidir; farkları, yüksekliği nasıl ve ne ölçüde denetledikleridir. Şekil 10.2 dört ikili türün küçük birer örneğini gösterir.

  • İkili arama ağacı (binary search tree, kısaca BST): Her düğümün anahtarı, sol alt ağacındaki anahtarların hepsinden büyük, sağ alt ağacındakilerin hepsinden küçüktür. Denge garantisi yoktur; şekli ekleme sırasına bağlıdır.
  • AVL ağacı: Her düğümde iki alt ağacın yükseklik farkı, yani denge faktörü en çok 1'dir. Bu koşul bozulunca döndürmelerle düzeltilir.
  • Kırmızı-siyah ağaç (red-black tree): Düğümler kırmızı ya da siyaha boyanır. Renk kuralları yüksekliği en çok 2 log2(n + 1) ile sınırlar.
  • Splay ağacı (splay tree): Erişilen düğümü döndürmelerle köke taşır. Tek bir işlem pahalı olabilir, ama işlem başına amortize maliyet O(log n)'dir.
63148 40201030605070
201051530 53148← son erişilen
Şekil 10.2: Dört ikili arama ağacı türü. (a) BST: 6 kökte; solunda 3, 1, 4, sağında 8. (b) AVL: yedi düğümlü, her düğümde denge faktörü 0 olan mükemmel dengeli ağaç. (c) Kırmızı-siyah: 20 siyah kök, 10 kırmızı, 30, 5 ve 15 siyah. (d) Splay: en son erişilen 5 kökte duruyor.

Çok yollu ağaçlarda (B-ağacı ve B+ ağacı) bir düğüm birden çok anahtar ve ikiden çok çocuk taşır. Bu ağaçlar özellikle verinin diskte durduğu durumlar için tasarlanmıştır ve 10.7'nin konusudur.

10.2İkili arama ağacı

İkili ağaçta her düğümün en çok iki çocuğu vardır: sol çocuk ve sağ çocuk. İkili arama ağacı, ikili ağaca bir sıralama kuralı ekler. Bu kural sayesinde ağaç, sıralı bir dizinin “bağlı” bir karşılığı gibi davranır.

10.2.1BST özelliği

Tanım 10.2

İkili arama ağacı (binary search tree, BST), her düğümü x için şu koşulu sağlayan ikili ağaçtır. x'in sol alt ağacındaki bütün anahtarlar x.anahtar'dan küçüktür. Sağ alt ağacındaki bütün anahtarlar ise x.anahtar'dan büyüktür. Bu koşula BST özelliği denir.

Tanımdaki “bütün” sözcüğü önemlidir. Koşul yalnız düğüm ile çocukları arasında değil, düğüm ile bütün torunları arasında da geçerlidir. Kökün sol alt ağacındaki en derin yaprak bile kökten küçük olmak zorundadır. Şekil 10.3 bu farkı gösterir. Soldaki ağaç geçerli bir BST'dir. Sağdakinde 4'ün sağ çocuğu olarak 7 eklenmiştir. Yalnız 4 ile 7'ye bakılırsa sorun yoktur: 7, 4'ün sağındadır ve 4'ten büyüktür. Ama 7, 6'nın sol alt ağacındadır ve 6'dan büyüktür. Bu yüzden sağdaki ağaç BST değildir.

6 2 1 4 3 8 6 2 1 4 3 7 8✗
Şekil 10.3: Solda geçerli bir ikili arama ağacı. Sağda aynı ağaçta 4'ün sağ çocuğu olarak 7 eklenmiş. 7, 4'ten büyük olduğu için ebeveyniyle sorun yok; ama 6'nın sol alt ağacında olduğu hâlde 6'dan büyük. Ağaç BST değildir.

BST özelliğinin doğrudan bir sonucu şudur: bir düğümün anahtarı, ağacın kalanını iki parçaya böler. Küçük anahtarların hepsi solda, büyüklerin hepsi sağdadır. Bu, sıralı dizide ortadaki öğenin diziyi ikiye bölmesine benzer. İkili aramanın her adımda aralığı yarıya indirmesi gibi, BST'de de her düğümde aramanın gideceği taraf kesinleşir ve öbür taraf tamamen elenir.

Aynı anahtar ağaçta iki kez bulunabilir mi? Tanım katı eşitsizlik kullandığı için hayır. Uygulamada iki yol vardır. Birincisi, tekrarlayan anahtarları reddetmektir; bu notun kodları böyle davranır. İkincisi, eşit anahtarları tutarlı biçimde hep aynı tarafa, örneğin hep sağa koymaktır. Hangisi seçilirse seçilsin, arama, ekleme ve silme aynı kurala uymalıdır. Yoksa eklenen bir anahtar sonra bulunamayabilir.

Sık yapılan hata

BST özelliğini yalnız ebeveyn ile çocuk arasında denetlemek. “Her düğümün sol çocuğu küçük, sağ çocuğu büyük” koşulu Şekil 10.3'teki sağ ağaçta da sağlanır; ama ağaç BST değildir. Doğru denetim, her düğüm için izin verilen bir alt ve üst sınırı kökten aşağı taşır (10.3.12).

10.2.2Düğüm yapısı

BST'nin her düğümü üç alandan oluşur: veri (anahtar), sol çocuğa bir referans ve sağ çocuğa bir referans (Şekil 10.4). Çocuğu olmayan tarafta referans null'dır. Yaprakta iki referans da null'dır. Bağlı listedeki düğümle karşılaştırırsak tek fark, bir “sonraki” yerine iki çocuk referansının bulunmasıdır.

solverisagsol çocuksağ çocuk
Şekil 10.4: İkili ağaç düğümünün üç alanı: sol çocuk referansı, veri ve sağ çocuk referansı.

Sunumda düğüm iki biçimde yazılır. İlki yalnız int anahtar tutan sade bir sınıftır:

class AgacDugumu {
    int veri;
    AgacDugumu sol, sag;

    AgacDugumu(int veri) {
        this.veri = veri;
        this.sol = null;
        this.sag = null;
    }
}

Yapıcı, veriyi yazar ve iki çocuğu açıkça null yapar. Java'da referans türündeki alanlar zaten null ile başladığı için son iki atama zorunlu değildir; okuyana niyeti göstermek için yazılmıştır. Ardıl, aralık sorgusu ve BST doğrulama kodları bu sınıfı kullanır; alan adları kısadır: sol ve sag.

İkinci biçim jenerik (generic) sınıftır. Anahtar tamsayı olmak zorunda değildir; karşılaştırılabilen her tür olabilir:

class IkiliAgacDugum<E extends Comparable<E>> {
    E veri;                    // nesne
    IkiliAgacDugum<E> solCocuk; // sol çocuk
    IkiliAgacDugum<E> sagCocuk; // sağ çocuk

    IkiliAgacDugum(E veri) {
        this.veri = veri;
        // çocuklar başlangıçta null
    }
}

Tür parametresi E extends Comparable<E> biçiminde sınırlanmıştır. Bu, “E türünden iki nesne compareTo ile karşılaştırılabilmelidir” demektir. Integer, String ve LocalDate gibi sınıflar Comparable'ı gerçekler; bu yüzden hepsi anahtar olabilir. a.compareTo(b) çağrısı a < b ise negatif, eşitse 0, a > b ise pozitif bir sayı döndürür. Arama, ekleme ve silme kodları karar verirken yalnız bu işarete bakar. Bu sınıfta alan adları solCocuk ve sagCocuk'tur.

Bellek açısından her düğüm, verinin yanında iki referans taşır. n düğümlü ağaç bu yüzden O(n) bellek kullanır. Dengeli ağaç türleri düğüme küçük bir ek bilgi ekler: AVL ağacında yükseklik, kırmızı-siyah ağaçta bir bitlik renk. Bu ekler de düğüm başına sabittir; toplam bellek yine O(n) kalır.

10.2.3En küçük ve en büyük anahtar

BST özelliğinden ilk çıkan sonuç, en küçük ve en büyük anahtarın yeridir. Bir düğümün sol alt ağacında ondan küçük anahtarlar vardır. Öyleyse sol çocuğu olan bir düğüm en küçük olamaz; daha küçüğü solundadır. Bu akıl yürütme en küçük anahtara kökten başlayıp hep sola inerek ulaşıldığını gösterir. İniş, sol çocuğu olmayan ilk düğümde durur; o düğüm en küçüktür. En büyük için aynı yol sağa doğru izlenir (Şekil 10.5).

6 2 1 4 3 8min: hep solamax: hep sağa
Şekil 10.5: En küçük anahtar 1'e kökten hep sola (6 → 2 → 1), en büyük anahtar 8'e hep sağa (6 → 8) inilerek ulaşılır.

Sunumdaki kod iki metodu bir arada verir:

IkiliAgacDugum<E> minDeger(
        IkiliAgacDugum<E> dugum) {
    while (dugum.solCocuk != null)
        dugum = dugum.solCocuk; // sola in
    return dugum;  // en küçük
}
IkiliAgacDugum<E> maxDeger(
        IkiliAgacDugum<E> dugum) {
    while (dugum.sagCocuk != null)
        dugum = dugum.sagCocuk; // sağa in
    return dugum;  // en büyük
}

minDeger, kendisine verilen düğümden başlar. Döngü koşulu “bu düğümün sol çocuğu var mı?” sorusudur. Varsa dugum sol çocuğa geçer. Sol çocuk null olunca döngü biter ve o anki düğüm döndürülür. maxDeger aynı işi sağ çocuklarla yapar. İki metodun da ön koşulu ağacın boş olmamasıdır; dugum başlangıçta null olursa dugum.solCocuk ifadesi NullPointerException fırlatır.

Metotlar hiç anahtar karşılaştırması yapmaz; yalnız bağları izler. Her döngü turunda bir düzey aşağı inilir. En çok h tur dönülür, dolayısıyla süre O(h)'dir. Ek bellek olarak yalnız bir değişken kullanılır: O(1).

Metotların parametre olarak kökü değil herhangi bir düğümü alması bilinçli bir seçimdir. Silme kodu, iki çocuklu bir düğümün yerine koyacağı değeri bulmak için minDeger'i o düğümün sağ alt ağacının kökünden başlatır. Yani aynı metot “bütün ağacın en küçüğü” için de “bir alt ağacın en küçüğü” için de kullanılır.

Sık yapılan hata

Döngü koşulunu while (dugum != null) diye yazmak. Bu döngü en soldaki düğümü de geçer ve null'a ulaşınca durur; metot her zaman null döndürür. Doğru koşul bir adım önce durur: sol çocuğu olmayan düğümde. “Bir sonraki var mı?” diye sorup ondan sonra ilerlemek gerekir.

10.2.4Kod izleme: minDeger ve maxDeger

İzleme, Şekil 10.5'teki ağaç üzerinde yapılır. Ağaç 6, 2, 8, 1, 4, 3 sırasıyla eklenerek oluşmuştur: kök 6; solunda 2, sağında 8; 2'nin çocukları 1 ve 4; 4'ün sol çocuğu 3. Tablo 10.2 her döngü turunda dugum'ın nerede olduğunu ve koşulun nasıl değerlendirildiğini verir.

Tablo 10.2: minDeger(kok) ve maxDeger(kok) izlemesi.
ÇağrıAdımdugumKoşulSonuç
minDeger16solCocuk = 2 ≠ nullsola in
22solCocuk = 1 ≠ nullsola in
31solCocuk = nulldöngü biter, return 1
maxDeger16sagCocuk = 8 ≠ nullsağa in
28sagCocuk = nulldöngü biter, return 8

İzlemede dikkat çekici bir nokta vardır. Ağaçtaki en derin düğüm 3'tür, ama 3 en küçük değildir. 3, 4'ün solundadır; 4 de 2'nin sağındadır. Yani 3, 2'den büyüktür. En küçük anahtar en derin düğümde değil, en soldaki düğümdedir. Şekil 10.6 minDeger'in üç anını gösterir.

6dugum21438 62dugum1438
621dugum438 621438dugum
Şekil 10.6: Kod izlemesinin dört anı. (a) minDeger(kok): dugum kökten (6) başlar. (b) dugum = dugum.solCocuk: 2'ye inildi. (c) 1'in sol çocuğu yok; döngü bitti, en küçük 1. (d) maxDeger 6'dan 8'e iner ve 8'de durur: en büyük 8.

10.2.5Kök ortada gezinme

Gezinme, ağacın bütün düğümlerini belli bir sırayla ziyaret etmektir. İkili ağaçta üç klasik özyinelemeli sıra vardır: kök başta (preorder), kök ortada (inorder) ve kök sonda (postorder). Adlar, kökün iki alt ağaca göre ne zaman ziyaret edildiğini söyler. Kök ortada gezinmede önce sol alt ağaç, sonra kök, en son sağ alt ağaç gezilir.

void inorder(IkiliAgacDugum<E> d) {
    if (d == null) return;
    inorder(d.solCocuk);  // sol
    System.out.print(d.veri + " ");
    inorder(d.sagCocuk);  // sağ
}

Metot üç satırdan oluşur ve her satır sıranın bir parçasıdır. Boş alt ağaçta (d == null) hiçbir şey yapmadan döner; bu, özyinelemenin taban durumudur. Boş değilse önce sol alt ağacı bütünüyle gezer, sonra kendi verisini yazar, en son sağ alt ağacı gezer.

BST'de bu sıra çok değerli bir sonuç verir: anahtarlar küçükten büyüğe sıralı yazılır. Nedenini bir düğüm için düşünelim. Düğüm yazılmadan önce sol alt ağacının tamamı yazılmıştır ve oradaki her anahtar düğümden küçüktür. Düğümden sonra sağ alt ağaç yazılır ve oradaki her anahtar düğümden büyüktür. Her düğüm, kendinden küçüklerin hepsinden sonra ve kendinden büyüklerin hepsinden önce yazılır. Bu da tam olarak sıralı çıktının tanımıdır.

Teorem 10.1 (Kök ortada gezinme sıralıdır)

Bir ikili arama ağacının kök ortada gezinmesi, ağaçtaki anahtarları artan sırayla verir.

Ağacın yüksekliği üzerinden tümevarım yapalım. Boş ağaç için çıktı boştur ve iddia doğrudur. Yüksekliği h olan bir ağacın kökü r olsun. Sol ve sağ alt ağaçların yüksekliği h'den küçüktür; tümevarım varsayımıyla ikisinin gezinmesi de kendi anahtarlarını artan sırayla verir. Çıktı “sol alt ağacın sıralı listesi, r, sağ alt ağacın sıralı listesi” biçimindedir. BST özelliğine göre sol listedeki her anahtar r'den küçük, sağ listedeki her anahtar r'den büyüktür. Üç parça art arda konunca liste bütünüyle artan sırada olur. □

Teoremin tersi de doğrudur: kök ortada gezinmesi kesin artan sıra veren ikili ağaç bir BST'dir. Bu yüzden bir ağacın BST olup olmadığını denetlemenin bir yolu, kök ortada gezinip çıktının artan olduğuna bakmaktır.

Gezinme her düğümü bir kez yazar. Her düğüm için iki özyinelemeli çağrı yapılır; bunların toplam sayısı da O(n)'dir (n dolu düğüm için n + 1 boş çağrı vardır). Süre bu yüzden O(n)'dir. Bellek açısından çağrı yığınında en çok h + 1 çerçeve birikir: O(h).

10.2.6Kod izleme: inorder

Aynı altı düğümlü ağaçta inorder(kok) çağrıldığında olaylar Tablo 10.3'teki sırayla gelişir. “Bekleyenler” sütunu, o anda çağrı yığınında duran ve sol alt ağaçlarının bitmesini bekleyen düğümleri alttan üste sıralar.

Tablo 10.3: inorder(kok) izlemesi (ağaç: 6(2(1, 4(3, —)), 8)).
AdımOlayBekleyenlerÇıktı
1inorder(6) → önce sol: inorder(2)6
2inorder(2) → önce sol: inorder(1)6, 2
3inorder(1): sol null, hemen döner; 1 yazılır6, 21
41'in sağı null; çerçeve kapanır; 2 yazılır61 2
52'nin sağı: inorder(4) → önce sol: inorder(3)6, 41 2
6inorder(3): solu ve sağı null; 3 yazılır6, 41 2 3
74 yazılır; 4'ün sağı null; inorder(4) ve inorder(2) kapanır61 2 3 4
86 yazılır; sağ alt ağaç: inorder(8)—1 2 3 4 6
9inorder(8): solu null; 8 yazılır; sağı null—1 2 3 4 6 8

Çıktı 1 2 3 4 6 8'dir ve sıralıdır. İzlemede her düğümün “bekleme” süresi dikkat çekicidir. 6 ilk çağrılan düğümdür, ama beşinci yazılır; çünkü kendisinden küçük dört anahtarın hepsi sol alt ağacındadır. 8 en son çağrılır ve en son yazılır. Şekil 10.7 izlemenin dört anını gösterir.

621d438 62d1438
6214d38 621438
Şekil 10.7: inorder izlemesinden dört an; yazdırılmış düğümler turuncudur. (a) Sol taraf bitti, 1 yazıldı. (b) 2 yazıldı. (c) 4 yazıldı: 1, 2, 3, 4 sırayla çıktıda. (d) Bütün çağrılar bitti; çıktı 1 2 3 4 6 8. Her düğüm bir kez yazıldı: O(n).
Sık yapılan hata

Kök ortada gezinmenin sıralı çıktı vermesini her ikili ağaçtan beklemek. Bu özellik yalnız BST'de geçerlidir. Sıralama kuralı olmayan bir ikili ağaçta kök ortada gezinme anahtarları ağacın şekline göre karışık bir sırada verir. Tersine, BST'de kök başta ya da kök sonda gezinme de sıralı çıktı vermez; sıralı çıktıyı yalnız kök ortada sıra verir.

10.3BST işlemleri

BST'nin bütün güncelleme ve sorgu işlemleri aynı iskelete dayanır: kökten başla, aranan anahtarı düğümün anahtarıyla karşılaştır, küçükse sola, büyükse sağa in. İşlemleri birbirinden ayıran, bu inişin nerede durduğu ve durduktan sonra ne yapıldığıdır. Arama, eşit anahtarı bulunca ya da boş bir bağa düşünce durur. Ekleme boş bağa düşünce oraya yeni bir düğüm bağlar. Silme, bulduğu düğümü çocuk sayısına göre üç farklı biçimde ağaçtan çıkarır.

10.3.1Arama

Arama algoritması üç kuraldan oluşur:

  1. Kökten başla. Düğümün anahtarı aranan anahtara eşitse arama başarıyla biter.
  2. Aranan anahtar küçükse sol alt ağaçta, büyükse sağ alt ağaçta aynı işi tekrarla.
  3. null bir bağa ulaşılırsa anahtar ağaçta yoktur.

Üçüncü kuralın doğruluğu BST özelliğinden gelir. Aranan anahtar ağaçta olsaydı, her adımda seçilen alt ağaçta olmak zorundaydı; çünkü öbür alt ağaçtaki anahtarların hepsi ya ondan küçük ya da ondan büyüktür. Seçilen alt ağaç boş çıktıysa anahtarın bulunabileceği başka yer kalmamıştır.

6 4 2 5 8 7 95 < 65 > 4
Şekil 10.8: 6, 4, 8, 2, 5, 7, 9 anahtarlı ağaçta 5 aranıyor. 5 < 6 olduğu için sola, 5 > 4 olduğu için sağa inilir ve 5 bulunur.

Sunumdaki kod jenerik düğüm sınıfıyla yazılmıştır ve özyinelemelidir:

boolean ara(IkiliAgacDugum<E> kok, E veri) {
    if (kok == null)
        return false;            // yok
    int k = veri.compareTo(kok.veri);
    if (k == 0)
        return true;             // bulundu
    else if (k < 0)
        return ara(kok.solCocuk, veri);
    else
        return ara(kok.sagCocuk, veri);
}

İlk if taban durumudur: boş alt ağaçta hiçbir anahtar yoktur, sonuç false'tur. Sonra aranan veri düğümün verisiyle bir kez karşılaştırılır ve sonuç k'de saklanır. k == 0 ise anahtar bulunmuştur. k < 0 ise aranan daha küçüktür ve arama sol alt ağaçta, k > 0 ise sağ alt ağaçta sürer. Özyinelemeli çağrının döndürdüğü değer, hiç değiştirilmeden yukarı aktarılır.

Karşılaştırmanın bir kez yapılıp sonucun bir değişkende tutulması hem okunabilirliği hem de verimi artırır. compareTo bir String ya da karmaşık bir nesne için pahalı olabilir; iki ayrı if'te iki kez çağırmak gereksizdir.

Sık yapılan hata

Nesne anahtarları == ile karşılaştırmak. ==, iki referansın aynı nesneyi gösterip göstermediğine bakar; değerlerin eşitliğine bakmaz. Integer anahtarlı bir ağaçta düğümdeki 1000 ile aranan 1000, ayrı ayrı kutulanmış (boxing) iki nesnedir. veri == kok.veri yanlış çıkar ve ağaçta olan anahtar bulunamaz. Küçük değerlerde (−128 ile 127 arası) Java hazır nesneleri yeniden kullandığı için hata gizli kalır ve yalnız büyük sayılarda ortaya çıkar. Sıralama için compareTo, eşitlik için compareTo(...) == 0 ya da equals kullanılır.

Maliyet açısından her çağrı bir düzey aşağı iner ve sabit iş yapar. Başarılı arama, bulunan düğümün derinliğinin bir fazlası kadar düğüme bakar. Başarısız arama bir yaprağın boş çocuğuna kadar iner ve en çok h + 1 düğüme bakar. İkisi de O(h)'dir. Özyinelemeli sürüm çağrı yığınında O(h) bellek kullanır. Aynı algoritma bir while döngüsüyle de yazılabilir; o zaman ek bellek O(1)'e iner. Özyinelemeli çağrı metodun son işi olduğu için bu dönüşüm mekaniktir: kok = kok.solCocuk ya da kok = kok.sagCocuk atanıp döngü başa döner.

10.3.2Kod izleme: ara

İki çağrıyı Şekil 10.8'deki ağaçta izleyelim: başarılı ara(kok, 5) ve başarısız ara(kok, 10). Şekildeki ağaç 6(4(2, 5), 8(7, 9)) biçimindedir. Tablo satırlarındaki her çağrı, çağrı yığınında yeni bir çerçeve açar.

Tablo 10.4: ara(kok, 5) ve ara(kok, 10) izlemesi.
ÇağrıkokkKarar
ara(kok, 5)6−1 (5 < 6)sol alt ağaç: ara(4, 5)
41 (5 > 4)sağ alt ağaç: ara(5, 5)
50bulundu, return true; 4 ve 6'daki çerçeveler true'yu aynen döndürür
ara(kok, 10)61 (10 > 6)sağ: ara(8, 10)
81 (10 > 8)sağ: ara(9, 10)
91 (10 > 9)sağ: ara(null, 10)
null—9'un sağ çocuğu yok: return false; 9, 8 ve 6'daki çerçeveler false'u aynen döndürür

Başarılı aramada üç düğümle karşılaştırma yapıldı. Bu sayı, 5'in derinliği olan 2'nin bir fazlasıdır. Başarısız aramada da üç düğüme bakıldı (6, 8, 9) ve dördüncü çağrı boş bir bağa düştü. Ağacın yüksekliği 2 olduğu için en çok h + 1 = 3 düğüme bakılabilirdi; iki arama da bu sınıra ulaştı. Yedi düğümlü ağaçta hiçbir arama üç karşılaştırmayı geçmez. Aynı yedi anahtar bir dizide sırasız dursaydı, başarısız bir arama yedi karşılaştırma isterdi.

6kok425879 6425kok879
6kok425879 6425879kok
Şekil 10.9: Arama izlemesinden dört an. (a) ara(kok, 5): 5 < 6, k = −1. (b) Üçüncü çerçevede kok 5'i gösterir ve k = 0. (c) ara(kok, 10): 10 > 6, sağa. (d) kok == null: 9'un sağ çocuğu yok, 10 ağaçta değil.

10.3.3Ekleme

Yeni bir anahtar nereye eklenmelidir? Cevap aramanın kendisidir. Anahtar ağaçta aranıyormuş gibi kökten aşağı inilir. Anahtar ağaçta yoksa arama bir null bağda biter. Bu bağ, anahtarın BST özelliğini bozmadan durabileceği tek yerdir; çünkü yol boyunca her düğümde doğru tarafa dönülmüştür. Yeni düğüm oraya yaprak olarak bağlanır.

6 2 1 4 3 5 8ekle 5
Şekil 10.10: 6, 2, 8, 1, 4, 3 anahtarlı ağaca 5 ekleniyor. 6'dan sola, 2'den sağa, 4'ten sağa inilir; 4'ün sağ bağı boş olduğu için 5 oraya yaprak olarak bağlanır (kesikli çizgi).
IkiliAgacDugum<E> ekle(
        IkiliAgacDugum<E> kok, E veri) {
    if (kok == null)
        return new IkiliAgacDugum<>(veri);
    int k = veri.compareTo(kok.veri);
    if (k < 0)
        kok.solCocuk = ekle(kok.solCocuk, veri);
    else if (k > 0)
        kok.sagCocuk = ekle(kok.sagCocuk, veri);
    // k == 0: anahtar zaten var, eklenmez
    return kok;
}
// kullanım: kok = ekle(kok, 5);

Kodun en önemli özelliği dönüş değeridir. ekle, kendisine verilen alt ağacın ekleme sonrasındaki kökünü döndürür. Boş bir alt ağaca ekleme yapılınca yeni düğüm oluşturulur ve döndürülür; artık o alt ağacın kökü bu yeni düğümdür. Boş olmayan alt ağaçta kök değişmez; metot çocuk bağını özyinelemeli çağrının sonucuyla günceller ve kendi kökünü döndürür.

kok.solCocuk = ekle(kok.solCocuk, veri) satırı ilk okumada gereksiz görünebilir: sol çocuk zaten oradadır, neden yeniden atanıyor? Atamanın tek gerçek işi, inişin sonunda yapılan atamadır. Yeni düğümün ebeveyni olacak düğümde kok.sagCocuk o ana kadar null'dır; çağrı yeni düğümü döndürür ve atama onu bağlar. Yol üzerindeki öteki atamalar bağı kendi değerine yeniden yazar ve hiçbir şeyi değiştirmez. Bu “gereksiz” atamalar sayesinde kod, yeni düğümün ebeveynini ayrıca hatırlamak zorunda kalmaz.

Aynı desen kullanımda da sürer: ağaca ekleme her zaman kok = ekle(kok, 5); biçiminde yazılır. Ağaç boşken (kok == null) ilk ekleme yeni bir kök oluşturur ve onu ancak bu atama kok değişkenine yazar.

Sık yapılan hata

Dönüş değerini atamadan ekle(kok, 8); yazmak. Java'da parametreler değerle geçer. Metot içinde kok parametresine ne atanırsa atansın, çağıranın kok değişkeni değişmez. Boş ağaçta ekle(null, 8) yeni düğümü döndürür, ama sonuç bir yere yazılmadığı için kaybolur; ağaç boş kalır. Dolu ağaçta hata gizlenir, çünkü yeni yaprak iç çağrıda kok.solCocuk = … atamasıyla bağlanır. Kökün değiştiği her durumda (boş ağaç, kökün silinmesi, dengeli ağaçta kökte döndürme) ağaç bozulur.

Eşit anahtar geldiğinde (k == 0) iki if de atlanır ve metot hiçbir şeyi değiştirmeden kok'u döndürür; anahtar ikinci kez eklenmez. Eklemenin maliyeti aramayla aynıdır: kökten bir yaprağa inilir, her düzeyde sabit iş yapılır. Süre O(h), özyinelemeli sürümde çağrı yığını O(h)'dir. Yeni düğüm her zaman bir yaprak olarak eklenir; var olan düğümlerin yeri değişmez. Bu basitlik, düz BST'nin şeklinin bütünüyle ekleme sırasına bağlı olmasının da nedenidir.

10.3.4Kod izleme: ekle

Şekil 10.10'daki ağaca kok = ekle(kok, 5) çağrısıyla 5 eklenir. Tablo 10.5 hem inişi hem dönüşü gösterir. Dönüş satırlarında, her çerçevenin çocuk bağına hangi değeri atadığı yazılmıştır.

Tablo 10.5: kok = ekle(kok, 5) izlemesi (ağaç: 6(2(1, 4(3, —)), 8)).
AdımÇerçevekOlay
1kok = 6−1sola in: ekle(kok.solCocuk, 5)
2kok = 21sağa in: ekle(kok.sagCocuk, 5)
3kok = 41sağa in: ekle(kok.sagCocuk, 5)
4kok = null—new ile düğüm 5 oluşturulur ve döndürülür; henüz hiçbir bağ onu göstermez
5kok = 4—kok.sagCocuk = düğüm(5): boş bağın yerine yeni düğüm bağlandı; return 4
6kok = 2—kok.sagCocuk = düğüm(4): bağ değişmez; return 2
7kok = 6—kok.solCocuk = düğüm(2): bağ değişmez; return 6
8main—kok = 6: kök değişmedi

Sonuç ağacı 6(2(1, 4(3, 5)), 8)'dir ve kök ortada gezinmesi 1 2 3 4 5 6 8 verir. Üç düzey inildi, üç çerçeve açıldı ve geri dönüldü. Gerçekte değişen tek bağ 4'ün sağ bağıdır. Şekil 10.11, yeni düğümün oluştuğu ve bağlandığı anları gösterir.

6214kok38 6214385yeni
6214kok358 6kok214358
Şekil 10.11: Ekleme izlemesinden dört an. (a) 4'ten sağa inilirken kok = null. (b) Düğüm 5 oluşturuldu; hiçbir bağ onu göstermiyor. (c) 4'ün çerçevesinde kok.sagCocuk yeni düğüme bağlandı. (d) main'de kok = 6: kök değişmedi, yeni düğüm yaprak oldu.

10.3.5Ardıl, öncül, tavan ve taban

Sıralı bir listede her öğenin bir “sonraki” ve bir “önceki” komşusu vardır. BST'de bu komşulara ardıl ve öncül denir.

Tanım 10.3

Bir x anahtarının ardılı (successor), ağaçta x'ten büyük en küçük anahtardır. Öncülü (predecessor) ise x'ten küçük en büyük anahtardır. x'in kendisinin ağaçta bulunması gerekmez. Kök ortada sırada ardıl, x'ten hemen sonra; öncül hemen önce gelir.

x ağaçtaysa ardılını bulmak için iki durum vardır (Şekil 10.12):

  1. Sağ alt ağaç varsa, ardıl sağ alt ağacın en küçüğüdür: bir kez sağa, sonra hep sola inilir. x'ten büyük ve ona en yakın anahtarlar onun sağ alt ağacındadır; oradaki en küçük olan da ardıldır.
  2. Sağ alt ağaç yoksa, ardıl, kökten x'e inerken solundan geçilen en yakın atadır. Örneğin 5'e 6 → 4 → 5 yoluyla inilir. Bu yolda 6'dan sola dönülmüştür; yani 5, 6'nın sol alt ağacındadır ve 6'dan küçüktür. 4'ten ise sağa dönülmüştür; 4, 5'ten küçüktür ve ardıl olamaz. Ardıl 6'dır.
6x425879 6425x879
Şekil 10.12: Ardılın iki durumu, 6(4(2, 5), 8(7, 9)) ağacında. (a) 6'nın sağ alt ağacı var: bir sağa (8), sonra hep sola (7); ardil(6) = 7. (b) 5'in sağ alt ağacı yok: solundan geçilen en yakın ata 6; ardil(5) = 6.

Ardılın iki yüzlü bu tarifi koda geçirilirken birleştirilebilir. Kökten başlayıp aşağı inen tek bir döngü, yol boyunca “şimdiye kadarki en iyi aday”ı tutar:

AgacDugumu ardil(AgacDugumu kok, int x) {
    AgacDugumu aday = null; // x'ten büyük en küçük
    while (kok != null) {
        if (x < kok.veri) {
            aday = kok;     // kok bir aday
            kok = kok.sol;  // daha küçüğü solda
        } else
            kok = kok.sag;  // x'ten büyükler sağda
    }
    return aday;
}

Her düğümde iki olasılık vardır. x < kok.veri ise bu düğüm x'ten büyüktür ve bir ardıl adayıdır. Daha iyi bir aday, yani x'ten büyük ama bu düğümden küçük bir anahtar, ancak düğümün solunda olabilir; aday kaydedilir ve sola inilir. x ≥ kok.veri ise bu düğüm ve bütün sol alt ağacı x'ten küçük ya da ona eşittir; hiçbiri ardıl olamaz. x'ten büyükler ancak sağdadır; sağa inilir. Döngü boş bir bağda biter ve son aday döndürülür. Hiç aday bulunmadıysa sonuç null'dır; bu, x'in ağaçtaki en büyük anahtar ya da ondan büyük olduğu anlamına gelir.

Kod iki durumu kendiliğinden kapsar. Sağ alt ağaç varsa döngü x'i geçtikten sonra bir kez sağa, sonra adaylar toplayarak hep sola iner; son aday sağ alt ağacın en küçüğüdür. Sağ alt ağaç yoksa x'ten sonra döngü hemen biter ve son aday, solundan geçilen en yakın atadır. Üstelik kod x'in ağaçta bulunmasını da gerektirmez.

Ardılın akrabası üç sorgu daha vardır. Tablo 10.6 dördünü bir arada verir. Java'nın TreeMap ve TreeSet sınıfları bu sorguları hazır metotlar olarak sunar.

Tablo 10.6: Sıralı komşu sorguları; son sütun 6(4(2, 5), 8(7, 9)) ağacı içindir.
İşlemAnlamıJava TreeMapÖrnek
ardılx'ten büyük en küçük anahtarhigherKey(x)ardil(5) = 6
öncülx'ten küçük en büyük anahtarlowerKey(x)oncul(6) = 5
tavan (ceiling)≥ x olan en küçük anahtarceilingKey(x)tavan(3) = 4
taban (floor)≤ x olan en büyük anahtarfloorKey(x)taban(3) = 2

Öncül, ardılın aynadaki görüntüsüdür: x > kok.veri ise düğüm bir adaydır ve sağa inilir, değilse sola inilir. Tavan ile ardıl arasındaki tek fark eşitliktir: x ağaçta varsa tavan x'in kendisidir, ardıl ise ondan sonraki anahtardır. Taban da öncülün eşitliği kabul eden sürümüdür. Örnek ağaçta 3 yoktur; bu yüzden tavan(3) ardıl gibi 4'ü, taban(3) öncül gibi 2'yi verir. Dört sorgunun hepsi kökten tek bir yol izler ve O(h) sürer.

Örnek 10.2

Aynı ağaçta taban(kok, 7) ve oncul(kok, 7) bulunsun.

  1. taban: 7 ≥ 6, 6 aday; sağa (8). 7 < 8, sola (7). 7 ≥ 7, 7 aday; sağa (null). Sonuç 7: anahtar ağaçta olduğu için taban kendisidir.
  2. oncul: 7 > 6, 6 aday; sağa (8). 7 > 8 değil, sola (7). 7 > 7 değil, sola (null). Sonuç 6: 7'den küçük en büyük anahtar.

İki sorgu aynı yolu izledi; tek fark 7'nin kendisinin aday sayılıp sayılmamasıdır.

10.3.6Kod izleme: ardil

ardil(kok, 5) ve ardil(kok, 6) çağrıları, ardılın iki durumunu sırasıyla örnekler. Tablo 10.7'de her satır döngünün bir turudur.

Tablo 10.7: ardil(kok, 5) ve ardil(kok, 6) izlemesi.
ÇağrıkokKarşılaştırmaadayYön
ardil(kok, 5)65 < 66sola
45 ≥ 46sağa
55 ≥ 56sağa → null; return 6
ardil(kok, 6)66 ≥ 6nullsağa
86 < 88sola
76 < 77sola → null; return 7

İlk çağrıda aday kökte bulundu ve bir daha değişmedi: 5'in sağ alt ağacı yoktur, ardıl solundan geçilen ata 6'dır. İkinci çağrıda aday önce 8, sonra 7 oldu: 6'nın sağ alt ağacı vardır ve ardıl onun en küçüğüdür. Her iki çağrıda da üç düğüme bakıldı. Şekil 10.13 izlemenin dört anını gösterir.

6aday4kok25879 6aday425879
64258aday7kok9 642587aday9
Şekil 10.13: ardil izlemesi. (a) 5 < 6: 6 aday, sola inilir. (b) Döngü bitti; ardil(5) = 6. (c) ardil(kok, 6): 6 < 8, 8 aday. (d) 7 daha iyi aday; döngü bitti, ardil(6) = 7.
Sık yapılan hata

Ardılı “sağ çocuk” sanmak. 6'nın sağ çocuğu 8'dir, ama ardılı 7'dir; ardıl, sağ alt ağacın en küçüğüdür ve sağ çocuktan sonra sola inmeyi gerektirir. Öteki yanlış ise sağ alt ağaç yokken ardılın olmadığını düşünmektir. 5'in sağ çocuğu yoktur, ama ardılı 6 vardır; yukarıda, bir atadadır.

10.3.7Silme: üç durum

Silme, BST işlemlerinin en dikkat isteyenidir. Önce silinecek düğüm aramayla bulunur; bu O(h) sürer. Ardından düğüm, çocuk sayısına göre ağaçtan çıkarılır. Amaç, BST özelliğini bozmadan düğümü koparmaktır. Örneklerde Şekil 10.14'teki ağaç kullanılır: kök 50; 40'ın çocukları 30 ve 45; 45'in sol çocuğu 42; 60'ın sağ çocuğu 70.

50 40 30 45 42 60 70
Şekil 10.14: Silme örneklerinin ağacı: 50(40(30, 45(42, —)), 60(—, 70)). 70 ve 30 yaprak, 45 ile 60 tek çocuklu, 40 ve 50 iki çocukludur.
  1. Yaprak. Silinecek düğümün çocuğu yoktur. Ebeveynin ona giden bağı null yapılır. Örnek: 70'in silinmesi; 60'ın sağ bağı null olur.
  2. Tek çocuk. Düğüm atlanır: ebeveyn, doğrudan düğümün tek çocuğuna bağlanır. Örnek: 45 silinirken 40'ın sağ bağı 42'ye yönelir; 60 silinirken 50'nin sağ bağı 70'e yönelir.
  3. İki çocuk. Düğüm doğrudan çıkarılamaz, çünkü ebeveyninin onun yerine bağlayabileceği tek bir bağ vardır, oysa düğümün iki çocuğu vardır. Bunun yerine düğümün değeri, kök ortada ardılının (sağ alt ağacın en küçüğü) ya da öncülünün (sol alt ağacın en büyüğü) değeriyle değiştirilir. Sonra ardıl ya da öncül kendi yerinden silinir. Örnek: 40'ın silinmesi.

Tek çocuk durumunun neden BST özelliğini koruduğunu görmek kolaydır. 45, 40'ın sağ alt ağacındadır; 45'in bütün torunları da öyledir ve 40'tan büyüktür. 42'yi 40'ın sağına bağlamak bu ilişkiyi değiştirmez. Aynı biçimde 42, 50'nin sol alt ağacında kalır ve 50'den küçüktür. Alt ağaç olduğu gibi bir düzey yukarı çekilmiş, sırası değişmemiştir.

50 40 30 45 42 60 70 50 40 30 45 42 60
50 40 30 45 42 60 70 50 40 30 42 60 70
50 40 30 45 42 60 70 50 40 30 45 42 70
Şekil 10.15: Yaprak ve tek çocuk silme; her satır orijinal ağaçtan başlar. (a)–(b) Yaprak 70 silinir; 60'ın sağ bağı null olur. (c)–(d) 45'in yalnız sol çocuğu 42 vardır; 42, 45'in yerine 40'ın sağ çocuğu olur. (e)–(f) 60'ın yalnız sağ çocuğu 70 vardır; 70, 60'ın yerine 50'nin sağ çocuğu olur.

İki çocuk durumunda neden ardıl seçilir? Silinen değerin yerine konacak değer, sol alt ağacın hepsinden büyük ve sağ alt ağacın hepsinden küçük olmalıdır. Kök ortada sırada silinen anahtarın hemen yanındaki iki anahtar bu koşulu sağlar: öncül ve ardıl. 40'ın ardılı 42'dir. 42, 40'ın sağ alt ağacındaki en küçük anahtardır; yani sağ alt ağaçtaki öteki anahtarlardan küçüktür. 40'tan büyük olduğu için de sol alt ağaçtaki her anahtardan büyüktür. 42, 40'ın yerine konduğunda BST özelliği korunur.

Geriye ardılı eski yerinden silmek kalır. Bu silme her zaman kolay durumlardan birine düşer. Ardıl, sağ alt ağaçta hep sola inilerek bulunur ve sol çocuğu olmayan ilk düğümde durulur. Sol çocuğu yoktur; ya yapraktır ya da yalnız sağ çocuğu vardır. Yani ardılın silinmesi Durum 1 ya da Durum 2'dir; özyineleme bir daha iki çocuklu bir düğüme rastlamaz.

50 40 30 45 42 60 70 50 40 30 45 42 60 70
50 42 30 45 42 60 70 50 42 30 45 60 70
50 30 45 42 60 70
Şekil 10.16: İki çocuklu 40'ın silinmesi. (a) 40'ın hem solu (30) hem sağı (45) var. (b) Kök ortada ardıl: 45'e geç, sonra hep sola → 42. (c) 42'nin değeri 40'ın düğümüne kopyalanır; eski 42 fazladan duruyor. (d) Eski 42 sağ alt ağaçtan silinir; yaprak olduğu için Durum 1. (e) Öncül de kullanılabilir: sol alt ağacın en büyüğü 30; iki seçim de geçerli bir BST verir.

10.3.8Silme kodu

Üç durum, eklemedeki “alt ağacın yeni kökünü döndür” deseniyle tek bir özyinelemeli metotta toplanır:

IkiliAgacDugum<E> sil(
        IkiliAgacDugum<E> kok, E veri) {
    if (kok == null) return null;   // yok
    int k = veri.compareTo(kok.veri);
    if (k < 0)
        kok.solCocuk = sil(kok.solCocuk, veri);
    else if (k > 0)
        kok.sagCocuk = sil(kok.sagCocuk, veri);
    else if (kok.solCocuk == null)
        return kok.sagCocuk;  // yaprak/sağ çocuk
    else if (kok.sagCocuk == null)
        return kok.solCocuk;  // yalnız sol çocuk
    else {                    // iki çocuk
        kok.veri = minDeger(kok.sagCocuk).veri;
        kok.sagCocuk = sil(kok.sagCocuk, kok.veri);
    }
    return kok;
}

Metodun ilk yarısı aramadır. Anahtar bulunamazsa (kok == null) null döner; boş alt ağaçtan silme yapılınca alt ağaç boş kalır ve ağaç değişmez. k < 0 ya da k > 0 ise silme ilgili alt ağaçta sürer ve sonuç çocuk bağına atanır.

k == 0 olduğunda silinecek düğüm bulunmuştur ve üç durum üç dalda ele alınır:

  • kok.solCocuk == null ise kok.sagCocuk döndürülür. Bu dal iki durumu birden kapsar. Düğüm yapraksa sağ çocuk da null'dır ve null döner; ebeveynin bağı boşalır. Düğümün yalnız sağ çocuğu varsa o çocuk döner; ebeveyn doğrudan ona bağlanır.
  • kok.sagCocuk == null ise (sol çocuk var, sağ yok) sol çocuk döndürülür.
  • İki çocuk varsa minDeger(kok.sagCocuk) ardılı bulur ve değeri düğüme kopyalanır. Sonra sil(kok.sagCocuk, kok.veri) ardılı sağ alt ağaçtan siler. Burada kok.veri artık ardılın değeridir.

Düğümün ağaçtan kopması, döndürülen değerin ebeveynin bağına atanmasıyla olur. Yaprak ya da tek çocuk durumunda metot silinen düğümü değil, onun yerine geçecek olanı döndürür. Ebeveyn bu değeri kendi bağına yazar ve silinen düğüme giden referans kaybolur. Java'nın çöp toplayıcısı (garbage collector) artık hiçbir referansın göstermediği düğümü bir süre sonra temizler. Kullanım her zaman kok = sil(kok, 40); biçimindedir; kök silinirse yeni kök ancak bu atamayla kok değişkenine yazılır.

Maliyeti bulalım. Silinecek düğüme kadar iniş O(h)'dir. İki çocuk durumunda minDeger aynı yoldan aşağı devam eder. İkinci sil çağrısı da ardıla kadar yeniden iner. Bu iki iniş silinen düğümün altında kalır ve toplamları O(h)'yi geçmez. Silme O(h) zamanda yapılır.

Sık yapılan hata

İki çocuklu düğümü silerken ardılın değerini kopyalayıp ardılı yerinden silmeyi unutmak. O zaman 42 ağaçta iki kez bulunur ve ağaç BST olmaktan çıkar. Öteki yanlış da ardılı silerken anahtar olarak eski değeri kullanmaktır: sil(kok.sagCocuk, veri) yazılırsa sağ alt ağaçta 40 aranır ve bulunamaz. Doğru çağrı, kopyalanmış yeni değerle yapılır: sil(kok.sagCocuk, kok.veri).

10.3.9Kod izleme: üç silme

Üç silme de Şekil 10.14'teki orijinal ağaçtan başlar. Tablo 10.8 her çerçevenin karşılaştırmasını ve dönüşte bağına atadığı değeri verir.

Tablo 10.8: sil(kok, 70), sil(kok, 45) ve sil(kok, 40) izlemesi.
Çağrıİniş (k)Bulunan düğümdeDeğişen bağSonuç ağacı
sil(kok, 70)50 (1), 60 (1), 70 (0)sol çocuk yok → return kok.sagCocuk = null60'ın sağ bağı null50(40(30, 45(42, —)), 60)
sil(kok, 45)50 (−1), 40 (1), 45 (0)sol var, sağ yok → return kok.solCocuk = 4240'ın sağ bağı 4250(40(30, 42), 60(—, 70))
sil(kok, 40)50 (−1), 40 (0)iki çocuk: ardıl 42; kok.veri = 42; sil(45'li alt ağaç, 42): 45 (−1), 42 (0), yaprak → null45'in sol bağı null; 40'ın düğümünde artık 4250(42(30, 45), 60(—, 70))

Üç sonucun kök ortada gezinmeleri sırasıyla 30 40 42 45 50 60, 30 40 42 50 60 70 ve 30 42 45 50 60 70'tir. Her birinde silinen anahtar kaybolmuş, kalanların sırası korunmuştur. Üç izlemede kök hiç değişmedi; main'deki kok = sil(…) ataması her seferinde yine 50'yi yazdı.

Üçüncü izleme ayrıntılı incelenmeye değer, çünkü iki ayrı silme iç içe çalışır. Tablo 10.9 bu çağrıyı adım adım verir.

Tablo 10.9: kok = sil(kok, 40) izlemesinin ayrıntısı.
AdımÇerçeveOlay
1sil(50, 40)k = −1 (40 < 50): kok.solCocuk = sil(kok.solCocuk, 40)
2sil(40, 40)k = 0; iki çocuk var (30 ve 45)
3sil(40, 40)minDeger(45): 45'ten sola 42'ye; 42'nin solu yok → ardıl 42
4sil(40, 40)kok.veri = 42: düğümde artık 42; ağaçta iki tane 42 var
5sil(45, 42)k = −1 (42 < 45): kok.solCocuk = sil(kok.solCocuk, 42)
6sil(42, 42)k = 0; sol çocuk yok → return kok.sagCocuk = null
7sil(45, 42)kok.solCocuk = null: eski 42 koptu; return 45
8sil(42, 40)kok.sagCocuk = düğüm(45) (bağ aynı); return düğüm (değeri 42)
9sil(50, 40)kok.solCocuk = aynı düğüm; return 50; main: kok = 50

Dikkat edilirse 40'ı tutan düğüm nesnesi ağaçtan hiç çıkmadı; yalnız içindeki değer 42 oldu. Ağaçtan kopan nesne, eski 42'yi tutan yapraktır. Şekil 10.17 bu iki anı ve sonucu gösterir.

5040kok304542ardıl6070 5042kok3045426070
50423045kok607042 504230456070
Şekil 10.17: sil(kok, 40) izlemesi. (a) minDeger(kok.sagCocuk): ardıl 42. (b) kok.veri = 42: 42 iki kez var. (c) Eski 42'nin bağı null oldu. (d) Sonuç: 50(42(30, 45), 60(—, 70)); düğüm sayısı bir azaldı, BST özelliği korundu.
Örnek 10.3

Aynı orijinal ağaçtan bu kez kök 50 silinsin.

  1. sil(50, 50): k = 0; iki çocuk var (40 ve 60).
  2. minDeger(60): 60'ın sol çocuğu yok; ardıl 60'ın kendisidir.
  3. kok.veri = 60; sonra sil(60'lı alt ağaç, 60): k = 0, sol çocuk yok → return kok.sagCocuk = 70. Bu bir tek çocuk silmesidir.
  4. Kök düğümün sağ bağı 70'e yönelir. Sonuç: 60(40(30, 45(42, —)), 70). Kök ortada sıra: 30 40 42 45 60 70.

Ardıl, silinen düğümün doğrudan sağ çocuğu çıktı ve bir sağ çocuğu vardı. Bu durum, ardılın her zaman yaprak olmadığını gösterir; ama sol çocuğu hiçbir zaman yoktur.

10.3.10BST işlemlerinin analizi

Buraya kadar görülen işlemlerin hepsi aynı sonuca vardı: arama, ekleme ve silmenin maliyeti, işlemin eriştiği en derin düğümün derinliğiyle orantılıdır. En kötü durumda bu derinlik ağacın yüksekliğidir; maliyet O(h)'dir. Öyleyse asıl soru şudur: n anahtarlı bir BST'nin yüksekliği ne kadardır?

Cevap, anahtarların ekleme sırasına bağlıdır. Aynı yedi anahtar 4, 2, 6, 1, 3, 5, 7 sırasıyla eklenirse yüksekliği 2 olan mükemmel bir ağaç oluşur. 1, 2, 3, 4, 5, 6, 7 sırasıyla eklenirse her yeni anahtar bir öncekinin sağına düşer ve ağaç sağa uzanan bir zincire dönüşür; yükseklik 6 olur. Böyle bir ağaca devrik (degenerate) ağaç denir. Devrik ağaç, fazladan bellek harcayan bir bağlı listeden farksızdır.

4 2 1 3 6 5 7dengeli: h = 2 ≈ log₂ n 1 2 3 4 5devrik (zincir): h = n − 1
Şekil 10.18: Solda 7 düğümlü dengeli ağaç, yükseklik 2 (yaklaşık log2 n). Sağda 1, 2, 3, 4, 5 sırayla eklenince oluşan zincir; yükseklik n − 1 = 4.
Tablo 10.10: BST'nin yüksekliği ve işlem maliyeti; son sütun n = 106 için yaklaşık adım sayısıdır.
DurumYükseklik hArama / ekleme / silmen = 106
Dengeli (en iyi)≈ log2 nO(log n)≈ 20
Rastgele sıra (ortalama)≈ 1,39 log2 nO(log n)≈ 28
Devrik (en kötü, sıralı girdi)n − 1O(n)106

Tablonun orta satırı umut vericidir. Anahtarlar rastgele bir sırayla eklenirse, bir düğümün ortalama derinliği yaklaşık 2 ln n ≈ 1,39 log2 n'dir. Bir milyon anahtarda bu, ortalama 28 civarında karşılaştırma demektir; en iyi durumdan yalnız %40 kadar fazladır. Bu sonuç, hızlı sıralamanın ortalama karşılaştırma sayısıyla aynı hesaptan çıkar: her anahtar kök olarak seçildiğinde kalan anahtarları rastgele iki gruba ayırır.

Sorun, gerçek verinin çoğu zaman rastgele olmamasıdır. Bir dosyadan okunan kayıtlar zaten sıralı olabilir; öğrenci numaraları kayıt sırasıyla artar; zaman damgaları hep büyür. Böyle bir girdi düz BST'yi en kötü duruma iter ve her işlem O(n) olur. n anahtarı sıralı biçimde eklemek de toplam 1 + 2 + … + (n − 1) = n(n − 1)/2, yani O(n2) karşılaştırma ister. Bir milyon anahtar için bu yarım trilyon karşılaştırmadır.

Sık yapılan hata

“BST'de arama O(log n)'dir” demek. Bu yalnız ağaç dengeliyse doğrudur. Düz BST yükseklik için hiçbir garanti vermez; doğru ifade “O(h)”dir ve en kötü durumda h = n − 1'dir. O(log n) garantisi ancak AVL ve kırmızı-siyah gibi dengeli ağaçlarda vardır.

10.3.11Aralık sorgusu

Aralık sorgusu, [lo, hi] aralığına düşen bütün anahtarları sıralı olarak verir. “5 ile 10 arasındaki anahtarlar” ya da “not ortalaması 2,5 ile 3,0 arasındaki öğrenciler” bu tür sorgulardır. Basit bir çözüm, bütün ağacı kök ortada gezip aralıktakileri yazmaktır; ama bu, aralıkta kaç anahtar olursa olsun O(n) sürer. BST özelliği çok daha iyisini yapmaya izin verir: aralığa giremeyecek alt ağaçlara hiç inmemek. Buna budama (pruning) denir.

void aralik(AgacDugumu k, int lo, int hi) {
    if (k == null) return;
    if (lo < k.veri)           // solda aday var
        aralik(k.sol, lo, hi);
    if (lo <= k.veri && k.veri <= hi)
        System.out.print(k.veri + " ");
    if (k.veri < hi)           // sağda aday var
        aralik(k.sag, lo, hi);
}

Kod, kök ortada gezinmenin iki koşulla budanmış hâlidir. Sol alt ağaçtaki anahtarların hepsi k.veri'den küçüktür. lo < k.veri değilse, yani k.veri ≤ lo ise, sol alt ağaçtaki her anahtar lo'dan küçüktür ve hiçbiri aralığa giremez; sol alt ağaç atlanır. Aynı biçimde k.veri ≥ hi ise sağ alt ağaçtaki her anahtar hi'den büyüktür; sağ alt ağaç atlanır. Ortadaki if düğümün kendisini aralıktaysa yazar. Sıra sol–düğüm–sağ olduğu için çıktı sıralıdır.

Tablo 10.11, 8(4(2, 6(5, 7)), 12(10, 14)) ağacında aralik(kok, 5, 10) çağrısını izler.

Tablo 10.11: aralik(kok, 5, 10) izlemesi.
DüğümSol koşul 5 < k.veriAralıkta mı?Sağ koşul k.veri < 10Çıktı
8doğru → sola inkendisi ve sağı, sol alt ağaç bitince ele alınır
4yanlış → 2 budanırhayırdoğru → sağa in
6doğru → sola inkendisi ve sağı, sol alt ağaç bitince ele alınır
5yanlış → solu atlanırevetdoğru → sağ null5
6sola dönüldü; 6 yazılır; aralik(7)5 6
7doğru → sol nullevetdoğru → sağ null5 6 7
8sola dönüldü; 8 yazılır; aralik(12)5 6 7 8
12doğru → sola inhayıryanlış → 14 budanır
10doğru → sol nullevetyanlış → sağ budanır5 6 7 8 10

Çıktı 5 6 7 8 10'dur ve sıralıdır. Ziyaret edilen düğümler 8, 4, 6, 5, 7, 12 ve 10'dur. 2 ve 14 hiç ziyaret edilmedi: 2, 4'ün solundaydı ve 4 zaten alt sınırdan küçüktü; 14, 12'nin sağındaydı ve 12 zaten üst sınırdan büyüktü. Şekil 10.19 izlemenin başını ve sonunu gösterir.

842657121014lo = 5, hi = 10 842657121014lo = 5, hi = 10
842657121014lo = 5, hi = 10 842657121014lo = 5, hi = 10
Şekil 10.19: aralik(kok, 5, 10) izlemesi. (a) 5 < 8: solda aralığa giren anahtar olabilir. (b) 5 < 4 yanlış: 4'ün sol alt ağacı budanır. (c) 12 aralık dışında ve 12 ≥ 10: sağ alt ağaç budanır. (d) Sonuç 5 6 7 8 10; 2 ve 14 hiç ziyaret edilmedi (soluk).

Maliyeti bulmak için ziyaret edilen düğümleri iki gruba ayıralım. Birinci grup aralıktaki düğümlerdir; s tane yazılır. İkinci grup aralığın dışında kalıp yine de ziyaret edilen düğümlerdir. Bunlar, aralığın sol ve sağ sınırına doğru inen iki yolun üzerindedir ve her yol en çok h + 1 düğüm içerir. Toplam maliyet O(h + s) olur. Dengeli bir ağaçta bir milyon anahtar arasından 10 anahtarlık bir aralık, yalnızca birkaç düzine düğüm ziyaret edilerek bulunur.

10.3.12Bir ağaç BST mi?

Bir ikili ağacın BST olup olmadığını denetlemek, BST özelliğinin “bütün alt ağaç için” geçerli olduğunu iyi anlamayı gerektirir. Her düğüm yalnız ebeveyniyle değil, bütün atalarıyla da uyumlu olmalıdır. Sol alt ağaca inerken bir üst sınır, sağ alt ağaca inerken bir alt sınır kazanılır. Bir düğüm, kökten ona inen yoldaki bütün bu sınırların içinde kalmalıdır.

boolean bstMi(AgacDugumu k, long min, long max) {
    if (k == null) return true;
    if (k.veri <= min || k.veri >= max)
        return false;          // aralık dışında
    return bstMi(k.sol, min, k.veri)
        && bstMi(k.sag, k.veri, max);
}
// ilk çağrı: bstMi(kok, Long.MIN_VALUE,
//                  Long.MAX_VALUE)

Metot her düğüme bir izinli aralık (min, max) taşır. Kök için aralık sınırsızdır; Long.MIN_VALUE ve Long.MAX_VALUE değerleri eksi ve artı sonsuzun yerine kullanılır. Anahtarlar int olduğu için long türündeki bu uç değerler hiçbir anahtara eşit olamaz. Düğüm aralığın dışındaysa false döner. İçindeyse sol çocuk için üst sınır düğümün kendi değeri olur, sağ çocuk için alt sınır. İki alt ağacın sonuçları && ile birleşir.

Tablo 10.12, sağdaki hatalı ağacı, yani 6(2(1, 4(3, 7)), 8) ağacını denetler.

Tablo 10.12: bstMi(kok, Long.MIN_VALUE, Long.MAX_VALUE) izlemesi.
Çağrıİzinli aralıkDenetimSonuç
bstMi(6)(−∞, ∞)6 aralıktasol: bstMi(2, −∞, 6)
bstMi(2)(−∞, 6)2 aralıktasol: bstMi(1, −∞, 2)
bstMi(1)(−∞, 2)1 aralıkta; çocukları nulltrue
bstMi(4)(2, 6)4 aralıktasol: bstMi(3, 2, 4)
bstMi(3)(2, 4)3 aralıkta; çocukları nulltrue
bstMi(7)(4, 6)7 ≥ 6false
bstMi(4) = true && false; bstMi(2) = true && falsefalse
bstMi(6): sol false; && kısa devre yapar, 8 hiç denetlenmezfalse

7 için izinli aralık (4, 6)'dır. Alt sınır 4, 7'nin 4'ün sağında olmasından gelir. Üst sınır 6 ise 7'nin 6'nın sol alt ağacında olmasından gelir; bu sınır kökten iki düzey aşağı taşınmıştır. Yalnız ebeveyn–çocuk çiftlerine bakan bir denetim 4 < 7 olduğu için bu ağaca yanlışlıkla “BST” derdi. Doğru denetim her düğümü bir kez ziyaret eder; süre O(n), çağrı yığını O(h)'dir.

6(−∞, ∞)214378 6214(2, 6)378✓
621437(4, 6)8✓✓✗ 6214378✓✓✗✗✗✗
Şekil 10.20: bstMi izlemesi. (a) Kökte aralık (−∞, ∞). (b) 4 için aralık (2, 6). (c) 7 için aralık (4, 6); 7 ≥ 6 olduğu için false. (d) Sonuç: ağaç BST değil.
Sık yapılan hata

Sınırları int türünde Integer.MIN_VALUE ve Integer.MAX_VALUE ile başlatmak. Ağaçta tam bu değerlerden biri varsa, katı eşitsizlik yüzünden geçerli bir ağaç “BST değil” diye reddedilir. Sunumdaki kod bunu long sınırlarla önler. Öbür yol, sınırları Integer nesnesi olarak taşıyıp “sınır yok” durumunu null ile göstermektir.

10.3.13Uygulama: öğrenci kayıtları

Gerçek uygulamalarda düğüm yalnız bir sayı değil, bütün bir kayıt taşır. Sunumdaki örnekte anahtar öğrenci numarasıdır ve düğüm öğrenci nesnesinin tamamını tutar:

public class Ogrenci {
    int ogrenciNo;     // anahtar
    String adSoyad;

    public Ogrenci(int ogrenciNo,
                   String adSoyad) {
        this.ogrenciNo = ogrenciNo;
        this.adSoyad = adSoyad;
    }
}
public class OgrenciDugum {
    Ogrenci ogr;           // veri
    OgrenciDugum solCocuk;
    OgrenciDugum sagCocuk;

    public OgrenciDugum(Ogrenci ogr) {
        this.ogr = ogr;
        // çocuklar başlangıçta null
    }
}

Ogrenci sınıfı veriyi, OgrenciDugum ise ağaç yapısını taşır. Bu ayrım, aynı öğrenci nesnesinin başka bir yapıda da (örneğin bir listede) kullanılabilmesini sağlar. Ekleme ve arama, tamsayı anahtarlı sürümlerle aynı mantıkla çalışır:

OgrenciDugum ekle(OgrenciDugum kok, Ogrenci ogr) {
    if (kok == null) return new OgrenciDugum(ogr);
    if (ogr.ogrenciNo < kok.ogr.ogrenciNo)
        kok.solCocuk = ekle(kok.solCocuk, ogr);   // soldan devam
    else if (ogr.ogrenciNo > kok.ogr.ogrenciNo)
        kok.sagCocuk = ekle(kok.sagCocuk, ogr);   // sağdan devam
    return kok;                                   // aynı numara eklenmez
}
Ogrenci ara(OgrenciDugum kok, int numara) {
    if (kok == null) return null;                // kayıt yok
    if (numara == kok.ogr.ogrenciNo) return kok.ogr; // bulundu
    if (numara < kok.ogr.ogrenciNo)
        return ara(kok.solCocuk, numara);         // soldan devam
    return ara(kok.sagCocuk, numara);             // sağdan devam
}

İki fark dikkat çeker. Birincisi, anahtar int olduğu için karşılaştırmalar <, > ve == ile doğrudan yapılır; compareTo gerekmez. İkincisi, ara boolean yerine öğrenci nesnesini döndürür. Bulunursa ad-soyad gibi bilgilere de erişilir; bulunamazsa null döner.

Örnek 10.4

Boş ağaca sırasıyla (2205, Ayşe Kaya), (1980, Mehmet Demir), (2410, Zeynep Ak) ve (2205, Tekrar) eklensin.

  1. 2205 kök olur.
  2. 1980 < 2205: kökün soluna eklenir.
  3. 2410 > 2205: kökün sağına eklenir.
  4. 2205 kökte zaten vardır; iki koşul da yanlıştır ve kayıt eklenmez. Ağaçta “Ayşe Kaya” kalır.

ara(kok, 2410).adSoyad “Zeynep Ak”, ara(kok, 2205).adSoyad “Ayşe Kaya” verir. ara(kok, 1000) ise 2205'ten sola, 1980'den sola iner, boş bağa düşer ve null döndürür.

Aynı işi Java'da hazır bir sınıf görür: TreeMap<Integer, Ogrenci>. TreeMap anahtardan değere bir eşleme tutar ve içeride dengeli bir arama ağacı, kırmızı-siyah ağaç kullanır. put, get ve remove O(log n) sürer; higherKey, floorKey ve subMap gibi metotlar da ardıl, taban ve aralık sorgularını yapar. Elle yazılan BST'den farkı, sıralı girdiyle bile zincire dönüşmemesidir.

10.4Denge ve AVL ağaçları

BST işlemlerinin hepsi O(h) sürer ve h, en kötü durumda n − 1'e çıkar. Bu kesimin sorusu, yüksekliği her koşulda O(log n)'de tutmanın ucuz bir yolunun olup olmadığıdır. Cevap evettir ve ilk yolu 1962'de Adelson-Velsky ile Landis gösterdi.

10.4.1Denge ihtiyacı

Düz BST'nin şekli ekleme sırasına bağlıdır. 1, 2, 3, 4 sırayla eklenirse ağaç sağa uzanan bir zincir olur ve yüksekliği 3'tür. Oysa aynı dört anahtar, yüksekliği 2 olan ağaçlarda da durabilir. Şekil 10.21 iki seçeneği gösterir: kökü 2 olan 2(1, 3(—, 4)) ve kökü 3 olan 3(1(—, 2), 4). Üç ağacın da kök ortada gezinmesi 1 2 3 4'tür. Yani üçü de aynı sıralı kümeyi tutar; yalnız biçimleri farklıdır.

1 2 3 4 2 1 3 4 3 1 2 4h = 3h = 2h = 2
Şekil 10.21: 1, 2, 3, 4 zinciri (yükseklik 3), aynı anahtarlarla yüksekliği 2 olan iki farklı ağaca dönüştürülebilir: kökü 2 olan ve kökü 3 olan.
Tanım 10.4

Kendini dengeleyen arama ağacı (self-balancing search tree), yüksekliğin O(log n) kalmasını garanti eden BST'dir. Bunun için her ekleme ve silmeden sonra yapısını küçük yerel değişikliklerle düzeltir. Yerel değişiklikler döndürme ve renk değiştirmedir. Kısaca dengeli ağaç da denir.

Tanımdaki “yerel” sözcüğü önemlidir. Bozulmuş bir ağacı tamamen yıkıp kök ortada sırasından dengeli biçimde yeniden kurmak mümkündür; ama bu her güncellemede O(n) sürer ve ağacın bütün avantajını yok eder. Dengeli ağaçlar yalnız ekleme ya da silmenin geçtiği yol üzerindeki birkaç düğüme dokunur. Bu yol O(log n) uzunluktadır ve her düğümde yapılan iş sabittir; güncellemenin toplam maliyeti yine O(log n) kalır.

10.4.2Dengeleme örneği

Daha büyük bir örnek, yerel düzeltmenin nasıl işlediğini gösterir. Şekil 10.22'nin solundaki ağaçta 11 anahtar vardır. 9 → 14 → 12 ve 76 → 54 → 72 → 67 kolları uzamıştır ve yükseklik 4'tür. Bu iki kolun alt ağaçları yeniden düzenlenince aynı 11 anahtar yüksekliği 3 olan bir ağaca sığar. Yeni alt ağaç kökleri 12 ve 72'dir.

50 17 9 14 12 23 19 76 54 72 67 50 17 12 9 14 23 19 72 54 67 76yükseklik 4yükseklik 3 · tüm |bf| ≤ 1
Şekil 10.22: Solda yüksekliği 4 olan dengesiz ağaç; sağda aynı anahtarlarla yüksekliği 3 olan, her düğümde |bf| ≤ 1 sağlayan ağaç. Turuncu düğümler (12 ve 72) yeni alt ağaç kökleridir.

Yeniden düzenleme sırasında kök ortada sıra değişmez: iki ağaç da 9 12 14 17 19 23 50 54 67 72 76 verir. Değişen yalnız hangi anahtarın hangisinin ebeveyni olduğudur. 17'nin sol alt ağacında 9, 14 ve 12 vardı; bunların ortancası 12 alt ağacın kökü yapıldı, 9 ve 14 onun çocukları oldu. Sağda 76'nın sol alt ağacındaki 54, 72, 67 ve 76'nın kendisi yeniden dizildi; ortadaki 72 köke çıktı. Kökün öbür tarafına, 17'nin sağ alt ağacına ya da 50'ye dokunulmadı.

AVL ve kırmızı-siyah ağaçlar bu tür düzeltmeleri kendiliğinden yapar. Bir ekleme ya da silme yalnız kendi geçtiği yolu bozabilir; düzeltme de yalnız o yol üzerinde, aşağıdan yukarı doğru yapılır.

10.4.3AVL ağacı ve denge faktörü

AVL ağacı, adını Sovyet matematikçiler Georgy Adelson-Velsky ve Evgenii Landis'in soyadlarının baş harflerinden alır. 1962'de yayımlanan AVL, kendini dengeleyen ilk ikili arama ağacıdır. Fikri sadedir: her düğümün iki alt ağacının yükseklikleri arasındaki fark en çok 1 olsun.

Tanım 10.5

Bir x düğümünün denge faktörü (balance factor) bf(x) = h(sol) − h(sağ)'dır. Burada h(sol) ve h(sağ), x'in sol ve sağ alt ağaçlarının yükseklikleridir. Boş alt ağacın yüksekliği −1'dir. AVL ağacı, her düğümünde bf ∈ {−1, 0, +1} olan ikili arama ağacıdır.

Pozitif denge faktörü düğümün “sol ağır” olduğunu, negatif değer “sağ ağır” olduğunu söyler. Yaprakta iki alt ağaç da boştur: bf = −1 − (−1) = 0. Yalnız sol çocuğu olan bir düğümde bf = 0 − (−1) = 1'dir. Bu yüzden boş alt ağacın yüksekliğinin −1 alınması önemlidir; 0 alınsaydı yaprakla tek çocuklu düğüm aynı görünürdü.

300 200 100 250 40−1 500 302 202 101 50 400AVL ✓AVL değil: 20 ve 30'da |bf| = 2
Şekil 10.23: Düğümlerin sağ üstündeki sayı denge faktörüdür. Solda geçerli bir AVL ağacı: 30(20(10, 25), 40(—, 50)); 40'ın denge faktörü −1, ötekilerin 0. Sağda 5, 10, 20 zinciri yüzünden 20 ve 30'un denge faktörü 2 olmuş; ağaç AVL değil.

Sağdaki ağaçta iki düğüm dengesizdir. Böyle durumlarda en alttaki dengesiz düğüm, yani yeni eklenen düğüme en yakın olan, düzeltilir. Bu örnekte o düğüm 20'dir. 20 düzeltildiğinde alt ağacının yüksekliği bir azalır ve 30 da kendiliğinden dengeye gelir.

AVL ağacında her düğüm kendi yüksekliğini bir alanda saklar. Denge faktörü gerektiğinde iki çocuğun yüksekliğinden hesaplanır. Ekleme ve silme önce düz BST gibi yapılır. Sonra özyineleme geri dönerken yol üzerindeki her düğümün yüksekliği güncellenir ve denge faktörü denetlenir. |bf| = 2 olan bir düğüme rastlanırsa döndürmeyle düzeltilir.

Sık yapılan hata

Denge faktörünü alt ağaçlardaki düğüm sayısı farkıyla hesaplamak. AVL koşulu yükseklikle ilgilidir. 4(2(1, 3), 5) ağacında kökün solunda üç, sağında bir düğüm vardır; ama sol yükseklik 1, sağ yükseklik 0'dır ve bf = 1'dir. Ağaç geçerli bir AVL ağacıdır.

10.4.4AVL yüksekliği neden O(log n)?

AVL koşulu yerel bir koşuldur; her düğüm yalnız iki çocuğunun yüksekliğine bakar. Bu yerel koşulun bütün ağacın yüksekliğini sınırladığını göstermek için soruyu tersinden soralım: yüksekliği h olan bir AVL ağacında en az kaç düğüm vardır? Bu sayıya N(h) diyelim. En az düğümlü, yani “en seyrek” AVL ağacında kökün bir alt ağacının yüksekliği h − 1 olmak zorundadır (yoksa ağacın yüksekliği h olmazdı). Öbür alt ağaç, AVL koşulunun izin verdiği en kısa yükseklikte, h − 2'de olur. İki alt ağaç da kendi yüksekliklerinde en seyrek ağaçlardır. Buradan bir yineleme bağıntısı çıkar:

N(0) = 1,   N(1) = 2,   N(h) = 1 + N(h − 1) + N(h − 2)

Tablo 10.13: Yüksekliği h olan AVL ağacındaki en az düğüm sayısı.
hHesapN(h)
0tek düğüm1
1kök + tek çocuk2
21 + N(1) + N(0) = 1 + 2 + 14
31 + N(2) + N(1) = 1 + 4 + 27
41 + N(3) + N(2) = 1 + 7 + 412
51 + N(4) + N(3) = 1 + 12 + 720
61 + N(5) + N(4) = 1 + 20 + 1233
815131211040716011110190120N(0) = 1N(1) = 2N(2) = 4N(3) = 7N(4) = 12
Şekil 10.24: Yüksekliği 4 olan en seyrek AVL ağacı: 12 düğüm. Kök (turuncu), yüksekliği 3 olan en seyrek ağacı (7 düğüm) sol, yüksekliği 2 olan en seyrek ağacı (4 düğüm) sağ alt ağaç olarak taşır. Her düğümde iki alt ağacın yükseklik farkı tam 1'dir.

Sayılar Fibonacci sayıları gibi büyür; gerçekten de N(h) = F(h + 3) − 1'dir (F(1) = F(2) = 1). Fibonacci sayıları üstel büyüdüğü için N(h) de üstel büyür. Kesin bir alt sınır şöyle bulunur. N(h − 1) > N(h − 2) olduğundan N(h) = 1 + N(h − 1) + N(h − 2) > 2 · N(h − 2)'dir. Her iki düzeyde düğüm sayısı en az iki katına çıkar. Bunu h/2 kez uygulayınca N(h) > 2h/2 bulunur.

Teorem 10.2 (AVL yüksekliği)

n düğümlü bir AVL ağacının yüksekliği h < 2 log2 n'dir. Fibonacci sayılarıyla yapılan daha sıkı hesap h < 1,44 log2(n + 2) verir. Bu yüzden AVL ağacında arama, ekleme ve silme O(log n) sürer.

Yüksekliği h olan ağaçta en az N(h) düğüm vardır: n ≥ N(h) > 2h/2. İki tarafın 2 tabanında logaritması alınınca log2 n > h/2, yani h < 2 log2 n çıkar. □

Örnek 10.5

30 düğümlü bir AVL ağacının yüksekliği en çok kaçtır? Yüksekliği 6 olan bir AVL ağacı en az N(6) = 33 düğüm ister; 30 düğüm buna yetmez. Yüksekliği 5 olan bir ağaç ise en az N(5) = 20 düğümle kurulabilir. Cevap 5'tir. Karşılaştırma için 30 düğümlü bir düz BST'nin yüksekliği 29'a kadar çıkabilir; mükemmel dengeli bir ağaçta ise en az 4'tür (31 düğümlü mükemmel ağacın yüksekliği 4'tür).

Sonuç şunu söyler: AVL ağacı mükemmel dengeli bir ağaçtan en kötü durumda yalnız %44 kadar daha yüksektir. Bir milyon anahtarlı AVL ağacının yüksekliği 28'i geçmez. Düz BST'deki milyonluk zincirle karşılaştırılınca fark çok büyüktür.

10.4.5Döndürme

Dengesizliği düzeltmenin temel aracı döndürmedir (rotation). Döndürme, bir düğüm ile çocuklarından birinin ebeveyn–çocuk ilişkisini tersine çevirir. Çocuk bir düzey yukarı çıkar, eski ebeveyn onun altına iner. Bu sırada BST özelliğinin bozulmaması için bir alt ağacın sahibi değişir.

T3T1T2 y xsağa döndür(y)T1T2T3 x y kök ortada sıra her iki tarafta aynı: T1 · x · T2 · y · T3
T1T2T3 x ysola döndür(x)T3T1T2 y x kök ortada sıra her iki tarafta aynı: T1 · x · T2 · y · T3
Şekil 10.25: Üstte sağa döndürme: sol ağır y'de x yukarı çıkar, y onun sağ çocuğu olur; x'in sağ alt ağacı T2, y'nin soluna geçer. Altta sola döndürme bunun simetriğidir. Kalın çizgiler değişen bağlardır. Kök ortada sıra iki tarafta da T1 · x · T2 · y · T3'tür.

Sağa döndürmeyi ayrıntılı inceleyelim. Başlangıçta y ebeveyndir ve x onun sol çocuğudur. x'in iki alt ağacı T1 ve T2, y'nin sağ alt ağacı T3'tür. BST özelliğine göre kök ortada sıra şudur: T1'deki anahtarlar, x, T2'deki anahtarlar, y, T3'teki anahtarlar. Döndürmeden sonra x yukarıdadır ve y onun sağ çocuğudur. T1 x'in solunda kalır, T3 y'nin sağında kalır. T2'nin yeri değişir: x'ten büyük ve y'den küçük olduğu için y'nin sol alt ağacı olur. Kök ortada sıra yine T1 · x · T2 · y · T3'tür.

Teorem 10.3 (Döndürme)

Bir döndürme, ağacın kök ortada sırasını değiştirmez; bu yüzden BST özelliğini korur. Döndürmede yalnız üç bağ değişir ve işlem O(1) sürer. Döndürülen alt ağaçta bir taraf bir düzey kısalır, öbür taraf bir düzey uzar.

Üç bağ değişir. y'nin sol bağı artık T2'yi, x'in sağ bağı artık y'yi gösterir. Ebeveynden gelen bağ da y yerine x'i gösterir. Ağacın büyüklüğü ne olursa olsun bu üç atama yeter; döndürme sabit sürelidir. Yükseklik açısından bakıldığında T1 bir düzey yukarı çıkar, T3 bir düzey aşağı iner, T2 aynı düzeyde kalır. Sol ağır bir düğümde ağır taraf T1 ise sağa döndürme ağırlığı dengeler.

Sık yapılan hata

Döndürmede T2'yi unutmak. x.sag = y ataması önce yapılırsa x'in eski sağ alt ağacına giden tek referans kaybolur ve T2 ağaçtan düşer. Atama sırası önemlidir: önce y.sol = x.sag ile T2 güvenli yere taşınır, sonra x.sag = y yazılır.

10.4.6Tek döndürmeler: LL ve RR

Bir eklemeden sonra en alttaki dengesiz düğüm z olsun. Dengesizliğin dört biçimi vardır. Adlar, yeni düğüme z'den inen yolun ilk iki adımını söyler: L sol (left), R sağ (right) demektir. LL'de yeni düğüm z'nin sol çocuğunun sol alt ağacına eklenmiştir. RR bunun simetriğidir. Bu iki durumda z ile çocuğu aynı yöne eğiktir ve tek döndürme yeter.

302201100 200100300
10−220−1300 200100300
Şekil 10.26: Tek döndürmeler; düğümlerin sağ üstündeki sayı denge faktörüdür. (a) LL: 10, sol çocuğun (20) soluna eklendi; bf(30) = 2, bf(20) = 1. (b) 30'da sağa döndürme: 20 yukarı çıkar, 30 onun sağ çocuğu olur; bütün denge faktörleri 0. (c) RR: 30, sağ çocuğun (20) sağına eklendi; bf(10) = −2, bf(20) = −1. (d) 10'da sola döndürme.

LL durumu denge faktörlerinden tanınır. bf(z) = 2'dir, yani z sol ağırdır. Sol çocuğun denge faktörü de 1 ya da 0'dır; çocuk sola eğik ya da dengelidir. Çözüm z'de sağa döndürmedir. RR'de bf(z) = −2 ve sağ çocuğun denge faktörü −1 ya da 0'dır; çözüm z'de sola döndürmedir.

Ekleme sonrasında tek döndürmenin güzel bir sonucu vardır: döndürülen alt ağacın yüksekliği, eklemeden önceki değerine geri döner. Şekil 10.26'da 30'un alt ağacının yüksekliği 10 eklenmeden önce 1'di (30 ve 20); ekleme 2'ye çıkardı; döndürme yeniden 1'e indirdi. Daha yukarıdaki atalar yüksekliğin değiştiğini hiç görmez. Bu yüzden eklemede tek bir düzeltme yeterlidir.

10.4.7Çift döndürmeler: LR ve RL

Kalan iki durumda yeni düğüm “zikzak” bir yolun sonundadır. LR'de yeni düğüm z'nin sol çocuğunun sağ alt ağacına eklenmiştir. z sola, çocuğu sağa eğiktir; üç düğüm bir üçgen çizer. Bu durumda z'de tek sağa döndürme işe yaramaz: üçgeni ters yöne çevirir ve ağaç yine dengesiz kalır.

Çözüm iki adımlıdır. Önce çocukta sola döndürülür; üçgen düz bir çizgiye, yani LL durumuna dönüşür. Sonra z'de sağa döndürülür. Sonuçta üç anahtarın ortancası köke çıkar, küçüğü sola, büyüğü sağa yerleşir. RL bunun simetriğidir: önce çocukta sağa, sonra z'de sola döndürülür.

30210−1200 302201100 200100300
10−2301200 10−220−1300 200100300
Şekil 10.27: Çift döndürmeler. Üstte LR: (a) 20, sol çocuğun (10) sağına eklendi; bf(30) = 2, bf(10) = −1: üçgen. (b) 10'da sola döndürme: üçgen düz çizgiye (LL) döndü, bf(30) hâlâ 2. (c) 30'da sağa döndürme: ortanca 20 kök oldu. Altta RL: (d) 20, sağ çocuğun (30) soluna eklendi; bf(10) = −2, bf(30) = 1. (e) 30'da sağa döndürme: düz çizgi (RR). (f) 10'da sola döndürme: 20 kök.
Tablo 10.14: AVL'de dört dengesizlik durumu (z: en alttaki dengesiz düğüm).
Durumbf(z)Çocuğun bf'siDüzeltme
LL+2sol çocuk ≥ 0z'de sağa döndür
RR−2sağ çocuk ≤ 0z'de sola döndür
LR+2sol çocuk < 0sol çocukta sola, sonra z'de sağa
RL−2sağ çocuk > 0sağ çocukta sağa, sonra z'de sola
Sık yapılan hata

Durumu yalnız z'nin denge faktörüne bakarak seçmek. bf(z) = 2 yalnız “sol ağır” der; LL mi LR mi olduğu çocuğun eğimine bağlıdır. Boş bir AVL ağacına 50, 20, 30 eklenince bf(50) = 2 olur, ama bf(20) = −1'dir: durum LR'dir. 50'de tek sağa döndürme 20(—, 50(30, —)) verir ve bf(20) = −2 olur; ağaç yine dengesizdir. Doğru sonuç 30(20, 50)'dir.

10.4.8AVL kodu

AVL düğümü anahtar, yükseklik ve iki çocuk referansı taşır. Sunumdaki kod, önce üç küçük yardımcı ile sağa döndürmeyi verir:

int h(AvlDugum d) {
    return d == null ? -1 : d.yukseklik;
}
int bf(AvlDugum d) {
    return h(d.sol) - h(d.sag);
}
void guncelle(AvlDugum d) {
    d.yukseklik = 1 +
        Math.max(h(d.sol), h(d.sag));
}
AvlDugum sagaDondur(AvlDugum y) {
    AvlDugum x = y.sol;
    y.sol = x.sag;  // T2 → y.sol
    x.sag = y;      // y → x.sag
    guncelle(y);    // önce alttaki
    guncelle(x);
    return x;       // yeni kök
}

h, boş alt ağaç için −1 döndürür; böylece öteki metotlar null kontrolüyle uğraşmaz. bf, iki çocuğun yükseklik farkıdır. guncelle, bir düğümün yüksekliğini çocuklarınınkinden yeniden hesaplar. Bu hesap, çocukların yüksekliğinin doğru olmasına dayanır. Bu yüzden yükseklikler her zaman aşağıdan yukarı güncellenir.

sagaDondur(y), Şekil 10.25'teki adımları kodlar. x, yukarı çıkacak sol çocuktur. y.sol = x.sag T2'yi taşır; x.sag = y ilişkiyi tersine çevirir. Sonra iki düğümün yüksekliği güncellenir: önce y, sonra x. Döndürmeden sonra y, x'in çocuğudur; x'in yüksekliği y'nin yeni yüksekliğine bağlıdır. Sıra ters olsaydı x eski, yanlış bir değerden hesaplanırdı. Metot yeni alt ağaç kökünü, x'i döndürür. Üçüncü bağı, yani ebeveynden gelen bağı, çağıran bu dönüş değerini kendi bağına atayarak değiştirir.

Sola döndürme simetriktir; sunumda “sol ↔ sag simetriği” diye kısaltılan kod açık hâliyle şöyledir:

AvlDugum solaDondur(AvlDugum x) {
    AvlDugum y = x.sag;
    x.sag = y.sol;  // T2 → x.sag
    y.sol = x;      // x → y.sol
    guncelle(x);    // önce alttaki
    guncelle(y);
    return y;       // yeni kök
}

Dört durumu bir araya getiren metot dengele'dir:

AvlDugum dengele(AvlDugum d) {
    guncelle(d);
    if (bf(d) > 1) {       // sol ağır
        if (bf(d.sol) < 0)  // LR
            d.sol = solaDondur(d.sol);
        return sagaDondur(d); // LL
    }
    if (bf(d) < -1) {      // sağ ağır
        if (bf(d.sag) > 0)  // RL
            d.sag = sagaDondur(d.sag);
        return solaDondur(d); // RR
    }
    return d;
}
// solaDondur: sol ↔ sag simetriği
// ekle/sil BST gibi; dönüşte:
//   return dengele(kok);

dengele(d) önce d'nin yüksekliğini günceller. Denge faktörü 1'den büyükse düğüm sol ağırdır. Sol çocuk sağa eğikse (bf(d.sol) < 0) durum LR'dir; önce sol çocukta sola döndürülür ve sonuç d.sol'a yazılır. Ardından her iki durumda da d'de sağa döndürülür. Denge faktörü −1'den küçükse aynı mantık simetrik olarak işler. Denge bozuk değilse d olduğu gibi döner.

Ekleme, düz BST eklemesinin son satırını değiştirerek elde edilir:

AvlDugum ekle(AvlDugum d, int k) {
    if (d == null) return new AvlDugum(k);
    if (k < d.anahtar)
        d.sol = ekle(d.sol, k);
    else if (k > d.anahtar)
        d.sag = ekle(d.sag, k);
    else return d;          // tekrar yok
    return dengele(d);
}

Fark tek satırdadır: return d yerine return dengele(d). Özyineleme geri dönerken yeni yapraktan köke kadar yol üzerindeki her düğümde dengele çağrılır. Böylece yükseklikler aşağıdan yukarı güncellenir ve dengesiz bir düğüm bulunursa hemen düzeltilir. Silme de aynı biçimde düz BST silmesinin dönüşlerine dengele eklenerek yazılır.

Maliyet: iniş O(log n) düzeydir. Dönüşte her düzeyde dengele sabit iş yapar; döndürmeler de sabit sürelidir. Ekleme ve silme O(log n) sürer. Eklemede en çok bir düzeltme (bir tek ya da bir çift döndürme) gerekir. Silmede bir düzeltme alt ağacın yüksekliğini azaltabilir ve dengesizlik yukarıya taşınabilir; en kötü durumda yolun her düzeyinde, yani O(log n) kez döndürme yapılır.

10.4.9Kod izleme: sagaDondur ve solaDondur

İlk izleme LL durumudur. Ağaç 40(20(10, 30), 50)'dir ve 5 eklenir. 5, 40 → 20 → 10 yolundan inip 10'un soluna yerleşir. Dönüşte dengele(10) ve dengele(20) döndürme gerektirmez (bf sırasıyla 1 ve 1). dengele(40) ise bf = 2 bulur; sol çocuk 20'nin bf'si 1'dir, durum LL'dir ve sagaDondur(40) çağrılır.

Tablo 10.15: sagaDondur(y) izlemesi, y = 40 (LL durumu).
SatırEtkiDeğişen
x = y.solyukarı çıkacak çocuk 20; henüz bağ değişmedi—
y.sol = x.sag40'ın sol bağı artık T2'yi, yani 30'u gösterir; 20'ye yalnız x ulaşırbağ 1
x.sag = y20'nin sağ bağı 30 yerine 40'ı gösterirbağ 2
guncelle(y)40.yukseklik = 1 + max(0, 0) = 1 (önceden 3)yükseklik
guncelle(x)20.yukseklik = 1 + max(1, 1) = 2 (aynı kaldı, ama yeniden hesaplandı)yükseklik
return xyeni alt ağaç kökü 20; çağıran onu kendi bağına atarbağ 3

Sonuç ağacı 20(10(5, —), 40(30, 50))'dir. Denge faktörleri: 20'de 0, 10'da 1, 40'ta 0, yapraklarda 0. Kök ortada sıra 5 10 20 30 40 50'dir ve döndürmeden önceki sırayla aynıdır. 40'ın yüksekliği 3'ten 1'e indi, alt ağacın kökü 20'nin yüksekliği ise 2 oldu; ekleme öncesindeki alt ağacın yüksekliği de 2'ydi.

402h=3201h=2101h=150h=0300h=0500h=0 40h=3y20h=2x10h=15h=030h=050h=0
40h=3y20h=2x10h=15h=030h=050h=0 200h=2x101h=150h=0400h=1y300h=0500h=0
Şekil 10.28: sagaDondur(40); düğümlerin yanında denge faktörü ve yükseklik yazılıdır. (a) 5 eklendi; bf(40) = 2, bf(20) = 1: LL. (b) y.sol = x.sag: 40'ın sol bağı 30'u gösteriyor. (c) x.sag = y: 40, 20'nin sağ çocuğu oldu. (d) Yeni alt ağaç kökü 20; bütün denge faktörleri {−1, 0, 1} içinde.

İkinci izleme RR durumudur ve öncekinin aynadaki görüntüsüdür. Ağaç 20(10, 40(30, 50))'dir ve 60 eklenir. dengele(20) bf = −2 bulur, sağ çocuk 40'ın bf'si −1'dir ve solaDondur(20) çağrılır. y = x.sag 40'ı tutar. x.sag = y.sol 20'nin sağ bağını 30'a yöneltir. y.sol = x 20'yi 40'ın sol çocuğu yapar. guncelle(x) 20'nin yüksekliğini 1 + max(0, 0) = 1 bulur (önceden 3), guncelle(y) 40'ınkini 1 + max(1, 1) = 2 bulur. Sonuç 40(20(10, 30), 50(—, 60))'tır.

20−2h=3100h=040−1h=2300h=050−1h=1600h=0 400h=2y200h=1x100h=0300h=050−1h=1600h=0
Şekil 10.29: solaDondur(20). (a) 60 eklendi; bf(20) = −2, bf(40) = −1: RR. (b) Yeni alt ağaç kökü 40; 20 onun sol çocuğu, 30 da 20'nin sağ çocuğu oldu.
Sık yapılan hata

Döndürmeden sonra yükseklikleri ters sırayla güncellemek. guncelle(x) önce çağrılırsa x'in yüksekliği, y'nin henüz güncellenmemiş eski yüksekliğinden (LL örneğinde 3) hesaplanır ve 4 bulunur. Ardından üst düğümlerin denge faktörleri yanlış çıkar ve gereksiz döndürmeler yapılır. Önce aşağı inen düğüm, sonra yeni kök güncellenir.

10.4.10Kod izleme: dengele

Çift döndürmenin kodda nasıl işlediğini LR örneğinde izleyelim. Ağaç 50(20(10, 30), 60)'tır ve 25 eklenir. 25, 50 → 20 → 30 yolundan inip 30'un soluna yerleşir. Özyineleme geri dönerken dengele yol üzerindeki her düğümde çağrılır.

Tablo 10.16: 25 eklendikten sonra dönüşte dengele çağrıları (LR durumu).
ÇağrıYükseklikbfKarar
dengele(30)0 → 11döndürme yok, return d
dengele(20)1 → 20 − 1 = −1döndürme yok, return d
dengele(50)2 → 3h(20) − h(60) = 2 − 0 = 2sol ağır; bf(20) = −1 < 0 → LR
d.sol = solaDondur(20)30 yukarı çıkar, 25 20'nin sağına geçerüçgen düz çizgiye döndü; bf(50) hâlâ 2
return sagaDondur(50)30 kök olur, 50 onun sağına iner; 30'un eski sağı (null) 50'nin soluna geçeryeni alt ağaç kökü 30

Sonuç 30(20(10, 25), 50(—, 60))'tır. Yükseklik 3'ten 2'ye indi; ekleme öncesindeki değer de 2'ydi. Denge faktörleri 30'da 0, 20'de 0, 50'de −1, yapraklarda 0'dır. İlk döndürmenin sonucunun d.sol'a atanması şarttır: solaDondur(20) yeni alt ağaç kökü olarak 30'u döndürür ve 50'nin sol bağı ancak bu atamayla 30'u gösterir.

502h=3d20−1h=2100h=0301h=1250h=0600h=0 502h=3d20−1h=2100h=0301h=1250h=0600h=0
502h=3d302h=2200h=1100h=0250h=0600h=0 300h=2200h=1100h=0250h=050−1h=1600h=0
Şekil 10.30: dengele(50) izlemesi. (a) Yükseklik 3, bf = 2. (b) bf(20) = −1: sol çocuk sağa eğik, LR. (c) solaDondur(20): 30 yukarı çıktı, üçgen düz çizgiye döndü. (d) sagaDondur(50): 30 yeni alt ağaç kökü; yükseklik 3'ten 2'ye indi.

10.4.11AVL'ye ekleme örneği

Boş bir AVL ağacına sırasıyla 10, 20, 30, 40, 50, 25 eklensin. Düz BST bu girdiyle (son anahtar dışında) sağa uzanan bir zincir kurardı. Tablo 10.17 her eklemeden sonraki ağacı ve yapılan döndürmeleri verir.

Tablo 10.17: Boş AVL ağacına 10, 20, 30, 40, 50, 25 eklenmesi.
EklemeİnişDönüşte dengesizlikDöndürmeSonuç ağacı
10boş ağaç——10
2010 → sağbf(10) = −1, sorun yok—10(—, 20)
3010 → 20 → sağbf(10) = −2, bf(20) = −1: RR10'da sola20(10, 30)
4020 → 30 → sağbf(30) = −1, bf(20) = −1, sorun yok—20(10, 30(—, 40))
5020 → 30 → 40 → sağbf(30) = −2, bf(40) = −1: RR30'da sola20(10, 40(30, 50))
2520 → 40 → 30 → solbf(20) = −2, bf(40) = 1: RL40'ta sağa, 20'de sola30(20(10, 25), 40(—, 50))

50 eklendiğinde 30 ve 20 aynı anda dengesiz görünebilir mi? Hayır. Dönüşte önce dengele(40), sonra dengele(30) çağrılır. 30 dengesizdir ve düzeltilir; alt ağacın yüksekliği eski değeri olan 1'e iner. dengele(20) çağrıldığında yükseklik 2 → 2 kalır, bf(20) = −1'dir. En alttaki dengesiz ata düzeltildiği için yukarısı etkilenmez.

Son eklemede 25, 30'un soluna düşer. Dönüşte bf(30) = 1 ve bf(40) = 1 sorun değildir. bf(20) = h(10) − h(40) = 0 − 2 = −2'dir ve sağ çocuk 40 sola eğiktir: RL. Önce 40'ta sağa döndürülür (30 yukarı çıkar), sonra 20'de sola döndürülür. 30 köke çıkar; 25, 20'nin sağına geçer. Altı ekleme sonunda ağacın yüksekliği 2'dir; düz BST'de aynı sıra 4 yüksekliğinde bir ağaç verirdi.

10−2d20−1300 200100300
20−2d100401301250500 30020010025040−1500
Şekil 10.31: AVL ekleme izlemesinden dört an. (a) 30 eklendi: bf(10) = −2, RR. (b) 10'da sola döndürme; 20 kök. (c) 25 eklendi: bf(20) = −2, bf(40) = 1, RL. (d) İki döndürmeden sonra 30 kök; bütün denge faktörleri {−1, 0, 1} içinde.
Örnek 10.6

Boş bir AVL ağacına 1, 2, …, 7 sırayla eklenirse ne olur? Her döndürme RR durumundadır.

  1. 3 eklenince 1'de sola döndürülür: 2(1, 3).
  2. 4 eklenir: 2(1, 3(—, 4)); dengesizlik yok.
  3. 5 eklenince 3'te sola döndürülür: 2(1, 4(3, 5)).
  4. 6 eklenince kök 2'de bf = −2 olur; 2'de sola döndürülür: 4(2(1, 3), 5(—, 6)).
  5. 7 eklenince 5'te sola döndürülür: 4(2(1, 3), 6(5, 7)).

Toplam dört döndürme yapılır ve sonuç yüksekliği 2 olan mükemmel ağaçtır. Sıralı girdi, düz BST'nin en kötü durumuydu; AVL ağacı aynı girdiden en iyi şekli üretti.

10.5Kırmızı-siyah ağaçlar

AVL ağacı dengeyi sıkı tutar: her düğümde iki alt ağacın yükseklik farkı en çok 1'dir. Bu sıkılığın bedeli, silmede dengesizliğin köke kadar yayılabilmesi ve her düzeyde döndürme gerekebilmesidir. Kırmızı-siyah ağaç daha gevşek bir denge kabul eder. Karşılığında güncellemelerde çok az döndürme yapar ve çoğu düzeltmeyi yalnız renk değiştirerek halleder. Yapı, Guibas ve Sedgewick'in 1978'deki çalışmasında bugünkü renk diliyle tanımlanmıştır. Java'nın TreeMap ve TreeSet sınıfları ile C++'ın std::map'i bu yapıyı kullanır.

10.5.1Kurallar

Tanım 10.6

Kırmızı-siyah ağaç (red-black tree), her düğümü kırmızı ya da siyah renkli olan ve şu beş kuralı sağlayan ikili arama ağacıdır:

  1. Her düğüm kırmızı ya da siyahtır.
  2. Kök siyahtır.
  3. Bütün yapraklar, yani boş çocuklar (NIL), siyah sayılır.
  4. Kırmızı bir düğümün çocukları siyahtır: art arda iki kırmızı düğüm bulunmaz.
  5. Bir düğümden altındaki NIL yapraklara giden her yolda aynı sayıda siyah düğüm vardır. Bu sayıya düğümün siyah yüksekliği (black height) denir.

Üçüncü kural ilk bakışta tuhaf gelir: boş bir çocuğun rengi ne demektir? Kural, kodda null çocukların siyah sayılacağını söyler. Böylece kural 4 ve 5, çocuğu eksik düğümlerde de anlamlı kalır. Notta siyah düğüm sayılırken kök sayılır, NIL yapraklar sayılmaz.

13 8 1 6 11 17 15 25 22 27
Şekil 10.32: Geçerli bir kırmızı-siyah ağaç; küçük siyah kareler NIL yapraklardır. Kök 13 siyah; 8 ve 17 kırmızı; 1, 11, 15, 25 siyah; 6, 22, 27 kırmızı. Kökten her NIL'e giden yolda 2 siyah düğüm vardır (kök dahil, NIL hariç).

Kuralları Şekil 10.32 üzerinde denetleyelim. Kırmızı 8'in çocukları 1 ve 11 siyahtır; kırmızı 17'nin çocukları 15 ve 25 siyahtır. Kırmızı yaprakların (6, 22, 27) çocukları NIL'dir ve siyah sayılır. Kural 4 sağlanır. Kural 5 için birkaç yol sayalım: 13 → 8 → 1 → NIL yolunda siyahlar 13 ve 1'dir; 13 → 8 → 1 → 6 → NIL yolunda da 13 ve 1'dir, 6 kırmızıdır. 13 → 17 → 25 → 27 → NIL yolunda siyahlar 13 ve 25'tir. Her yolda iki siyah vardır.

Sık yapılan hata

Kural 5'i yalnız en uzun ve en kısa yola bakarak denetlemek. Siyah sayısı her yolda eşit olmalıdır ve yollar NIL'e kadar sayılır. Tek çocuklu bir düğümün boş tarafı da bir yoldur. Bir örnek düşünelim: kök siyah 20, çocukları siyah 10 ve siyah 30, 30'un sol çocuğu da siyah 25 olsun. 20 → 10 → NIL yolunda 2 siyah vardır. 20 → 30 → 25 → NIL yolunda ise 3 siyah vardır. Ağaç kural 5'i bozar.

10.5.2Kurallar dengeyi nasıl garanti eder?

Kurallar ağacın yüksekliğini doğrudan sınırlamaz. Sınır iki kuralın birlikte çalışmasından çıkar. Kökün siyah yüksekliği b olsun. Kökten bir NIL'e giden en kısa yol ne kadar kısa olabilir? Her yolda tam b siyah düğüm vardır; en kısa yol yalnız siyah düğümlerden oluşur ve b düğümlüdür. En uzun yol ne kadar uzun olabilir? Yine b siyah düğüm içerir, ama araya kırmızılar girebilir. Kural 4 iki kırmızının art arda gelmesini yasaklar; en fazla her siyahın altına bir kırmızı girer. En uzun yol en çok 2b düğümlüdür.

1052015302540en kısa: 2en uzun: 4 = 2·2
Şekil 10.33: Siyah yüksekliği 2 olan ağaçta en kısa yol 10 → 5 (2 düğüm, ikisi de siyah); en uzun yol 10 → 20 → 30 → 40 (4 düğüm, siyah ve kırmızı sırayla). En uzun yol, en kısanın 2 katıdır.
Teorem 10.4 (Kırmızı-siyah ağacın yüksekliği)

Kırmızı-siyah ağaçta kökten bir NIL'e giden en uzun yol, en kısa yolun en çok iki katıdır. n iç düğümlü bir kırmızı-siyah ağacın yüksekliği h ≤ 2 log2(n + 1)'dir.

Birinci cümle yukarıdaki sayımdan çıkar. İkincisi için şunu gözleyelim: siyah yüksekliği b olan bir düğümün alt ağacında en az 2b − 1 iç düğüm vardır. Bu, tümevarımla gösterilir. Siyah yüksekliği 0 olan alt ağaç NIL'dir ve 20 − 1 = 0 düğüm içerir. Siyah yüksekliği b olan bir düğümün her çocuğunun siyah yüksekliği en az b − 1'dir. Tümevarım varsayımına göre her çocuğun alt ağacında en az 2b−1 − 1 düğüm vardır. Düğümle birlikte toplam en az 2(2b−1 − 1) + 1 = 2b − 1'dir. Kökte kural 4'ten b ≥ h/2 olduğu için n ≥ 2h/2 − 1, buradan h ≤ 2 log2(n + 1) çıkar. □

Sonuç olarak kırmızı-siyah ağaçta arama, ekleme ve silme O(log n) sürer. Ekleme sonrası düzeltmede en çok 2, silme sonrası düzeltmede en çok 3 döndürme yapılır; geri kalan iş renk değiştirmektir. Yükseklik sınırı AVL'ninkinden (yaklaşık 1,44 log2 n) gevşektir; yani kırmızı-siyah ağaç biraz daha uzun olabilir. Buna karşılık güncellemeleri daha ucuzdur ve düğüm başına ek bilgi yalnız bir bitlik renktir.

10.5.3Ekleme ve düzeltme durumları

Ekleme iki aşamalıdır. Birinci aşamada yeni düğüm düz BST eklemesiyle yaprak olarak bağlanır ve kırmızıya boyanır. Neden kırmızı? Siyah eklenseydi, yeni düğümün bulunduğu yollarda siyah sayısı bir artar ve kural 5 bozulurdu. Kırmızı düğüm siyah yüksekliği değiştirmez. Bozulabilecek tek kural 4'tür: yeni düğümün ebeveyni de kırmızıysa art arda iki kırmızı oluşur. İkinci aşama bu bozukluğu gidermektir.

Düzeltmede dört düğüm rol alır (Şekil 10.34): yeni düğüm x, ebeveyni p, dedesi g ve amcası u, yani ebeveynin kardeşi. Ebeveyn kırmızıysa kök olamaz (kök siyahtır); demek ki bir dede vardır ve dede siyahtır (yoksa ağaçta önceden art arda iki kırmızı olurdu). Neyin yapılacağına amcanın rengi karar verir.

g p x udedeebeveynamcayeni düğüm
Şekil 10.34: Düzeltmedeki roller: yeni düğüm x, ebeveyn p, dede g, amca u.
Tablo 10.18: Ekleme sonrası düzeltme durumları (ebeveyn kırmızıyken).
DurumYapılanSonra
Amca kırmızıRenk değiştir: p ve u siyah, g kırmızı.Sorun dedeye taşınabilir; x = g alınıp döngü sürer.
Amca siyah, üçgenx–p–g zikzak yapıyorsa p'de döndür; x ile p'nin rolleri değişir.Çizgi durumuna geçilir.
Amca siyah, çizgig'de döndür; eski ebeveyn siyah, eski dede kırmızı olur.Bitti.

Ebeveyn siyahsa hiçbir şey yapılmaz; art arda kırmızı yoktur. En sonda kök her zaman siyaha boyanır. NIL amca siyah sayılır.

Renk değiştirmenin neden kural 5'i koruduğunu görelim. Önce g siyah, p ve u kırmızıdır. Sonra g kırmızı, p ve u siyahtır. g'den aşağı inen her yol, p ya da u'dan tam birinden geçer. Önce bu yolda bir siyah vardı (g), şimdi yine bir siyah var (p ya da u). Siyah sayıları değişmez. Ama g kırmızı olduğu için onun ebeveyni de kırmızıysa kural 4 bu kez yukarıda bozulur. Bu yüzden döngü g'den devam eder. Sorun her adımda iki düzey yukarı çıkar; en kötü durumda köke ulaşır ve kökün siyaha boyanmasıyla biter.

Amca siyahken renk değiştirmek işe yaramaz. Amca zaten siyahtır; g'yi kırmızı yapmak ise amcanın tarafındaki yollarda bir siyah eksiltir. Bunun yerine döndürme yapılır. Çizgi durumunda (x ile p aynı yönde çocuk) g'de döndürülür; p yukarı çıkar ve siyah olur, g aşağı iner ve kırmızı olur. Üçgen durumunda önce p'de döndürülerek çizgi elde edilir. Bu iki adım, AVL'deki tek ve çift döndürmelerin aynısıdır. Döndürmeden sonra alt ağacın kökü siyahtır ve döngü biter.

10.5.4Düzeltme kodu

Kırmızı-siyah ağaç düğümünde anahtar ve iki çocuğun yanında bir renk alanı ve ebeveyne bir referans vardır. Ebeveyn referansı, düzeltmenin aşağıdan yukarı yürüyebilmesi içindir. Sunumdaki düzeltme kodu şöyledir:

void duzelt(Dugum x) {      // x: yeni, kırmızı
    while (x != kok && kirmizi(x.ebeveyn)) {
        Dugum p = x.ebeveyn, g = p.ebeveyn;
        Dugum u = (p == g.sol) ? g.sag : g.sol;
        if (kirmizi(u)) {         // amca kırmızı
            p.renk = u.renk = SIYAH;
            g.renk = KIRMIZI;
            x = g;                // sorun dedeye
        } else {                  // amca siyah
            if (ucgen(x, p, g)) { // zikzak
                dondur(p, x);     // çizgiye çevir
                Dugum t = x; x = p; p = t;
            }
            dondur(g, p);         // çizgi
            p.renk = SIYAH; g.renk = KIRMIZI;
        }
    }
    kok.renk = SIYAH;
}

Yardımcılar ve eklemenin bütünü yorum satırlarıyla verilmiştir:

// Ekleme: BST gibi yaprağa ekle,
// ebeveyni bağla, kırmızı boya:
//   x = bstEkle(anahtar);
//   x.renk = KIRMIZI; duzelt(x);

// NIL (null) siyah sayılır
boolean kirmizi(Dugum n) {
    return n != null
        && n.renk == KIRMIZI;
}
// x–p–g zikzak (üçgen) mı?
boolean ucgen(Dugum x, Dugum p,
              Dugum g) {
    return (p == g.sol)
        != (x == p.sol);
}
// dondur(a, b): b, a'nın çocuğu;
// b'yi a'nın yerine çıkarır (b
// soldaysa sağa, sağdaysa sola
// döndürme; ebeveyn bağları dahil)

kirmizi(n), null'ı siyah sayar; böylece NIL amca ayrıca ele alınmaz. ucgen(x, p, g), “p dedenin sol çocuğu mu?” ile “x ebeveynin sol çocuğu mu?” sorularının cevaplarını karşılaştırır. Cevaplar farklıysa x–p–g zikzaktır. dondur(a, b), a'nın çocuğu olan b'yi a'nın yerine çıkarır; b soldaysa sağa, sağdaysa sola döndürür ve ebeveyn bağlarını da günceller. İzlemelerde duzelt'in satır numaraları kullanılır. Satır 2 döngü koşulu, satır 5 amca kontrolü, satır 10 üçgen kontrolüdür. Satır 14 dedede döndürür, satır 18 kökü siyaha boyar.

Döngüdeki Dugum t = x; x = p; p = t; satırı üçgen durumundan sonra rolleri değiştirir. p'de döndürmeden sonra x yukarı, p aşağı geçmiştir. Çizgi durumundaki dondur(g, p) çağrısının doğru düğümü döndürmesi için p değişkeninin artık yukarıdaki düğümü göstermesi gerekir.

Maliyet: BST eklemesi O(log n)'dir. Düzeltme döngüsü her turda en az iki düzey yukarı çıkar; en çok O(log n) tur döner. Döndürme yalnız döngünün son turunda yapılır (amca siyah dalı döngüyü bitirir) ve en çok iki tanedir.

Sık yapılan hata

Döngüden sonraki kok.renk = SIYAH satırını unutmak. Renk değiştirme dedeyi kırmızı yapar; dede kökse döngü “x kök” koşuluyla biter ve kök kırmızı kalır. Kural 2 bozulur. Bu satır, siyah yüksekliğin ağaç genelinde bir artmasının da tek yoludur.

10.5.5Kod izleme: 10, 20, 30, 15

Boş bir kırmızı-siyah ağaca sırasıyla 10, 20, 30 ve 15 eklensin. Metinde renk, düğümün yanına yazılan K (kırmızı) ve S (siyah) harfleriyle gösterilir: K, S.

Tablo 10.19: 10, 20, 30, 15 eklenirken duzelt izlemesi.
EklemeDöngüp, g, uYapılanSonuç
10x kök → girilmez—satır 18: kök siyah10S
20ebeveyn 10 siyah → girilmez——10S(—, 20K)
30ebeveyn 20 kırmızı → girilir20, 10, NIL (siyah)çizgi: dondur(10, 20) sola; 20 siyah, 10 kırmızı20S(10K, 30K)
15ebeveyn 10 kırmızı → girilir10, 20, 30Kamca kırmızı: 10 ve 30 siyah, 20 kırmızı; x = 20; x kök → döngü biter; satır 18: 20 siyah20S(10S(—, 15K), 30S)

30 eklenirken amca NIL'dir ve siyah sayılır. 30, 20'nin sağ çocuğu; 20 de 10'un sağ çocuğudur. Üçü düz bir çizgi oluşturur; ucgen yanlış döner ve doğrudan satır 14'e geçilir. Dedede sola döndürme, AVL'deki RR düzeltmesinin aynısıdır. Renkler değiştirilince alt ağacın kökü (20) siyah olur ve döngü biter.

15 eklendiğinde amca 30 kırmızıdır; döndürme gerekmez, renk değiştirmek yeter. Dede 20 kırmızı olur ve döngü 20'den devam eder. 20 kök olduğu için döngü koşulu yanlıştır. Satır 18 kökü yeniden siyaha boyar. Bu ekleme, ağacın siyah yüksekliğini 1'den 2'ye çıkardı: artık kökten her NIL'e giden yolda iki siyah düğüm vardır.

102030x 20p10g30x
20g10p15x30u 20x101530
Şekil 10.35: İlk izlemeden dört an. (a) 30 eklendi ve kırmızı boyandı; ebeveyn 20 de kırmızı. (b) 10'da sola döndürme ve renk değişiminden sonra 20 siyah, 10 ve 30 kırmızı. (c) 15 eklendi; amca 30 kırmızı olduğu için yalnız renk değişti: 10 ve 30 siyah, 20 kırmızı. (d) Kök yeniden siyah; siyah yükseklik 1 arttı.

10.5.6Kod izleme: 12 ve 5

Aynı ağaca, 20S(10S(—, 15K), 30S), önce 12, sonra 5 eklensin. 12 üçgen durumunu, 5 ise renk değiştirmenin bir düzey yukarı taşınmasını gösterir.

Tablo 10.20: 12 ve 5 eklenirken duzelt izlemesi.
EklemeKonump, g, uYapılanSonuç
1215'in sol çocuğu15K, 10S, NILamca siyah, üçgen: dondur(15, 12) sağa; roller değişir (x = 15, p = 12); dondur(10, 12) sola; 12 siyah, 10 kırmızı20S(12S(10K, 15K), 30S)
510'un sol çocuğu10K, 12S, 15Kamca kırmızı: 10 ve 15 siyah, 12 kırmızı; x = 12; ebeveyn 20 siyah → döngü biter20S(12K(10S(5K, —), 15S), 30S)

12 eklenirken 15, 10'un sağ çocuğudur; 12 ise 15'in sol çocuğudur. Yol zikzak çizer. Önce 15'te sağa döndürülür: 12 yukarı, 15 aşağı geçer ve 10 → 12 → 15 düz çizgisi oluşur; ama art arda iki kırmızı hâlâ vardır. Roller değiştirildikten sonra 10'da sola döndürülür. 12 alt ağacın köküne çıkar ve siyah olur, 10 kırmızı olur. Bu, AVL'deki RL düzeltmesinin aynısıdır.

5 eklenirken amca 15 kırmızıdır. Renk değiştirme 12'yi kırmızı yapar ve döngü 12'den devam eder. 12'nin ebeveyni 20 siyah olduğu için döngü biter; kök zaten siyahtır. Son ağaçta her yolda 2 siyah düğüm vardır. 20 → 12 → 10 → 5 → NIL yolundaki siyahlar 20 ve 10'dur. 20 → 30 → NIL yolunda da 20 ve 30 siyahtır.

20101512x30 NILu2010g12x15p30
2012p10g15x30 2012x1051530
Şekil 10.36: İkinci izlemeden dört an. (a) 12, 15'in sol çocuğu olarak eklendi; 15 de kırmızı. (b) 15'te sağa döndürme: üçgen düz çizgiye döndü. (c) 10'da sola döndürme ve renkler: 12 siyah, 10 ve 15 kırmızı. (d) 5 eklendi, renk değişti; kurallar sağlanıyor: 20S(12K(10S(5K, —), 15S), 30S).
Örnek 10.7

Boş bir kırmızı-siyah ağaca 1'den 10'a kadar sayılar sırayla eklenirse ne olur? Kod çalıştırıldığında beş renk değişimi ve beş döndürme yapılır. Sonuç ağacı 4S(2S(1S, 3S), 6S(5S, 8K(7S, 9S(—, 10K))))'dur. Yükseklik 4, siyah yükseklik 3'tür. Aynı girdi düz BST'de yüksekliği 9 olan bir zincir verirdi. AVL ağacı ise aynı girdiden yüksekliği 3 olan bir ağaç kurar. Kırmızı-siyah ağaç AVL'den bir düzey uzun kaldı; ama ikisi de logaritmik yüksekliktedir.

10.5.7Dengeli ağaçlarda silme

AVL ve kırmızı-siyah ağaçlarda silme aynı biçimde başlar: önce düz BST silmesi uygulanır. İki çocuklu düğüm ardılı ya da öncülüyle değiştirilir; gerçekte ağaçtan kopan düğüm her zaman bir yaprak ya da tek çocuklu bir düğümdür. Düğümün kopması dengeyi bozabilir: AVL'de yükseklik farkı, kırmızı-siyah ağaçta siyah yükseklik kuralı ihlal edilebilir.

  • AVL düzeltmesi: Kopan düğümden köke doğru yükseklikler güncellenir. |bf| = 2 olan her düğümde LL, RR, LR ya da RL döndürmesi uygulanır. Eklemenin tersine, bir döndürme alt ağacın yüksekliğini eski değerinin altına indirebilir; bu da üstteki bir atayı dengesizleştirebilir. Bu yüzden birden fazla atada düzeltme gerekebilir.
  • Kırmızı-siyah düzeltmesi: Kırmızı bir düğümün silinmesi kolaydır; siyah sayıları değişmez. Siyah bir düğüm kopunca onun yolları bir siyah eksik kalır. Bu eksiklik, kardeşin rengine ve kardeşin çocuklarının renklerine göre yeniden renklendirme ve döndürmeyle giderilir. Bu düzeltmeye kitaplarda “çift siyah” düzeltmesi denir; eksik siyahlık köke kadar taşınabilir, ama döndürme sayısı en çok üçtür.
Tablo 10.21: Dengeli ağaçlarda silme maliyeti.
YapıYukarı doğru incelemeDöndürmeToplam
AVLO(log n)O(log n) olabilirO(log n)
Kırmızı-siyahO(log n)en çok 3O(log n)

Ekleme ve silme BST sırasını korur. Dengeleme yalnız ağacın biçimini ve düğümlerdeki yardımcı bilgileri (yükseklik ya da renk) değiştirir; kök ortada gezinme her an aynı sıralı listeyi verir.

10.5.8AVL mi, kırmızı-siyah mı?

İki yapı da bütün işlemleri O(log n)'de yapar. Seçim, işlemlerin karışımına bağlıdır. Tablo 10.22 farkları özetler.

Tablo 10.22: AVL ve kırmızı-siyah ağacın karşılaştırması.
ÖzellikAVLKırmızı-siyah
Denge ölçütüher düğümde |bf| ≤ 1renk kuralları, eşit siyah yükseklik
En büyük yükseklik≈ 1,44 log2 n2 log2(n + 1)
AramaO(log n), biraz daha hızlı (ağaç daha kısa)O(log n)
Ekleme döndürmesien çok 2 (bir tek ya da bir çift)en çok 2
Silme döndürmesiO(log n) olabiliren çok 3
Düğümde ek bilgiyükseklik (ya da denge faktörü)1 bit renk
Terciharamanın baskın olduğu işlersık ekleme ve silme, kütüphaneler

Uygulamada fark çoğu zaman küçüktür. Bir sözlük bir kez kurulup milyonlarca kez sorgulanacaksa AVL'nin daha kısa ağacı her aramada birkaç karşılaştırma kazandırır. Ekleme ve silmenin sık olduğu genel amaçlı bir kütüphane sınıfında kırmızı-siyah ağacın ucuz güncellemesi ağır basar. Java ve C++ standart kütüphanelerinin kırmızı-siyah ağacı seçmesinin nedeni budur.

10.6Splay ağaçları

AVL ve kırmızı-siyah ağaçlar her düğümde ek bilgi tutar ve her işlemden sonra dengeyi bu bilgiye bakarak düzeltir. Splay ağacı başka bir yol izler: hiçbir denge bilgisi tutmaz ve ağacın dengeli olmasına da çalışmaz. Bunun yerine her erişimde erişilen düğümü köke taşır. Yöntemi 1985'te Sleator ve Tarjan önerdi; yapıya kendini düzenleyen (self-adjusting) BST de denir.

10.6.1Kendini düzenleyen ağaç

Tanım 10.7

Splay ağacı (splay tree), her erişimden (arama, ekleme, silme) sonra erişilen düğümü bir dizi döndürmeyle köke taşıyan ikili arama ağacıdır. Bu köke taşıma işlemine splay işlemi denir. Düğümlerde yükseklik ya da renk gibi denge bilgisi tutulmaz.

Splay ağacının yüksekliği için hiçbir garanti yoktur; ağaç bir zincire bile dönüşebilir. Böyle bir ağaçta en alttaki düğüme erişmek O(n) sürer. Ama bu pahalı erişim ağacı da düzeltir: erişilen düğüm köke çıkarken yolu üzerindeki düğümlerin derinliği kabaca yarıya iner. Sleator ve Tarjan, boş ağaçtan başlayan m işlemlik her dizinin toplam O(m log n) sürdüğünü gösterdi. İşlem başına amortize maliyet O(log n)'dir. Amortize analiz, tek tek işlemlere değil, işlem dizisinin toplamına söz verir: bazı işlemler pahalı olabilir, ama ucuz işlemler onların maliyetini dengeler.

Yapının üç belirgin özelliği vardır:

  • Yerellik: Sık erişilen anahtarlar köke yakın durur. Ağaç bir önbellek gibi davranır. Erişimlerin küçük bir anahtar kümesinde yoğunlaştığı uygulamalarda splay ağacı dengeli ağaçlardan hızlı olabilir.
  • Basitlik: Ek alan gerekmez. Tek bir splay yordamı bütün işlemlere yeter: arama bulduğu düğümü, ekleme yeni düğümü, silme ise silinecek düğümü önce köke taşır.
  • Dezavantaj: Arama bile ağacı değiştirir. Bu, birden çok iş parçacığının aynı ağacı okuduğu durumlarda sorun çıkarır. Tek bir işlemin süresi için garanti yoktur; gerçek zamanlı sistemlerde bu kabul edilemeyebilir.

10.6.2Zig, zig-zig ve zig-zag

Splay, erişilen düğüm x kök olana kadar üç tür adımı tekrarlar. Adımın türünü x'in ebeveyni p ve dedesi g belirler.

20 10 5 15 30 x = 10, ebeveyni p = 20 kök sağa döndür(p) 10 5 20 15 30 x kök oldu; 15, p'nin soluna geçti
30 20 10 x, p, g aynı yönde (sol-sol) döndür(g) 20 10 30 önce dede g = 30 döndür(p) 10 20 30 sonra ebeveyn p = 20
30 10 20 x sağ, p sol çocuk (zikzak) sola döndür(p) 30 20 10 önce ebeveyn p = 10 sağa döndür(g) 20 10 30 sonra dede g = 30
Şekil 10.37: Splay adımları. Üstte zig: x = 10, ebeveyni p = 20 kök; tek döndürme x'i kök yapar ve 15, p'nin soluna geçer. Ortada zig-zig: x, p, g aynı yönde (sol-sol); önce dede (30), sonra ebeveyn (20) döndürülür. Altta zig-zag: x sağ, p sol çocuk; önce ebeveyn (10), sonra dede (30) döndürülür.
  • Zig: x'in ebeveyni köktür (dede yoktur). p'de tek bir döndürme yapılır ve x kök olur. Bu adım yalnız splay'in son adımında görülebilir.
  • Zig-zig: x ve p aynı yönde çocuktur (ikisi de sol ya da ikisi de sağ). Önce dedede, sonra ebeveynde döndürülür.
  • Zig-zag: x ve p zıt yönde çocuktur. Önce ebeveynde, sonra dedede döndürülür. Bu, AVL'deki çift döndürmenin aynısıdır.

Zig-zig'deki sıra önemlidir ve AVL'den farklıdır. Önce dedede döndürmek, yoldaki düğümleri “katlar”: zincirin üst kısmı da kısalır. Önce ebeveynde döndürmek (yani x'i tek döndürmelerle yukarı itmek) x'i yine köke çıkarır, ama zincirin geri kalanı uzun kalabilir. Sleator ve Tarjan'ın amortize sınırı tam olarak bu dededen başlayan sıraya dayanır.

Sık yapılan hata

Zig-zig adımında önce ebeveyni döndürmek. Bu, düğümü köke çıkaran “basit köke taşıma”dır ve amortize O(log n) garantisini vermez. Örneğin sıralı bir zincirde en alttaki düğüme tekrar tekrar erişildiğinde zincir uzun kalır ve her erişim O(n) sürebilir. Doğru sıra: önce dede g, sonra ebeveyn p.

Sunumdaki splay kodu bu üç adımı bir döngüde toplar:

void splay(Dugum x) {
    while (x != kok) {
        Dugum p = x.ebeveyn, g = p.ebeveyn;
        if (g == null)                // zig
            dondur(p, x);
        else if (ayniYon(x, p, g)) {  // zig-zig
            dondur(g, p);   // önce dede
            dondur(p, x);
        } else {                      // zig-zag
            dondur(p, x);   // önce ebeveyn
            dondur(g, x);
        }
    }
}

dondur(a, b), kırmızı-siyah koddaki tek döndürmenin aynısıdır: a'nın çocuğu olan b'yi a'nın yerine çıkarır. ayniYon(x, p, g), x'in p'nin hangi tarafında olduğunu, p'nin g'nin hangi tarafında olduğuyla karşılaştırır. Zig-zag dalındaki ikinci çağrı dondur(g, x) biçimindedir: ilk döndürmeden sonra x, p'nin yerine geçmiştir ve artık g'nin çocuğudur.

10.6.3Kod izleme: splay(50) ve splay(10)

Başlangıç ağacı 80(40(20(10, 30), 60(50, 70)), 90)'dır. Önce 50'ye, sonra 10'a erişilsin.

Tablo 10.23: splay(50) ve ardından splay(10) izlemesi.
ErişimTurp, gAdımDöndürmeler
50 (yol 80 → 40 → 60 → 50)160, 40zig-zag (50 sol, 60 sağ çocuk)60'ta sağa, sonra 40'ta sola
280, yokzig80'de sağa; 50 kök
10 (yol 50 → 40 → 20 → 10)120, 40zig-zig (ikisi de sol çocuk)önce 40'ta sağa, sonra 20'de sağa
250, yokzig50'de sağa; 10 kök

splay(50)'den sonra ağaç 50(40(20(10, 30), —), 80(60(—, 70), 90))'dır. 50 derinlik 3'ten köke çıktı. Eski kök 80 bir düzey indi ve 90 da onunla birlikte 1'den 2'ye geçti. 40, 20, 60 ve 70'in derinlikleri değişmedi.

splay(10)'dan sonra ağaç 10(—, 50(20(—, 40(30, —)), 80(60(—, 70), 90)))'dır. 10 köke çıktı ve az önce erişilen 50 hâlâ köke yakındır: derinliği 1'dir. Zig-zig adımında önce 40'ta döndürüldüğü için 40, 20'nin altına inmiş ve 30'u sol çocuğu olarak almıştır.

80402010306050x7090 8050x40g20103060p7090
50x4020103080607090 5010x20p40g3080607090
10x5020403080607090
Şekil 10.38: Splay izlemesi. (a) splay(50): BST araması 80 → 40 → 60 → 50. (b) Zig-zag bitti: 50, 40 ile 60'ın ebeveyni oldu. (c) Zig'den sonra 50 kök. (d) splay(10)'da zig-zig: önce 40'ta, sonra 20'de sağa döndürüldü; 10 iki düzey yükseldi. (e) Son zig'den sonra 10 kök; 50'nin derinliği 1.

Tek bir splay işleminin maliyeti, erişilen düğümün derinliğiyle orantılıdır; derin bir düğüm için O(n) olabilir. Ama ağaç bu işlemlerle sürekli yeniden şekillenir ve uzun bir işlem dizisinin ortalaması O(log n)'de kalır.

10.7Çok yollu arama ağaçları

Buraya kadar bütün ağaçlar ikiliydi: her düğümde bir anahtar ve en çok iki çocuk. Veri ana bellekte durduğunda bu yeterlidir; bir düğümden ötekine geçmek birkaç nanosaniye sürer. Veri diskte durduğunda durum değişir. Diskten bir blok okumak, bellekten bir değer okumaktan binlerce, eski dönen disklerde yüz binlerce kat yavaştır. Bir milyar kayıtlı bir veritabanı indisinde ikili ağacın yüksekliği en iyi durumda 30'dur. Her düğüm ayrı bir disk okuması isterse tek bir arama 30 disk erişimi tutar. Çok yollu ağaçlar bu sayıyı birkaç erişime indirir.

10.7.1B-ağacı

Tanım 10.8

B-ağacı (B-tree), bir düğümde birden çok anahtar ve çok sayıda çocuk bulunan dengeli bir çok yollu arama ağacıdır. Derecesi m olan B-ağacında:

  • Bir düğüm en çok m çocuk ve m − 1 anahtar tutar.
  • Kök dışındaki iç düğümlerde en az ⌈m/2⌉ çocuk bulunur.
  • Düğüm içindeki anahtarlar sıralıdır. k anahtarlı bir iç düğümün k + 1 çocuğu vardır. Ardışık iki anahtar arasındaki çocukta, değeri bu iki anahtarın arasında kalan anahtarlar bulunur.
  • Bütün yapraklar aynı derinliktedir.

Düğüm içindeki anahtarlar, çocukların arasındaki sınırlar gibi çalışır. Şekil 10.39'daki kök [20, 40] üç çocuğa yön verir: 20'den küçükler sol çocukta, 20 ile 40 arasındakiler ortada, 40'tan büyükler sağdadır. Bu, BST özelliğinin çok yollu genellemesidir. İkili arama ağacı, derecesi 2 olan bir B-ağacı gibi düşünülebilir; ama B-ağacının asıl gücü, bütün yaprakların aynı derinlikte olması kuralından gelir. Bu kural ağacı her zaman mükemmel biçimde dengeli tutar.

2040510253035506030 aranıyor: 20 < 30 < 40 → orta çocuk
Şekil 10.39: Derecesi 4 olan bir B-ağacı: kök [20, 40], yapraklar [5, 10], [25, 30, 35], [50, 60]. 30 aranırken kökte 20 < 30 < 40 olduğu için orta çocuğa inilir.

Neden böyle bir yapı? Bir düğüm bir disk bloğuna denk getirilir. Bir blokta yüzlerce anahtar tutulabilir; derecesi örneğin 200 olan bir B-ağacında her düğüm 200'e kadar çocuğa yön verir. Yüksekliği 3 olan böyle bir ağaç 2004, yani bir milyardan fazla anahtar tutabilir. Bir arama dört blok okur; kök genellikle bellekte tutulduğu için diske gidilen sayı daha da azdır. Geniş ve alçak ağaç az disk erişimi demektir. Bu yüzden veritabanı ve dosya sistemi indislerinin temeli B-ağacıdır. Yapıyı 1972'de Bayer ve McCreight tanıttı.

Derecesi 4 olan B-ağacına 2-3-4 ağacı da denir; düğümleri 2, 3 ya da 4 çocukludur. 2-3-4 ağacı kırmızı-siyah ağacın eşdeğeridir. Kırmızı-siyah ağaçta her siyah düğüm kırmızı çocuklarıyla tek bir düğümde toplanırsa bir 2-3-4 ağacı elde edilir. Kırmızı-siyah ağacın “eşit siyah yükseklik” kuralı, 2-3-4 ağacının “bütün yapraklar aynı derinlikte” kuralının ta kendisidir.

10.7.2B-ağacında arama ve ekleme

Arama BST aramasının genellemesidir. Kökte, düğümün anahtarları sırayla aranan anahtarla karşılaştırılır. Anahtar bulunursa arama biter. Bulunamazsa, aranan anahtarın düştüğü aralığa karşılık gelen çocuğa inilir. Her düzeyde yalnız bir düğüm, yani bir disk bloğu okunur. Maliyet yükseklikle orantılıdır: O(logm n) düğüm okuması. Düğüm içindeki arama da ikili aramayla O(log m) karşılaştırmada yapılabilir; toplam karşılaştırma sayısı O(log n)'dir.

Ekleme her zaman bir yaprakta başlar. Anahtar için aynı arama yolu izlenir ve anahtar yaprakta sıralı yerine konur. Yaprakta yer varsa iş biter. Yaprak taşarsa, yani m anahtara ulaşırsa, bölünür: ortanca anahtar ebeveyne çıkar, kalan anahtarlar iki ayrı düğüme ayrılır. Ebeveyne bir anahtar ve bir çocuk eklendiği için ebeveyn de taşabilir; o zaman o da bölünür. Bölünme köke kadar yayılabilir. Kök bölünürse yeni bir kök oluşur ve ağacın yüksekliği bir artar.

51025303550602040kök: 20 < 30 < 40 → orta çocuğa in 51025303550602040yaprak: 25, 30 → bulundu (2 düğüm okundu)
51025303335taşma!50602040m = 4: bir düğümde en çok 3 anahtar 510253035506020334025 30 | 33 ↑ | 35
Şekil 10.40: Derecesi 4 olan B-ağacında arama ve ekleme. (a) 30 aranıyor: 20 < 30 < 40, orta çocuğa inilir. (b) 30 yaprakta bulunur; her seviyede bir düğüm okundu. (c) 33 aynı yoldan inip yaprağa sıralı yerine konur; yaprakta 4 anahtar oldu, kapasite (m − 1 = 3) aşıldı. (d) Yaprak bölünür: ortadaki 33 ebeveyne çıkar, kalanlar [25, 30] ve [35] olur. Kök artık [20, 33, 40].

Dört anahtarlı [25, 30, 33, 35] yaprağının tam bir ortancası yoktur; iki ortadaki anahtardan biri seçilmelidir. Sunumdaki örnek sağdakini (33) seçer ve sol yarıda iki, sağ yarıda bir anahtar kalır. Soldakini seçmek de geçerlidir; önemli olan seçimin tutarlı olmasıdır. Derecesi tek olan ağaçlarda bu belirsizlik doğmaz: m = 3'te taşan düğümde üç anahtar vardır ve ortanca tektir.

Bölünmenin güzel bir sonucu vardır: B-ağacı aşağıdan yukarı büyür. İkili arama ağacında yeni düğümler aşağıya eklenir ve ağaç aşağı doğru uzar. B-ağacında ise yükseklik ancak kök bölünürken, yani en üstte artar. Bütün yapraklar aynı anda bir düzey aşağı itilmiş olur; bu yüzden hepsi her zaman aynı derinlikte kalır.

Silme eklemenin tersidir. Bir düğüm en az doluluk sınırının altına düşerse kardeşinden anahtar ödünç alır ya da kardeşiyle birleşir. Birleşme ebeveynden bir anahtar eksiltir ve eksiklik yukarı taşınabilir. Kökün son anahtarı inerse kök kaldırılır ve yükseklik bir azalır. Böylece hem denge hem doluluk sınırları korunur.

10.7.3Ekleme ve bölünme adım adım

Derecesi m = 3 olan boş bir B-ağacına sırasıyla 10, 20, 30, 40, 50, 60, 70 eklensin. Bu ağaçta bir düğüm en çok 2 anahtar ve 3 çocuk tutar; 3 anahtara ulaşan düğüm bölünür.

Tablo 10.24: m = 3 olan B-ağacına 10, 20, 30, 40, 50, 60, 70 eklenmesi. Ağaç “kök{çocuklar}” biçiminde yazılmıştır.
EklemeOlaySonuç
10kök yaprağa eklenir; 1 anahtar[10]
20kök yaprağa eklenir; 2 anahtar[10, 20]
30[10, 20, 30] taşar; ortanca 20 yeni köke çıkar (kök bölünmesi, yükseklik 1)[20]{[10] [30]}
40sağ yaprağa eklenir[20]{[10] [30, 40]}
50[30, 40, 50] taşar; 40 ebeveyne çıkar; ebeveynde yer var[20, 40]{[10] [30] [50]}
60sağ yaprağa eklenir[20, 40]{[10] [30] [50, 60]}
70[50, 60, 70] taşar; 60 ebeveyne çıkar; ebeveyn [20, 40, 60] de taşar; 40 yeni köke çıkar (kök bölünmesi, yükseklik 2)[40]{[20]{[10] [30]} [60]{[50] [70]}}

Sonuçta 7 anahtar 3 seviyeye yerleşti. Her iç düğümün 2 çocuğu vardır ve bütün yapraklar aynı derinliktedir. Kök iki kez bölündü ve yükseklik yalnız bu iki anda arttı. Sıralı girdi düz BST'yi zincire çevirirdi; B-ağacı ise aynı girdiden mükemmel dengeli bir ağaç kurdu.

102030taşma! 103020
10304050taşma!20 1030502040
10305070204060taşma! 10305070206040
Şekil 10.41: Bölünmeler. (a) 30 eklenince düğüm [10, 20, 30] oldu, kapasite aşıldı. (b) Kök bölündü: 20 yeni kök. (c) 50 eklenince [30, 40, 50] taştı. (d) 40 ebeveyne çıktı; bölünme orada durdu. (e) 70 eklenince 60 ebeveyne çıktı, ama ebeveyn [20, 40, 60] taştı. (f) Kök bölündü: 40 yeni kök; yükseklik bir arttı ve bütün yapraklar yine aynı derinlikte.
Sık yapılan hata

Taşan yaprağın altına yeni bir yaprak eklemeye çalışmak. B-ağacında yeni düğüm aşağıya değil, yana açılır: bölünme taşan düğümü iki kardeşe ayırır ve ortanca anahtarı yukarı gönderir. Ağaç ancak kök bölünürken uzar. Yaprakların altına düğüm eklemek “bütün yapraklar aynı derinlikte” kuralını bozar.

10.7.4B+ ağacı

B+ ağacı, B-ağacının veritabanlarında en çok kullanılan çeşididir. İki yapı arasında üç fark vardır:

  • Veri yapraklarda: Bütün anahtarlar ve kayıtlar yapraklarda bulunur. İç düğümler yalnız yönlendirme anahtarları tutar; bu anahtarların kopyası yapraklarda da vardır.
  • Bağlı yapraklar: Yapraklar soldan sağa bir bağlı liste gibi birbirine bağlıdır. “20 ile 50 arası” gibi aralık sorguları bu zincir üzerinde hızla yürür.
  • Daha geniş iç düğümler: İç düğümler kayıt taşımadığı için bir bloğa daha çok anahtar sığar. Derece büyür, ağaç alçalır.
2040510202530405060yapraklar bağlı liste: aralık sorgusu soldan sağa okunur
Şekil 10.42: B+ ağacı: kökte 20 ve 40 yönlendirme anahtarları; yapraklarda [5, 10], [20, 25, 30], [40, 50, 60]. Yapraklar soldan sağa bağlıdır; aralık sorgusu bu zinciri izler. 20 ve 40 hem kökte hem yapraklarda bulunur.

Arama her zaman bir yaprakta biter, çünkü kayıt yalnız orada vardır. B-ağacında aranan anahtar bir iç düğümde bulunursa arama erken biterdi; B+ ağacında bu kısa yol yoktur. Buna karşılık her arama aynı sayıda düzey iner ve maliyet tahmin edilebilir. Çoğu ilişkisel veritabanı yönetim sisteminde tablo indisleri B+ ağacıdır.

10.7.5B+ ağacında aralık sorgusu

Şekil 10.42'deki ağaçta 10 ile 45 arasındaki anahtarlar istensin.

  1. Kökten başlanır: 10 < 20 olduğu için alt sınır 10'u içeren yaprak en soldaki çocuktadır. Bu iniş bir aramadır ve O(log n) düzey sürer.
  2. İlk yaprak [5, 10]: 5 < 10 olduğu için atlanır, 10 alınır. Yaprağın sonuna gelindi; yukarı çıkılmadan sonraki yaprak bağına geçilir.
  3. İkinci yaprak [20, 25, 30]: üçü de aralıkta, alınır. Yine bağ izlenir.
  4. Üçüncü yaprak [40, 50, 60]: 40 alınır; 50 > 45 olduğu için durulur.

Sonuç 10 20 25 30 40'tır ve sıralıdır. Maliyet, bir kez aşağı inmek (O(log n)) ile okunan yaprakların toplamıdır: O(log n + k), burada k sonuç sayısıdır. B-ağacında aynı sorgu, anahtarlar iç düğümlere de dağıldığı için ağaçta tekrar tekrar inip çıkmayı gerektirirdi. B+ ağacındaki yaprak zinciri bu gidiş gelişi ortadan kaldırır.

2040510202530405060sorgu: 10 ≤ anahtar ≤ 45sonuç: 10 2040510202530405060sorgu: 10 ≤ anahtar ≤ 45sonuç: 10 20 25 30
2040510202530405060sorgu: 10 ≤ anahtar ≤ 45sonuç: 10 20 25 30 40 2040510202530405060sorgu: 10 ≤ anahtar ≤ 45sonuç: 10 20 25 30 40
Şekil 10.43: B+ ağacında 10 ile 45 arası aralık sorgusu. (a) İlk yaprak: 10 alındı. (b) İkinci yaprak: 20, 25, 30 alındı. (c) Üçüncü yaprak: 40 alındı, 50 > 45 olduğu için duruldu. (d) Sonuç 10 20 25 30 40; maliyet O(log n + k).
Örnek 10.8

Bir öğrenci kayıt sisteminde 2 milyon kayıt, öğrenci numarasına göre derecesi 100 olan bir B+ ağacında tutulsun. İç düğümler en az yarı dolu olduğu için her iç düğüm en az 50 çocuğa yön verir. Yükseklik en çok ⌈log50(2 · 106)⌉ civarındadır: 503 = 125.000, 504 = 6.250.000; yani en çok 4 düzey. Bir öğrenciyi bulmak en çok 4 blok okur. Numarası 2020001 ile 2020150 arasındaki 150 öğrenciyi listelemek ise bir iniş ve birkaç yaprak okumasıyla yapılır. Aynı kayıtlar dengeli bir ikili ağaçta tutulsaydı yükseklik en az 20 olurdu.

10.8Sık yapılan hatalar

Arama ağacı kodundaki hataların çoğu birkaç tipe ayrılır. Not boyunca konuların yanında verilen hatalar burada bir arada toplanmıştır; bir ağaç kodunu gözden geçirirken kontrol listesi olarak kullanılabilir.

Tablo 10.25: Arama ağacı kodunda sık görülen hatalar.
HataNe olur?Doğrusu
Dönüş değerini atamamaksil(kok, 40); kök silinince ya da boş ağaca eklenince ağaç bozulur.kok = sil(kok, 40); ve kok = ekle(kok, 5);
Yalnız çocuklara bakmakEbeveyn–çocuk denetimi BST olmayan ağaçları geçirir.İzinli (min, max) aralığı kökten aşağı taşınır (bstMi).
Nesneleri == ile karşılaştırmakReferanslar karşılaştırılır; ağaçta olan anahtar bulunamaz.Sıra için compareTo, eşitlik için equals ya da compareTo(...) == 0.
Yükseklik güncelleme sırasıAVL döndürmesinden sonra yanlış denge faktörleri, gereksiz döndürmeler.Önce aşağı inen düğüm, sonra yeni kök güncellenir.
“BST her zaman O(log n)”Sıralı girdide h = n − 1, işlemler O(n).O(log n) yalnız dengeli ağaçlarda (AVL, kırmızı-siyah) garantidir.
null çocuğu unutmakNullPointerException ya da yanlış denge ve renk kararları.Boş alt ağacın yüksekliği −1, kırmızı-siyahta NIL siyahtır; h(null), kirmizi(null) gibi yardımcılar bu yüzden vardır.
Çift döndürme yerine tek döndürmeLR ya da RL durumunda ağaç ters yöne eğilir ve dengesiz kalır.Çocuğun denge faktörüne bakılır; zikzakta önce çocukta döndürülür.
Zig-zig sırasıSplay'in amortize garantisi kaybolur.Önce dede, sonra ebeveyn döndürülür.

Bu hataların arkasında iki ortak düşünce yanlışı vardır. Birincisi, ağacı yalnız “yerel” görmektir: bir düğümü yalnız ebeveyni ve çocuklarıyla düşünmek. Oysa BST özelliği, AVL koşulu ve siyah yükseklik kuralı bütün alt ağaçla ilgili koşullardır. İkincisi, Java'nın referans ve değer ayrımını unutmaktır. Ağaç metotları alt ağacın yeni kökünü döndürür ve çağıran bu değeri bir bağa ya da değişkene yazmadıkça hiçbir şey değişmez.

10.9Özet

  • Arama ağacı, öğeleri anahtarlarına göre sıralı tutar; arama, ekleme ve silme kökten tek bir yol izler ve O(h) sürer. n düğümlü ağaçta h, en iyi durumda yaklaşık log2 n, en kötü durumda n − 1'dir.
  • BST özelliği: her düğümde sol alt ağacın bütün anahtarları küçük, sağ alt ağacın bütün anahtarları büyüktür. Özellik bütün alt ağaç için geçerlidir. Kök ortada gezinme anahtarları sıralı verir ve O(n) sürer. En küçük anahtar en soldaki, en büyük en sağdaki düğümdedir.
  • Ekleme aranıyormuş gibi iner ve düştüğü boş bağa yeni bir yaprak bağlar. Metotlar alt ağacın yeni kökünü döndürür; kullanım kok = ekle(kok, x) biçimindedir.
  • Ardıl x'ten büyük en küçük, öncül x'ten küçük en büyük anahtardır. Tavan ve taban eşitliği de kabul eder. Hepsi O(h)'dir.
  • Silme üç durumludur: yaprak kaldırılır; tek çocuklu düğüm atlanır; iki çocuklu düğümün değeri ardılıyla (ya da öncülüyle) değiştirilir ve ardıl kendi yerinden silinir. Ardılın sol çocuğu olmadığı için bu ikinci silme her zaman kolay durumlardan birine düşer.
  • Aralık sorgusu budamayla O(h + s) sürer. BST doğrulama, izinli aralığı kökten aşağı taşıyarak O(n)'de yapılır.
  • Düz BST'nin şekli ekleme sırasına bağlıdır; sıralı girdi zincir verir. Rastgele sırada ortalama derinlik yaklaşık 1,39 log2 n'dir. Dengeli ağaçlar küçük yerel değişikliklerle h = O(log n) garantisi verir.
  • AVL ağacında her düğümde bf = h(sol) − h(sağ) ∈ {−1, 0, 1}'dir. En seyrek AVL ağacının düğüm sayısı Fibonacci gibi büyür ve h < 1,44 log2(n + 2)'dir. Dengesizlik LL ve RR'de tek, LR ve RL'de çift döndürmeyle düzeltilir. Döndürme O(1)'dir ve kök ortada sırayı korur.
  • Kırmızı-siyah ağaçta kök siyahtır, art arda iki kırmızı yoktur ve her yolda siyah sayısı eşittir. En uzun yol en kısanın en çok iki katıdır ve h ≤ 2 log2(n + 1)'dir. Ekleme sonrası düzeltme amcanın rengine bakar: amca kırmızıysa renk değiştirilir, siyahsa en çok iki döndürme yapılır.
  • Splay ağacı erişilen düğümü zig, zig-zig ve zig-zag adımlarıyla köke taşır. Denge bilgisi tutmaz; tek işlem O(n) olabilir, amortize maliyet O(log n)'dir.
  • B-ağacında bir düğüm en çok m − 1 anahtar ve m çocuk tutar, bütün yapraklar aynı derinliktedir. Taşan düğüm bölünür ve ortanca anahtar yukarı çıkar; ağaç aşağıdan yukarı büyür. B+ ağacında veri yalnız yapraklardadır ve yapraklar bağlıdır; aralık sorgusu O(log n + k) sürer.
Tablo 10.26: Arama ağaçlarında işlem maliyetleri. (*) Amortize; tek işlem O(n) olabilir. Bütün yapılarda kök ortada gezinme O(n)'dir ve anahtarları sıralı verir.
YapıYükseklikArama / ekleme / silmeNasıl dengelenir?
BSTlog n … nO(h)dengelenmez; en kötü O(n)
AVL≤ 1,44 log nO(log n)bf ∈ {−1, 0, 1}; LL, RR, LR, RL döndürmeleri
Kırmızı-siyah≤ 2 log(n + 1)O(log n)renk değiştirme + en çok 2–3 döndürme
SplaysınırsızO(log n)*erişilen düğüm zig / zig-zig / zig-zag ile köke
B-ağacı / B+≈ logm nO(log n)taşan düğüm bölünür; bütün yapraklar aynı derinlikte

10.10Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.

10.11Kendini Dene

  1. Boş bir BST'ye sırasıyla 35, 18, 52, 10, 25, 44, 60, 22, 48 ekleniyor. Ağacın yüksekliği nedir? Kök ortada gezinme ne verir? ara(kok, 48) kaç düğümle karşılaştırma yapar?

    Cevap

    Ağaç 35(18(10, 25(22, —)), 52(44(—, 48), 60))'tır. En derin düğümler 22 ve 48'dir (derinlik 3); yükseklik 3'tür. Kök ortada gezinme 10 18 22 25 35 44 48 52 60 verir. 48 aranırken 35 (sağa), 52 (sola), 44 (sağa) ve 48 ile karşılaştırılır: 4 düğüm, yani 48'in derinliği artı bir.

  2. Aynı ağaçtan önce 18, sonra 35 siliniyor (iki çocuklu düğümde kök ortada ardıl kullanılıyor). Her silmeden sonra ağaç nasıl olur?

    Cevap

    18'in iki çocuğu vardır. Ardıl, sağ alt ağacın en küçüğüdür: 25'e geçilir, sonra sola 22'ye inilir. 22, 18'in düğümüne kopyalanır ve eski 22 (yaprak) silinir: 35(22(10, 25), 52(44(—, 48), 60)). 35'in de iki çocuğu vardır; ardıl 52'den sola 44'tür. 44 köke kopyalanır, eski 44 tek çocuklu olduğu için atlanır ve 52'nin sol bağı 48'e yönelir: 44(22(10, 25), 52(48, 60)).

  3. 1. sorudaki ağaçta ardil(25), oncul(44), tavan(45) ve taban(21) nedir?

    Cevap

    ardil(25) = 35: 25'in sağ alt ağacı yoktur; solundan geçilen en yakın ata köktür. oncul(44) = 35: 44'ün sol alt ağacı yoktur; sağından geçilen en yakın ata köktür. tavan(45) = 48 (45 ağaçta yok, ≥ 45 olan en küçük anahtar). taban(21) = 18 (21 ağaçta yok, ≤ 21 olan en büyük anahtar).

  4. 1. sorudaki ağaçta aralik(kok, 20, 46) ne yazdırır? Hangi düğümler hiç ziyaret edilmez?

    Cevap

    Çıktı 22 25 35 44'tür. Ziyaret sırası 35, 18, 25, 22, 52, 44, 48'dir. 18'de 20 < 18 yanlış olduğu için 10 budanır. 52'de 52 < 46 yanlış olduğu için 60 budanır. 48 ziyaret edilir (44'ün sağı, çünkü 44 < 46), ama aralık dışındadır. Hiç ziyaret edilmeyenler 10 ve 60'tır.

  5. Boş bir AVL ağacına sırasıyla 30, 20, 10, 40, 50, 35 ekleniyor. Hangi eklemelerde hangi durum oluşur ve son ağaç nedir?

    Cevap

    10: bf(30) = 2, bf(20) = 1 → LL; 30'da sağa → 20(10, 30). 40: dengesizlik yok. 50: bf(30) = −2, bf(40) = −1 → RR; 30'da sola → 20(10, 40(30, 50)). 35: 30'un sağına düşer; bf(20) = −2, bf(40) = 1 → RL; 40'ta sağa, 20'de sola. Son ağaç 30(20(10, —), 40(35, 50)), yükseklik 2.

  6. Boş bir kırmızı-siyah ağaca sırasıyla 30, 20, 10, 25, 27 ekleniyor. Düzeltme gerektiren eklemelerde hangi durum uygulanır? Son ağacı renkleriyle yazın.

    Cevap

    10: ebeveyn 20 kırmızı, amca NIL, 10–20–30 çizgi → 30'da sağa döndür; 20 siyah, 30 kırmızı: 20S(10K, 30K). 25: ebeveyn 30 kırmızı, amca 10 kırmızı → renk değiştir (10 ve 30 siyah, 20 kırmızı), 20 kök olduğu için yeniden siyah. 27: ebeveyn 25 kırmızı, amca NIL; 27 sağ, 25 sol çocuk → üçgen: 25'te sola, sonra 30'da sağa döndür; 27 siyah, 30 kırmızı. Son ağaç 20S(10S, 27S(25K, 30K)); her yolda 2 siyah düğüm.

  7. Splay ağacı 50(30(20(10, —), 40), 70) olsun. splay(40) hangi adımla çalışır ve sonuç ağacı nedir?

    Cevap

    Arama yolu 50 → 30 → 40'tır; p = 30, g = 50. 40, 30'un sağ çocuğu; 30 ise 50'nin sol çocuğudur: zıt yön, zig-zag. Önce 30'da sola, sonra 50'de sağa döndürülür. Sonuç 40(30(20(10, —), —), 50(—, 70)). 40 tek adımda köke çıktı.

  8. Derecesi 3 olan boş bir B-ağacına sırasıyla 5, 15, 25, 35, 45 ekleniyor. Hangi eklemelerde bölünme olur ve son ağaç nedir?

    Cevap

    25 eklenince [5, 15, 25] taşar; 15 yeni köke çıkar: [15]{[5] [25]}. 35 sağ yaprağa eklenir: [25, 35]. 45 eklenince [25, 35, 45] taşar; 35 köke çıkar ve kökte yer vardır: [15, 35]{[5] [25] [45]}. İki bölünme olur, kök bir kez bölünür; yükseklik 1'dir.

  9. Kökünün siyah yüksekliği 3 olan bir kırmızı-siyah ağaçta kökten bir NIL'e giden en kısa ve en uzun yol en çok kaç düğüm içerebilir? Böyle bir ağaçta en az kaç düğüm vardır?

    Cevap

    Her yolda 3 siyah düğüm vardır. En kısa yol yalnız siyahlardan oluşur: 3 düğüm. En uzun yolda her siyahın altına bir kırmızı girebilir: en çok 6 düğüm (kök siyah olduğu için siyah, kırmızı, siyah, kırmızı, siyah, kırmızı). En az düğüm sayısı 23 − 1 = 7'dir: bütün düğümleri siyah olan, yüksekliği 2 olan mükemmel ağaç.

  10. Şekil 10.42'deki B+ ağacında 22 ile 55 arasındaki anahtarlar isteniyor. Sonuç nedir ve kaç yaprak okunur?

    Cevap

    Kökte 20 ≤ 22 < 40 olduğu için orta çocuğa, [20, 25, 30] yaprağına inilir. 20 atlanır; 25 ve 30 alınır. Bağ izlenerek [40, 50, 60] yaprağına geçilir; 40 ve 50 alınır, 60 > 55 olduğu için durulur. Sonuç 25 30 40 50'dir; 2 yaprak okunur ve köke bir daha çıkılmaz.

10.12İleri okuma

  • M. T. Goodrich, R. Tamassia ve M. H. Goldwasser, Data Structures and Algorithms in Java (6. baskı, 2014), Bölüm 11: ikili arama ağaçlarını, AVL, splay, (2, 4) ve kırmızı-siyah ağaçları aynı sırayla işler. Kodları bu nottakilere yakın bir Java diliyle yazılmıştır.
  • R. Sedgewick ve K. Wayne, Algorithms (4. baskı, 2011), Kesim 3.2 ve 3.3: BST'yi ve sola yatık kırmızı-siyah ağacı 2-3 ağaçlarıyla ilişkilendirerek anlatır; kırmızı-siyah ağacın neden çalıştığını görmenin en kolay yoludur.
  • T. H. Cormen, C. E. Leiserson, R. L. Rivest ve C. Stein, Introduction to Algorithms (4. baskı, 2022), Bölüm 12, 13 ve 18: BST, kırmızı-siyah ağaç (silme dahil bütün durumlarıyla) ve B-ağaçlarını ispatlarıyla verir.
  • D. D. Sleator ve R. E. Tarjan, “Self-Adjusting Binary Search Trees” (1985): splay ağacını tanıtan özgün makaledir; amortize O(log n) sınırının potansiyel fonksiyonuyla ispatını içerir.

10.13Kaynaklar

  1. G. M. Adelson-Velsky, E. M. Landis, “An algorithm for the organization of information”, Soviet Mathematics Doklady, 3, 1259–1263, 1962.
  2. R. Bayer, E. M. McCreight, “Organization and maintenance of large ordered indexes”, Acta Informatica, 1(3), 173–189, 1972.
  3. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022.
  4. M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6. baskı, Wiley, 2014.
  5. L. J. Guibas, R. Sedgewick, “A dichromatic framework for balanced trees”, 19th Annual Symposium on Foundations of Computer Science, 8–21, 1978.
  6. R. Sedgewick, K. Wayne, Algorithms, 4. baskı, Addison-Wesley, 2011.
  7. D. D. Sleator, R. E. Tarjan, “Self-adjusting binary search trees”, Journal of the ACM, 32(3), 652–686, 1985.

İlgili sayfalar: Bölüm 10 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.