SEN2212 · 4. hafta · Implementing binary search tree using Java.

BST'yi Java ile yazmak

Generic BinarySearchTree sınıfı: düğüm sınıfı, özyinelemeli insert/remove/contains/findMin/findMax, gezinme, size ve height; sınavda istenen 'write a method' kalıpları.

generic classComparablecompareTonested classBinaryNoderecursive helperpublic driver methodinsertremovecontainsfindMinfindMaxsizeheightrange countkth smallestfloorlowest common ancestor

Geçen hafta BST’yi kâğıt üzerinde işledin. Bu hafta aynı algoritmaları Java’ya döküyoruz. Sınavda iki tip soru gelir: verilen kodun çıktısı (insert/remove çağrıları sonrası gezinme) ve “write a method”: size, height, aralıktaki eleman sayısı, k. en küçük, BST kontrolü gibi kısa özyinelemeli metotlar. Bu sayfadaki kalıpları bilen biri yeni bir metodu da 5 dakikada yazar.

Üç altın kural:

  1. Özyinelemeli metot düğüm döndürür, çağıran atar: t.left = insert(x, t.left);. Java’da referans değerle geçer.
  2. Her metot önce t == null durumunu çözer. Temel durum (base case) yanlışsa gerisi çöker.
  3. BST’de bir tarafı budayabiliyorsan buda. Arama, aralık ve floor gibi işlemler O(h), tüm ağacı gezen işlemler O(n).

Sınıfın iskeleti

Anahtar tipi generic. Karşılaştırma < ile değil compareTo ile yapılır, bu yüzden tip Comparable olmalı.

public class BinarySearchTree<T extends Comparable<? super T>> {

    private static class BinaryNode<T> {
        T element;
        BinaryNode<T> left, right;
        BinaryNode(T e) { element = e; }
    }

    private BinaryNode<T> root;   // boş ağaçta null

    public boolean isEmpty() { return root == null; }
    public void makeEmpty()  { root = null; }
}
  • BinaryNode private static nested class: dışarıdan görünmez, ağaç nesnesine bağlı değil.
  • <? super T>: T’nin kendisi değil de üst sınıfı Comparable olsa da kabul et demek. Ezberlemen yeterli.
  • makeEmpty sadece kökü null yapar. Düğümleri çöp toplayıcı (garbage collector) temizler.

Public sürücü, private yardımcı

Her işlem iki metottan oluşur. Public olan kullanıcıya açık ve sadece kökü verir. Private olan asıl özyinelemeyi yapar ve bir alt ağacın kökünü parametre alır.

public void insert(T x)      { root = insert(x, root); }
public void remove(T x)      { root = remove(x, root); }
public boolean contains(T x) { return contains(x, root); }

Kullanıcı düğüm kavramını hiç görmez. Kökün değişebileceği işlemlerde (ilk ekleme, kökü silme) root = ... ataması şart.

contains, findMin, findMax

private boolean contains(T x, BinaryNode<T> t) {
    if (t == null) return false;
    int c = x.compareTo(t.element);
    if (c < 0) return contains(x, t.left);
    if (c > 0) return contains(x, t.right);
    return true;                         // c == 0: bulundu
}

private BinaryNode<T> findMin(BinaryNode<T> t) {
    if (t.left == null) return t;        // özyinelemeli
    return findMin(t.left);
}

private BinaryNode<T> findMax(BinaryNode<T> t) {
    while (t.right != null) t = t.right; // döngüyle
    return t;
}

compareTo negatif, sıfır ya da pozitif döner. == -1 diye kontrol etme: sözleşme sadece işaret garantiler. contains kuyruk özyinelemesi (tail recursion) olduğu için döngüye çevirmek kolay. findMin ve findMax private sürümleri düğüm döndürür, public sürümleri boş ağaçta istisna fırlatıp element döndürür.

insert

private BinaryNode<T> insert(T x, BinaryNode<T> t) {
    if (t == null) return new BinaryNode<>(x);   // yeni yaprak
    int c = x.compareTo(t.element);
    if (c < 0)      t.left  = insert(x, t.left);
    else if (c > 0) t.right = insert(x, t.right);
    // c == 0: tekrar, bir şey yapma
    return t;
}

Mantığı adım adım: insert(25, kök) çağrısı 40’tan 20’ye, 20’den 30’a iner. 30’un solu null, insert(25, null) yeni düğüm döndürür ve 30.left = yeni düğüm ataması yapılır. Yukarı dönerken diğer atamalar (20.right = 30, 40.left = 20) zaten olan bağlantıları yeniden yazar, bir şey bozmaz.

TUZAK · atamasız özyineleme. insert(x, t.left); yazıp dönen değeri atamazsan yeni düğüm havada kalır. Metodun içinde t = new BinaryNode<>(x) yapmak çağırandaki referansı değiştirmez. Sonuç: ağaçta sadece kök olur.

remove

private BinaryNode<T> remove(T x, BinaryNode<T> t) {
    if (t == null) return null;                  // yok: bir şey yapma
    int c = x.compareTo(t.element);
    if (c < 0)      t.left  = remove(x, t.left);
    else if (c > 0) t.right = remove(x, t.right);
    else if (t.left != null && t.right != null) {        // iki çocuk
        t.element = findMin(t.right).element;
        t.right = remove(t.element, t.right);
    } else {                                     // 0 ya da 1 çocuk
        t = (t.left != null) ? t.left : t.right;
    }
    return t;
}

Son satırdaki üçlü ifade iki durumu birden çözer. Yaprakta iki çocuk da null, t null olur ve ebeveynin referansı null’a atanır. Tek çocukta t o çocuğa döner ve ebeveyn doğrudan ona bağlanır.

İki çocuk durumunda sağ alt ağaçta ikinci bir arama yapılıyor. Toplam yine O(h)O(h): bir kez aşağı inip successor’ı bulmak, bir kez de onu silmek.

Gezinmeler, size, height

private void inorder(BinaryNode<T> t, StringBuilder sb) {
    if (t == null) return;
    inorder(t.left, sb);
    sb.append(t.element).append(' ');
    inorder(t.right, sb);
}

private int size(BinaryNode<T> t) {
    return t == null ? 0 : 1 + size(t.left) + size(t.right);
}

private int height(BinaryNode<T> t) {
    return t == null ? -1 : 1 + Math.max(height(t.left), height(t.right));
}

size her çağrıda tüm ağacı gezdiği için O(n)O(n). Sık çağrılıyorsa sınıfa bir int size alanı koyup insert ve remove’da güncellemek O(1)O(1) verir. Ama o zaman insert’in gerçekten eklenip eklenmediğini (tekrar mı) bilmen gerekir.

Çalışan örnek

BinarySearchTree<Integer> bst = new BinarySearchTree<>();
int[] keys = {40, 20, 60, 10, 30, 50, 70, 25};
for (int k : keys) bst.insert(k);
System.out.println("inorder  : " + bst.inorder());
System.out.println("preorder : " + bst.preorder());
System.out.println("postorder: " + bst.postorder());
System.out.println("size=" + bst.size() + " height=" + bst.height()
        + " min=" + bst.findMin() + " max=" + bst.findMax());
System.out.println("contains 25? " + bst.contains(25)
        + ", contains 35? " + bst.contains(35));
bst.remove(20);
System.out.println("after remove(20): " + bst.preorder());
inorder  : 10 20 25 30 40 50 60 70
preorder : 40 20 10 30 25 60 50 70
postorder: 10 25 30 20 50 70 60 40
size=8 height=3 min=10 max=70
contains 25? true, contains 35? false
after remove(20): 40 25 10 30 60 50 70
  ekleme sonrası       remove(20) sonrası
       40                    40
     /    \                /    \
   20      60            25      60
  /  \    /  \          /  \    /  \
10   30  50   70      10   30  50   70
     /
   25

20’nin successor’ı sağ alt ağacın en küçüğü, yani 25. Aynı sınıf String ile de çalışır: "mango kiwi apple peach banana" eklenince inorder apple banana kiwi mango peach çıkar, çünkü String’in compareTo’su alfabetik.

Write-a-method kalıpları

Sınavda istenen metotların çoğu üç kalıptan birine uyar.

KalıpNe zamanİskeletSüre
Tüm ağacı toplasize, toplam, yaprak sayısı, tek çocuklu sayısınull → 0; kendi katkın + f(L) + f(R)O(n)O(n)
Tek yol boyunca incontains, floor, ceiling, depth, LCAwhile (t != null) { karşılaştır, sola ya da sağa }O(h)O(h)
Budayarak gezaralık sayma/yazdırma, k. elemankoşula göre bir tarafı atlaO(h+k)O(h + k)

Bu sayfanın sorularında kullanılan örnek ağaç (50, 30, 70, 20, 40, 60, 80, 35, 45, 65 sırasıyla eklendi):

            50
          /    \
        30      70
       /  \    /  \
     20   40  60   80
         / \    \
       35  45    65

İPUCU · önce boş ağacı ve tek düğümü düşün. Yazdığın metodu null ve tek yaprakla kafanda çalıştır. Bu iki durum doğruysa özyineleme genelde gerisini halleder.

Sık yapılan hatalar

YanlışDoğrusu
insert(x, t.left);t.left = insert(x, t.left);
insert(x) içinde insert(x, root);root = insert(x, root); (ilk ekleme kökü değiştirir)
if (x < t.element) generic tiptex.compareTo(t.element) < 0
compareTo(...) == -1< 0; sadece işaret garantili
height’ta null → 0Bu derste null → -1, yaprak 0
isBST’de sadece çocukları kontrol etmek(lo, hi) aralığını özyinelemeyle taşı
remove’da successor’ı silmeyi unutmakt.right = remove(t.element, t.right);
Boş ağaçta findMin() null döndürür sanmakroot.left erişimi NullPointerException; public metot önce kontrol etmeli

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

What is printed? (BinarySearchTree is the class on this page; duplicates are ignored.)

BinarySearchTree<Integer> t = new BinarySearchTree<>();
for (int x : new int[]{33, 18, 47, 9, 25, 40, 52, 18}) t.insert(x);
System.out.println(t.findMin() + " " + t.findMax() + " " + t.size());
Cevabı göster
9 52 7

8 ekleme var ama ikinci 18 zaten ağaçta: insert'te compareTo 0 döner, hiçbir şey yapılmaz. size = 7.
findMin hep sola: 33 → 18 → 9. findMax hep sağa: 33 → 47 → 52.Integer + " " String birleştirmedir, toplama değil: çıktı "9 52 7".

Soru 2orta

What is printed?

BinarySearchTree<Integer> u = new BinarySearchTree<>();
for (int x : new int[]{33, 18, 47, 9, 25, 40, 52, 21}) u.insert(x);
u.remove(18);
u.remove(47);
System.out.println(u.preorder());
System.out.println(u.height());
Cevabı göster
33 21 9 25 52 40 / 2

Ağaç: 33(18(9, 25(21, -)), 47(40, 52)).
remove(18): iki çocuk. Sağ alt ağacın min'i: 25 → 21. 18'in yerine 21 yazılır, 21 yaprağı silinir: 33(21(9, 25), 47(40, 52)).
remove(47): iki çocuk. Sağ alt ağacın min'i 52 (solu yok). 47'nin yerine 52, eski 52 silinir: 33(21(9, 25), 52(40, -)).
Preorder: 33 21 9 25 52 40. Yükseklik 2 (33 → 21 → 9).
Çıktı iki satır: 33 21 9 25 52 40 ve 2.

Soru 3orta

The keys 50, 30, 70, 20, 60, 80, 65, 10 are inserted into an empty BST. What does mystery(root) return, and what does the method compute?

static int mystery(Node t) {
    if (t == null) return 0;
    int c = ((t.left == null) != (t.right == null)) ? 1 : 0;
    return c + mystery(t.left) + mystery(t.right);
}
Cevabı göster
3; the number of nodes with exactly one child

(t.left == null) != (t.right == null) sadece çocuklardan biri null, diğeri dolu olduğunda true. Yani metot tek çocuklu düğümleri sayar.
Ağaç: 50(30(20(10, -), -), 70(60(-, 65), 80)).
Tek çocuklular: 30 (sadece 20), 20 (sadece 10), 60 (sadece 65). Sonuç 3.

Soru 4orta

This insert method has a bug. After inserting 5, 3, 8, 1 with root = badInsert(root, x);, what does size(root) return? Fix the method.

static Node badInsert(Node t, int x) {
    if (t == null) return new Node(x);
    if (x < t.element) badInsert(t.left, x);
    else if (x > t.element) badInsert(t.right, x);
    return t;
}
Cevabı göster
1; assign the results: t.left = badInsert(t.left, x) and t.right = badInsert(t.right, x)

İlk çağrıda root null, yeni düğüm (5) dönüyor ve root'a atanıyor. Sonraki çağrılarda badInsert(t.left, x) yeni düğümü oluşturup döndürüyor, ama dönen değer hiçbir yere bağlanmıyor. Java'da referans değerle geçer: metodun içinde t'ye yeni düğüm atamak çağırandaki t.left'i değiştirmez.
Ağaçta sadece 5 kalır: size 1.
Doğrusu: t.left = badInsert(t.left, x); ve t.right = badInsert(t.right, x);.

Soru 5kolay

Write a recursive method static int size(Node t) that returns the number of nodes in the tree rooted at t.

Cevabı göster
null → 0, else 1 + size(left) + size(right)

static int size(Node t) {
    if (t == null) return 0;
    return 1 + size(t.left) + size(t.right);
}
Her düğüm bir kez sayılır: O(n). Sayfadaki 10 düğümlü örnek ağaçta 10 döner.

Soru 6orta

Write static int countInRange(Node t, int lo, int hi) that returns how many keys k satisfy lo ≤ k ≤ hi in a BST. Skip subtrees that cannot contain such keys. For the sample tree on this page, what does countInRange(root, 33, 62) return?

Cevabı göster
5 (keys 35, 40, 45, 50, 60)

Düğüm lo'dan küçükse sol alt ağacındaki her şey de küçük: sadece sağa bak. hi'dan büyükse sadece sola bak. Aralıktaysa 1 say ve iki tarafa da bak.

static int countInRange(Node t, int lo, int hi) {
    if (t == null) return 0;
    if (t.element < lo) return countInRange(t.right, lo, hi);
    if (t.element > hi) return countInRange(t.left, lo, hi);
    return 1 + countInRange(t.left, lo, hi)
             + countInRange(t.right, lo, hi);
}
Örnek ağaçta 33..62 arasında 35, 40, 45, 50, 60 var: 5. Budama sayesinde süre O(h + cevap sayısı).

Soru 7zor

Write static boolean isBST(Node t) that checks the BST property correctly for every subtree. Why is checking only t.left.element < t.element < t.right.element at each node not enough?

Cevabı göster
Pass allowed (lo, hi) bounds down the recursion; the child-only check misses deeper violations

Her düğüm, atalarından gelen bir aralığa (lo, hi) düşmeli. Sola inerken üst sınır düğümün değeri olur, sağa inerken alt sınır.

static boolean isBST(Node t) {
    return isBST(t, Long.MIN_VALUE, Long.MAX_VALUE);
}
static boolean isBST(Node t, long lo, long hi) {
    if (t == null) return true;
    if (t.element <= lo || t.element >= hi) return false;
    return isBST(t.left, lo, t.element)
        && isBST(t.right, t.element, hi);
}
Sadece çocuklara bakan sürüm 50(30(-, 55), 70) ağacına true der: 30 < 55 doğru, ama 55 kökün sol alt ağacında ve 50'den büyük. Doğru sürüm false döner. long sınırlar Integer.MIN_VALUE gibi anahtarlarda taşmayı önler.

Soru 8zor

Write static int kthSmallest(Node t, int k) that returns the k-th smallest key (k = 1 is the minimum) using an inorder traversal that stops early. For the sample tree, what is the 4th smallest key?

Cevabı göster
40

Inorder sıralı sırayla ziyaret eder. Ziyaret sayacı k'ya ulaşınca dur.

static int count, answer;
static int kthSmallest(Node t, int k) {
    count = 0; answer = -1;
    kth(t, k);
    return answer;
}
static void kth(Node t, int k) {
    if (t == null || count >= k) return;
    kth(t.left, k);
    if (++count == k) { answer = t.element; return; }
    kth(t.right, k);
}
Örnek ağacın inorder'ı 20 30 35 40 45 ...: 4. eleman 40. Süre O(h + k).

Soru 9orta

Write an iterative method static Integer floor(Node t, int x) that returns the largest key ≤ x, or null if none exists. For the sample tree, what are floor(root, 58) and floor(root, 15)?

Cevabı göster
50 and null

Düğüm x'e eşitse cevap o. Düğüm x'ten küçükse bu bir aday, daha büyük aday sağda olabilir. Büyükse sola git.

static Integer floor(Node t, int x) {
    Integer best = null;
    while (t != null) {
        if (t.element == x) return x;
        if (t.element < x) { best = t.element; t = t.right; }
        else t = t.left;
    }
    return best;
}
58: 50 < 58 aday 50, sağa 70; 70 > 58 sola 60; 60 > 58 sola git, 60'ın solu null: cevap 50.
15: 50, 30, 20 hepsi büyük, sola gidip null'a düşer, aday yok: null. Süre O(h).

Soru 10zor

Write static Node lca(Node t, int a, int b) that returns the lowest common ancestor of keys a and b (both present, a < b) in a BST. In the sample tree, what are lca(35, 45) and lca(35, 65)?

Cevabı göster
40 and 50

İkisi de düğümden küçükse ortak ata solda, ikisi de büyükse sağda. Biri solda biri sağdaysa (ya da biri düğümün kendisiyse) ayrılma noktası bu düğüm.

static Node lca(Node t, int a, int b) {
    while (t != null) {
        if (a < t.element && b < t.element) t = t.left;
        else if (a > t.element && b > t.element) t = t.right;
        else return t;
    }
    return null;
}
35 ve 45: 50'den küçük, 30'dan büyük, 40'ta ayrılıyorlar. 35 ve 65: kökte ayrılıyorlar, 50. O(h).

Soru 11kolay

What is the time complexity of (a) printing all keys of a BST in sorted order, and (b) building a BST by inserting n keys that arrive already sorted?

Cevabı göster
(a) O(n); (b) O(n^2)

(a) Inorder her düğümü bir kez ziyaret eder: O(n), ağacın şekli ne olursa olsun.
(b) i. anahtar eklenirken ağaç i düğümlük bir sağa zincir, i karşılaştırma yapılır. Toplam 0 + 1 + ... + (n-1) = n(n-1)/2. Program n = 100 için 4950 karşılaştırma saydı. O(n^2).
Not: sıralı veriden dengeli BST istiyorsan ortadaki elemanı kök yapıp iki yarıyı özyinelemeli kurarsın: O(n).

Doğrulama: Gövdedeki BinarySearchTree sınıfı ve sorulardaki tüm metotlar verify/SEN2212/W04.java içinde birebir yazılıp çalıştırıldı (java W04.java). Çıktı sorularının cevapları programın çıktısı. size, isBST, countInRange, kthSmallest, floor, lca ve generic remove 2000 rastgele ağaçta java.util.TreeSet ile karşılaştırıldı.

Deftere not kartı

class BinarySearchTree<T extends Comparable<? super T>> → compareTo ile karşılaştır. BinaryNode: element, left, right. Private static nested class. Public driver: insert(x) { root = insert(x, root); } private özyinelemeli yardımcı. ÖZYİNELEMELİ EKLEME: t == null → new node döndür. t.left = insert(x, t.left) (ATAMA ŞART). remove iki çocuk: t.element = findMin(t.right).element; t.right = remove(t.element, t.right). remove tek/sıfır çocuk: t = (t.left != null) ? t.left : t.right. size: null → 0, 1 + size(L) + size(R). height: null → -1, 1 + max(h(L), h(R)). isBST: (lo, hi) aralığı taşı, sadece çocuklara bakmak YETMEZ. countInRange: düğüm < lo → sadece sağ; > hi → sadece sol; arada → 1 + iki taraf. kth smallest: inorder + sayaç. floor: küçükse aday al, sağa git. LCA: ikisi küçük → sol, ikisi büyük → sağ, yoksa bu düğüm. Hepsi O(h); size/height/gezinme O(n).