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ı.
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:
- Özyinelemeli metot düğüm döndürür, çağıran atar:
t.left = insert(x, t.left);. Java’da referans değerle geçer. - Her metot önce
t == nulldurumunu çözer. Temel durum (base case) yanlışsa gerisi çöker. - 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; }
}
BinaryNodeprivate 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.makeEmptysadece 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çindet = 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 : 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 . Sık çağrılıyorsa sınıfa bir int size alanı koyup insert ve remove’da güncellemek 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ıp | Ne zaman | İskelet | Süre |
|---|---|---|---|
| Tüm ağacı topla | size, toplam, yaprak sayısı, tek çocuklu sayısı | null → 0; kendi katkın + f(L) + f(R) | |
| Tek yol boyunca in | contains, floor, ceiling, depth, LCA | while (t != null) { karşılaştır, sola ya da sağa } | |
| Budayarak gez | aralık sayma/yazdırma, k. eleman | koşula göre bir tarafı atla |
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
nullve 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 tipte | x.compareTo(t.element) < 0 |
compareTo(...) == -1 | < 0; sadece işaret garantili |
height’ta null → 0 | Bu derste null → -1, yaprak 0 |
| isBST’de sadece çocukları kontrol etmek | (lo, hi) aralığını özyinelemeyle taşı |
| remove’da successor’ı silmeyi unutmak | t.right = remove(t.element, t.right); |
Boş ağaçta findMin() null döndürür sanmak | root.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ç.
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
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".
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
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.
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
(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.
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
İ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);.
Write a recursive method static int size(Node t) that returns the number of nodes in the tree rooted at t.
Cevabı göster
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.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
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ı).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
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.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
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).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
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).
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
İ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).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) 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ı.