Chapter 10: Search Trees
Introduction
Learning Outcomes
Carry out search, insertion, and deletion in a binary search tree.
Explain how the height of a tree drives the cost of its operations.
Read AVL rotations and apply the red-black tree rules.
Choose among BSTs, AVL, red-black, splay, and B-trees for a given use.
Search Trees
Search Trees
- Each node holds an item, and each item has a key.
- Items are stored in order of their keys.
- A search starts at the root and follows a single branch at each node.
- The cost is proportional to the height h of the tree: O(h).
- With n nodes, h is ≈ log2 n at best and n − 1 at worst.
Search Trees
Basic Operations
Also: the smallest / largest key, a key's successor / predecessor, and range queries. All O(h).
Search Trees
Kinds of Search Trees
Search Trees
Binary Search Trees
For every node x: all keys in the left subtree < x.key < all keys in the right subtree.
- The property covers the whole subtree, not just the children: on the right, 7 > 4 holds, but 7 cannot sit left of 6.
- Duplicate keys are rejected or always sent to one side (say, the right).
Search Trees
Minimum, Maximum, and Sorted Traversal
1 2 3 4 6 8// Precondition: node != null BinaryTreeNode<E> minValue( BinaryTreeNode<E> node) { while (node.leftChild != null) node = node.leftChild; // go left return node; // the smallest } // maxValue: the same, with rightChild void inorder(BinaryTreeNode<E> d) { if (d == null) return; inorder(d.leftChild); // left System.out.print(d.data + " "); inorder(d.rightChild); // right }
Search Trees · Step through the code
Code Trace: minValue and maxValue
BinaryTreeNode<E> minValue(
BinaryTreeNode<E> node) {
while (node.leftChild != null)
node = node.leftChild; // go left
return node; // the smallest
}
BinaryTreeNode<E> maxValue(
BinaryTreeNode<E> node) {
while (node.rightChild != null)
node = node.rightChild; // go right
return node; // the largest
}
minValue(root): node starts at the root (6). Precondition: the tree is not empty.
node.leftChild = 2 ≠ null → keep looping.
node = node.leftChild → 2. Moving left.
node.leftChild = 1 ≠ null → keep looping.
node = node.leftChild → 1. Moving left.
node.leftChild == null → the loop ends. 1 has no left child.
return 1: the smallest key. (3 is deeper but lies left of 4; the minimum is always the leftmost node.)
maxValue(root): node starts at the root (6). Precondition: the tree is not empty.
node.rightChild = 8 ≠ null → keep looping.
node = node.rightChild → 8. Moving right.
node.rightChild == null → the loop ends. 8 has no right child.
return 8: the largest key. No comparisons, just links followed: O(h).
Search Trees · Step through the code
Code Trace: inorder(root)
inorder(6): d ≠ null. Left subtree first: inorder(2); 6 waits.
inorder(2): d ≠ null. Left subtree first: inorder(1); 2 waits.
inorder(1): no left child; inorder(null) returns at line 2.
Left side done → print 1. (Every key on its left is smaller.)
No right child → the frame closes; inorder(2) resumes.
Left side done → print 2. (Every key on its left is smaller.)
Now the right subtree: inorder(4).
inorder(4): d ≠ null. Left subtree first: inorder(3); 4 waits.
inorder(3): no left child; inorder(null) returns at line 2.
Left side done → print 3. (Every key on its left is smaller.)
No right child → the frame closes; inorder(4) resumes.
Left side done → print 4. (Every key on its left is smaller.)
No right child → the frame closes; inorder(2) resumes.
2's right subtree is done too → inorder(2) closes, and its caller resumes.
Left side done → print 6. (Every key on its left is smaller.)
Now the right subtree: inorder(8).
inorder(8): no left child; inorder(null) returns at line 2.
Left side done → print 8. (Every key on its left is smaller.)
No right child → the frame closes; inorder(6) resumes.
All calls done; the output is sorted: 1 2 3 4 6 8. Each node printed once → O(n).
void inorder(BinaryTreeNode<E> d) { if (d == null) return; inorder(d.leftChild); // left System.out.print(d.data + " "); inorder(d.rightChild); // right }
Search Trees
The Tree Node
class TreeNode { int data; TreeNode left, right; TreeNode(int data) { this.data = data; this.left = null; this.right = null; } }
class BinaryTreeNode<E extends Comparable<E>> { E data; // the object BinaryTreeNode<E> leftChild; // left child BinaryTreeNode<E> rightChild; // right child BinaryTreeNode(E data) { this.data = data; // children start out null } }
Keys are compared with compareTo, so E must be Comparable<E>.
BST Operations
Searching for a Key
- Start at the root; if the keys are equal, we are done.
- If smaller, continue in the left subtree; if larger, in the right.
- Reaching null means the key is not in the tree.
boolean search(BinaryTreeNode<E> root, E data) { if (root == null) return false; // not found int k = data.compareTo(root.data); if (k == 0) return true; // found else if (k < 0) return search(root.leftChild, data); else return search(root.rightChild, data); }
compareTo (or equals), not ==, which only checks whether two references are identical.BST Operations · Step through the code
Code Trace: search(root, 5) and search(root, 10)
boolean search(BinaryTreeNode<E> root, E data) { if (root == null) return false; // not found int k = data.compareTo(root.data); if (k == 0) return true; // found else if (k < 0) return search(root.leftChild, data); else return search(root.rightChild, data); }
search(root, 5): root is node 6, not null. k = compareTo = -1 since 5 < 6.
k < 0 → go left: search(root.leftChild, 5) opens a new frame (root = 4).
root ≠ null. k = compareTo = 1 since 5 > 4.
k > 0 → go right: search(root.rightChild, 5) opens a new frame (root = 5).
root ≠ null. k = compareTo = 0 since 5 = 5.
k == 0 → key found: return true. This frame closes.
search(5, 5) returned true; the frame at 4 simply returns it.
search(4, 5) returned true; the frame at 6 simply returns it.
main gets true. 3 nodes compared = depth of the found node + 1.
search(root, 10): root is node 6, not null. k = compareTo = 1 since 10 > 6.
k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = 8).
root ≠ null. k = compareTo = 1 since 10 > 8.
k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = 9).
root ≠ null. k = compareTo = 1 since 10 > 9.
k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = null).
root == null: 9 has no right child, so 10 is not in the tree → return false.
search(null, 10) returned false; the frame at 9 simply returns it.
search(9, 10) returned false; the frame at 8 simply returns it.
search(8, 10) returned false; the frame at 6 simply returns it.
Result: false, after 3 nodes. At most h + 1 nodes → O(h).
BST Operations
Inserting a Key
- Descend as if searching; put the new leaf in the null spot where you land.
- The new node is always a leaf; O(h).
BinaryTreeNode<E> insert(
BinaryTreeNode<E> root, E data) {
if (root == null)
return new BinaryTreeNode<>(data);
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = insert(root.leftChild, data);
else if (k > 0)
root.rightChild = insert(root.rightChild, data);
// k == 0: key already present, skip it
return root;
}
// usage: root = insert(root, 5);
BST Operations · Step through the code
Code Trace: insert(root, 5)
BinaryTreeNode<E> insert(
BinaryTreeNode<E> root, E data) {
if (root == null)
return new BinaryTreeNode<>(data);
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = insert(root.leftChild, data);
else if (k > 0)
root.rightChild = insert(root.rightChild, data);
// k == 0: key already present, skip it
return root;
}
// usage: root = insert(root, 5);
main calls root = insert(root, 5). Like search, insertion starts at the root.
root ≠ null (6). k = -1 since 5 < 6.
Go left: insert(root.leftChild, 5); root = 2. The result will go into root.leftChild.
root ≠ null (2). k = 1 since 5 > 2.
Go right: insert(root.rightChild, 5); root = 4. The result will go into root.rightChild.
root ≠ null (4). k = 1 since 5 > 4.
Go right: insert(root.rightChild, 5); root = null. The result will go into root.rightChild.
root == null: an empty spot. new creates node 5 and returns it; no link points to it yet.
Back in 4's frame: root.rightChild = node(5). The new node replaces the null link (orange).
return root: 4 goes back to its parent (2).
At 2, root.rightChild = node(4): same link, no change. Then return 2.
At 6, root.leftChild = node(2) (the same link), then return 6.
main: root = 6 (unchanged). A new node is always a leaf; one call per node on the path → O(h).
BST Operations
Successor and Predecessor
| Operation | Meaning | Java TreeMap | In the tree above |
|---|---|---|---|
| successor | smallest key > x | higherKey(x) | successor(5) = 6 |
| predecessor | largest key < x | lowerKey(x) | predecessor(6) = 5 |
| ceiling | smallest key ≥ x | ceilingKey(x) | ceiling(3) = 4 |
| floor | largest key ≤ x | floorKey(x) | floor(3) = 2 |
Each follows one path from the root: O(h). A deleted node with two children is replaced by its successor.
BST Operations · Step through the code
Code Trace: successor(root, 5) and successor(root, 6)
successor(root, 5): no candidate yet (candidate = null); start at the root (6).
5 < 6 → 6 is a candidate (> x); anything smaller is on the left → go left.
5 ≥ 4 → 4 and everything to its left are ruled out; larger keys are on the right → go right.
5 ≥ 5 → 5 and everything to its left are ruled out; larger keys are on the right → go right. Now root = null.
The loop ends (root == null) → return node(6): successor(5) = 6.
Second call, successor(root, 6): no candidate yet (candidate = null); start at the root (6).
6 ≥ 6 → 6 and everything to its left are ruled out; larger keys are on the right → go right.
6 < 8 → 8 is a candidate (> x); anything smaller is on the left → go left.
6 < 7 → 7 is a candidate (> x); anything smaller is on the left → go left. Now root = null.
The loop ends (root == null) → return node(7): successor(6) = 7.
TreeNode successor(TreeNode root, int x) { TreeNode candidate = null; // smallest key above x while (root != null) { if (x < root.data) { candidate = root; // root is a candidate root = root.left; // smaller ones are left } else root = root.right; // keys above x are right } return candidate; }
BST Operations
Deleting a Key: Three Cases
First find the node (O(h)). Then, by number of children:
null.delete 70Example tree
BST Operations
Deletion: A Leaf or One Child
Leaf: 70 has no children. Its parent, 60, sets its right link to null.
70 is gone; the rest of the tree is unchanged.
One child (left): 45 has only a left child, 42. (We start again from the original tree.)
42 takes 45's place as 40's right child: the parent links straight to its grandchild.
One child (right): 60 has only a right child, 70.
70 takes 60's place as 50's right child. The order is preserved.
BST Operations
Deletion: A Node with Two Children
Two children: 40 has a left (30) and a right (45) child, so it cannot simply be removed.
Inorder successor = smallest key in the right subtree: step to 45, then left all the way → 42.
The successor's value (42) is copied into 40's node. The old 42 is now a duplicate.
Delete the old 42 from the right subtree; the successor has no left child, so this is always Case 1 or 2.
Alternatively, use the inorder predecessor (largest key on the left = 30). Both give a valid BST.
BST Operations
The Deletion Code
BinaryTreeNode<E> delete(
BinaryTreeNode<E> root, E data) {
if (root == null) return null; // not found
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = delete(root.leftChild, data);
else if (k > 0)
root.rightChild = delete(root.rightChild, data);
else if (root.leftChild == null)
return root.rightChild; // leaf / right child only
else if (root.rightChild == null)
return root.leftChild; // left child only
else { // two children
root.data = minValue(root.rightChild).data;
root.rightChild = delete(root.rightChild, root.data);
}
return root;
}
root = delete(root, 40); (if the root is deleted, this assignment installs the new one).BST Operations · Step through the code
Code Trace: delete(root, 70) — a Leaf
BinaryTreeNode<E> delete(
BinaryTreeNode<E> root, E data) {
if (root == null) return null; // not found
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = delete(root.leftChild, data);
else if (k > 0)
root.rightChild = delete(root.rightChild, data);
else if (root.leftChild == null)
return root.rightChild; // leaf / right child only
else if (root.rightChild == null)
return root.leftChild; // left child only
else { // two children
root.data = minValue(root.rightChild).data;
root.rightChild = delete(root.rightChild, root.data);
}
return root;
}
main calls root = delete(root, 70). First, find the node.
root ≠ null. k = 1 since 70 > 50.
Go right: delete(root.rightChild, 70). The result will go into root.rightChild.
root ≠ null. k = 1 since 70 > 60.
Go right: delete(root.rightChild, 70). The result will go into root.rightChild.
root ≠ null. k = 0 since 70 = 70.
k = 0 → found the node to delete. No left child → return root.rightChild = null. (A leaf returns null.)
Changed link: root.rightChild = null. Nothing points to the deleted node now; the garbage collector reclaims it.
return root: 60 goes back to its parent.
root.rightChild = node(60): same link, no change. Then return root (50).
main: root = 50. 70 was a leaf, so only 60's right link changed (to null).
BST Operations · Step through the code
Code Trace: delete(root, 45) — One Child
BinaryTreeNode<E> delete(
BinaryTreeNode<E> root, E data) {
if (root == null) return null; // not found
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = delete(root.leftChild, data);
else if (k > 0)
root.rightChild = delete(root.rightChild, data);
else if (root.leftChild == null)
return root.rightChild; // leaf / right child only
else if (root.rightChild == null)
return root.leftChild; // left child only
else { // two children
root.data = minValue(root.rightChild).data;
root.rightChild = delete(root.rightChild, root.data);
}
return root;
}
main calls root = delete(root, 45). First, find the node.
root ≠ null. k = -1 since 45 < 50.
Go left: delete(root.leftChild, 45). The result will go into root.leftChild.
root ≠ null. k = 1 since 45 > 40.
Go right: delete(root.rightChild, 45). The result will go into root.rightChild.
root ≠ null. k = 0 since 45 = 45.
k = 0 → found. Left child, no right child → return root.leftChild = node(42): the only child goes to the parent.
Changed link: root.rightChild = 42. The parent bypasses the deleted node and links to its child.
return root: 40 goes back to its parent.
root.leftChild = node(40): same link, no change. Then return root (50).
main: root = 50. 45 is gone; its only child, 42, is now 40's right child. Inorder sequence preserved.
BST Operations · Step through the code
Code Trace: delete(root, 40) — Two Children
BinaryTreeNode<E> delete(
BinaryTreeNode<E> root, E data) {
if (root == null) return null; // not found
int k = data.compareTo(root.data);
if (k < 0)
root.leftChild = delete(root.leftChild, data);
else if (k > 0)
root.rightChild = delete(root.rightChild, data);
else if (root.leftChild == null)
return root.rightChild; // leaf / right child only
else if (root.rightChild == null)
return root.leftChild; // left child only
else { // two children
root.data = minValue(root.rightChild).data;
root.rightChild = delete(root.rightChild, root.data);
}
return root;
}
main calls root = delete(root, 40). First, find the node.
root ≠ null. k = -1 since 40 < 50.
Go left: delete(root.leftChild, 40). The result will go into root.leftChild.
root ≠ null. k = 0 since 40 = 40.
k = 0 → found, but it has two children (30 and 45), so it cannot simply be removed.
minValue(root.rightChild): one step right (45), then left all the way → successor = 42 (smallest on the right).
root.data = 42: the successor's value is copied into the target node. 42 now appears twice; the lower copy must go.
delete(root.rightChild, 42): delete the old 42 from the right subtree. The successor has no left child, so this is always the leaf or one-child case.
root ≠ null. k = -1 since 42 < 45.
Go left: delete(root.leftChild, 42). The result will go into root.leftChild.
root ≠ null. k = 0 since 42 = 42.
k = 0 → found the node to delete. No left child → return root.rightChild = null. (A leaf returns null.)
Changed link: root.leftChild = null. Nothing points to the deleted node now; the garbage collector reclaims it.
return root: 45 goes back to its parent.
root.rightChild = node(45) (the same link), return root. 42 now stands where the deleted 40 was.
root.leftChild = node(42): same link, no change. Then return root (50).
main: root = 50. 40 is gone and its successor 42 stands in its place; one node fewer, still a BST. Cost: O(h).
BST Operations
Analyzing BST Operations
- Search, insertion, and deletion take time proportional to the depth of the deepest node reached: O(h).
| Case | Height h | Search / Insert / Delete | Steps for n = 106 |
|---|---|---|---|
| Balanced (best) | ≈ log2 n | O(log n) | ≈ 20 |
| Random order (average) | ≈ 2.99 log2 n | O(log n) | ≈ 28 (average) |
| Degenerate (worst, sorted input) | n − 1 | O(n) | 106 |
With random insertion order, the average depth of a node is ≈ 2 ln n ≈ 1.39 log2 n (≈ 28 steps for n = 106), while the expected height is ≈ 4.31 ln n ≈ 2.99 log2 n (≈ 60).
BST Operations · Step through the code
Code Trace: the Range Query range(root, 5, 10)
range(8): 5 < 8 → the left may hold keys in range: range(k.left).
range(4): 5 < 4 is false → every key on the left is < 4 ≤ lo: left subtree pruned (never visited).
Left subtree pruned (4 ≤ lo = 5), no call made; range(4) continues. 4 is out of range, not printed. 4 < 10 → try the right too.
range(6): 5 < 6 → the left may hold keys in range: range(k.left).
range(5): 5 < 5 is false → every key on the left is < 5 ≤ lo: left subtree pruned (never visited).
Left subtree pruned (5 ≤ lo = 5), no call made; range(5) continues. 5 ∈ [5, 10] → print. 5 < 10 → try the right too.
range(null): empty subtree → return at once.
Left side done; back in range(6). 6 ∈ [5, 10] → print. 6 < 10 → try the right too.
range(7): 5 < 7 → the left may hold keys in range: range(k.left).
range(null): empty subtree → return at once.
Left side done; back in range(7). 7 ∈ [5, 10] → print. 7 < 10 → try the right too.
range(null): empty subtree → return at once.
Left side done; back in range(8). 8 ∈ [5, 10] → print. 8 < 10 → try the right too.
range(12): 5 < 12 → the left may hold keys in range: range(k.left).
range(10): 5 < 10 → the left may hold keys in range: range(k.left).
range(null): empty subtree → return at once.
Left side done; back in range(10). 10 ∈ [5, 10] → print. 10 ≥ 10 → right subtree pruned.
Left side done; back in range(12). 12 is out of range, not printed. 12 ≥ 10 → right subtree pruned.
Done. The output is in sorted (inorder) order: 5 6 7 8 10. 2 and 14 were never visited (faded). Cost: O(h + s), s = keys printed.
void range(TreeNode k, int lo, int hi) { if (k == null) return; if (lo < k.data) // candidates on the left range(k.left, lo, hi); if (lo <= k.data && k.data <= hi) System.out.print(k.data + " "); if (k.data < hi) // candidates on the right range(k.right, lo, hi); }
BST Operations · Step through the code
Code Trace: Is This Tree a BST? isBST()
isBST(6): 6 ∈ (−∞, ∞) ✓. Left subtree gets upper bound 6: isBST(k.left, −∞, 6).
isBST(2): 2 ∈ (−∞, 6) ✓. Left subtree gets upper bound 2: isBST(k.left, −∞, 2).
isBST(1): 1 ∈ (−∞, 2) ✓. Leaf: both null children return true → return true.
Left side true. Right subtree gets lower bound 2: isBST(k.right, 2, 6).
isBST(4): 4 ∈ (2, 6) ✓. Left subtree gets upper bound 4: isBST(k.left, 2, 4).
isBST(3): 3 ∈ (2, 4) ✓. Leaf: both null children return true → return true.
Left side true. Right subtree gets lower bound 4: isBST(k.right, 4, 6).
isBST(7), allowed range (4, 6): 7 ≥ 6 → return false. Right of 4, 7 must exceed 4 (✓); in 6's left subtree, it must also be less than 6.
false from the right → isBST(4) = true && false = false.
false from the right → isBST(2) = true && false = false.
Left result false → && short-circuits: the right subtree is never checked; isBST(6) returns false.
Result: false; not a BST. A check of parent–child pairs alone (4 < 7 ✓) would wrongly say true; the range must be passed down. Cost: O(n).
boolean isBST(TreeNode k, long min, long max) { if (k == null) return true; if (k.data <= min || k.data >= max) return false; // out of range return isBST(k.left, min, k.data) && isBST(k.right, k.data, max); } // first call: isBST(root, Long.MIN_VALUE, // Long.MAX_VALUE)
BST Operations
Application: Student Records
The student ID is the key; each node carries the whole student object.
public class Student { int id; // key String fullName; public Student(int id, String fullName) { this.id = id; this.fullName = fullName; } }
public class StudentNode { Student st; // data StudentNode leftChild; StudentNode rightChild; public StudentNode(Student st) { this.st = st; // children start out null } }
TreeMap<Integer, Student> packages the same idea: a key → value map backed by a balanced (red-black) search tree.BST Operations
Inserting and Finding Students
StudentNode insert(StudentNode root, Student st) {
if (root == null) return new StudentNode(st);
if (st.id < root.st.id)
root.leftChild = insert(root.leftChild, st); // continue left
else if (st.id > root.st.id)
root.rightChild = insert(root.rightChild, st); // continue right
return root; // duplicate IDs are skipped
}
Student search(StudentNode root, int id) { if (root == null) return null; // no such record if (id == root.st.id) return root.st; // found if (id < root.st.id) return search(root.leftChild, id); // continue left return search(root.rightChild, id); // continue right }
search returns the student object, not a boolean, so a hit also gives the name and the rest of the record. With an int key, == and < work directly.
Balance and AVL Trees
Why Balance Matters
- A BST's shape depends on the insertion order: insert 1, 2, 3, 4 in order and it becomes a linked list.
- Arranged differently, the same keys fit in a shorter tree (two options on the right).
Balance and AVL Trees
A Rebalancing Example
On the left, the branches 9 → 14 → 12 and 76 → 54 → 72 → 67 have grown long. Rearranging those subtrees (new subtree roots in orange) fits the same 11 keys into a shorter tree:
- Rearranging does not change the inorder sequence: 9 12 14 17 19 23 50 54 67 72 76.
- Rebuilding the tree each time would cost O(n); AVL and red-black trees work only along the update path, in O(log n).
Balance and AVL Trees
AVL Trees
- Adelson-Velsky and Landis (1962): the first balanced BST.
- Balance factor: bf(x) = h(left) − h(right). An empty subtree has height −1.
- AVL condition: bf ∈ {−1, 0, +1} at every node.
- Consequence: h < 1.44 log2(n + 2), so every operation is O(log n).
- Each node stores its height; after an update, bf is checked on the way back up to the root.
Balance and AVL Trees
Why Is the Height of an AVL Tree O(log n)?
Question: what is the fewest nodes an AVL tree of height h can have? In the sparsest one, the two subtrees of every internal node differ in height by exactly 1.
h = 0: a single node. N(0) = 1.
h = 1: a root and one child. N(1) = 2.
h = 2: root (orange) + sparsest tree of height 1 (left) + sparsest tree of height 0 (right). N(2) = 1 + N(1) + N(0) = 1 + 2 + 1 = 4.
h = 3: root (orange) + sparsest tree of height 2 (left) + sparsest tree of height 1 (right). N(3) = 1 + N(2) + N(1) = 1 + 4 + 2 = 7.
h = 4: root (orange) + sparsest tree of height 3 (left) + sparsest tree of height 2 (right). N(4) = 1 + N(3) + N(2) = 1 + 7 + 4 = 12. The counts grow like Fibonacci numbers: N(h) = F(h+3) − 1.
Balance and AVL Trees
Rotation
Right rotation (y left-heavy): x moves up, y becomes x's right child, and x's right subtree T2 moves to y's left.
Left rotation (x right-heavy): the mirror image. Only 3 links change (thick lines) → O(1).
Balance and AVL Trees
Single Rotations: LL and RR
LL: 10 went to the left of the left child (20). bf(30) = 2 ✗ and bf(20) = 1: both nodes lean the same way (left).
Rotate right at 30: 20 moves up and 30 becomes its right child (orange link). All bf = 0.
RR: 30 went to the right of the right child (20). bf(10) = −2 ✗ and bf(20) = −1: both lean right.
Rotate left at 10: 20 moves up and 10 becomes its left child. All bf = 0.
The number at a node's upper right is its bf. LL takes a single right rotation, RR a single left rotation; the orange link is the one the rotation changes.
Balance and AVL Trees
Double Rotations: LR and RL
LR: 20 went to the right of the left child (10). bf(30) = 2 but bf(10) = −1: opposite leans (a triangle). A single right rotation still leaves a triangle.
Rotation 1: rotate left at 10 → 20 moves up; the triangle becomes a straight line (LL). bf(30) is still 2.
Rotation 2: rotate right at 30 → the middle key, 20, becomes the root; all bf = 0.
RL: 20 went to the left of the right child (30). bf(10) = −2, bf(30) = 1: a triangle.
Rotation 1: rotate right at 30 → a straight line (RR). bf(10) is still −2.
Rotation 2: rotate left at 10 → 20 is the root; all bf = 0.
LR: rotate left at the child, then right at the unbalanced node. RL: right at the child, then left.
Balance and AVL Trees
AVL Rotation and Rebalancing Code
int h(AvlNode d) { return d == null ? -1 : d.height; } int bf(AvlNode d) { return h(d.left) - h(d.right); } void update(AvlNode d) { d.height = 1 + Math.max(h(d.left), h(d.right)); } AvlNode rotateRight(AvlNode y) { AvlNode x = y.left; y.left = x.right; // T2 → y.left x.right = y; // y → x.right update(y); // lower node first update(x); return x; // new root }
AvlNode rebalance(AvlNode d) {
update(d);
if (bf(d) > 1) { // left-heavy
if (bf(d.left) < 0) // LR
d.left = rotateLeft(d.left);
return rotateRight(d); // LL
}
if (bf(d) < -1) { // right-heavy
if (bf(d.right) > 0) // RL
d.right = rotateRight(d.right);
return rotateLeft(d); // RR
}
return d;
}
// rotateLeft: mirror image (left ↔ right)
// insert/delete as in a BST; on return:
// return rebalance(root);
As the recursion unwinds, rebalance runs at each node on the path; a deletion may rotate at several nodes (at most O(log n)).
Balance and AVL Trees · Step through the code
Code Trace: rotateRight(40) — the LL Case
AvlNode rotateRight(AvlNode y) {
AvlNode x = y.left;
y.left = x.right; // T2 → y.left
x.right = y; // y → x.right
update(y); // lower node first
update(x);
return x; // new root
}
void update(AvlNode d) {
d.height = 1 +
Math.max(h(d.left), h(d.right));
}
After inserting 5, rebalance(40) finds bf = 2; its left child 20 has bf = 1 → LL. Call rotateRight(y).
x = y.left: 20 is the child that will move up. No link has changed yet.
y.left = x.right: 40's left link now points to T2 (30). 20 is detached; only x still reaches it.
x.right = y: x.right: 30 → 40. 40 is now 20's right child (the orange arrow points up).
update(y): 40.height = 1 + max(0, 0) = 1 (was 3). The lower node goes first, since x's height depends on it.
update(x): 20.height = 1 + max(1, 1) = 2 (unchanged, but must be recomputed).
return x: 20 is the subtree's new root (same links, redrawn). All bf ∈ {−1, 0, 1}; inorder sequence unchanged. The method changed 2 links; the caller sets the 3rd (from the parent) → O(1).
Balance and AVL Trees · Step through the code
Code Trace: rotateLeft(20) — the RR Case
AvlNode rotateLeft(AvlNode x) {
AvlNode y = x.right;
x.right = y.left; // T2 → x.right
y.left = x; // x → y.left
update(x); // lower node first
update(y);
return y; // new root
}
void update(AvlNode d) {
d.height = 1 +
Math.max(h(d.left), h(d.right));
}
After inserting 60, rebalance(20) finds bf = −2; its right child 40 has bf = −1 → RR. Call rotateLeft(x).
y = x.right: 40 is the child that will move up. No link has changed yet.
x.right = y.left: 20's right link now points to T2 (30). 40 is detached; only y still reaches it.
y.left = x: y.left: 30 → 20. 20 is now 40's left child (the orange arrow points up).
update(x): 20.height = 1 + max(0, 0) = 1 (was 3). The lower node goes first, since y's height depends on it.
update(y): 40.height = 1 + max(1, 1) = 2 (unchanged, but must be recomputed).
return y: 40 is the subtree's new root (same links, redrawn). All bf ∈ {−1, 0, 1}; inorder sequence unchanged. The method changed 2 links; the caller sets the 3rd (from the parent) → O(1).
Balance and AVL Trees · Step through the code
Code Trace: rebalance(50) — the LR Case
AvlNode rebalance(AvlNode d) {
update(d);
if (bf(d) > 1) { // left-heavy
if (bf(d.left) < 0) // LR
d.left = rotateLeft(d.left);
return rotateRight(d); // LL
}
if (bf(d) < -1) { // right-heavy
if (bf(d.right) > 0) // RL
d.right = rotateRight(d.right);
return rotateLeft(d); // RR
}
return d;
}
After inserting 25, the unwinding recursion calls rebalance(30). update: height 0 → 1, bf = 1.
bf = 1: neither > 1 nor < −1 → no rotation; return d to the caller.
After inserting 25, the unwinding recursion calls rebalance(20). update: height 1 → 2, bf = −1.
bf = −1: neither > 1 nor < −1 → no rotation; return d to the caller.
rebalance(50): update → height 3, bf = h(20) − h(60) = 2 − 0 = 2.
bf(d) = 2 > 1 → left-heavy. Which rotation? Look at how the left child leans.
bf(d.left) = bf(20) = -1 < 0: the left child leans right → LR (a triangle). A single right rotation is not enough.
d.left = rotateLeft(20): 30 moves up and 25 moves to 20's right. The triangle is now a straight line (LL); bf(50) is still 2.
return rotateRight(50): 30 becomes the root, 50 drops to its right, and 30's old right (null) becomes 50's left.
rebalance(50) returns the new subtree root 30; insert hands it to its caller (here root). Height drops from 3 to 2.
Balance and AVL Trees · Step through the code
Tracing AVL Insertion: 10, 20, 30
Empty tree: insert(null, 10) → d == null, a new node is returned; root = 10, bf = 0.
insert(root, 20) descends as in a BST: 20 > 10 → right.
d == null → new leaf 20 is returned and becomes 10's right child. The bf values on the path have changed.
Unwinding, each ancestor runs return rebalance(d): bf(10) = −1 → all |bf| ≤ 1, no rotation.
insert(root, 30) descends as in a BST: 30 > 10 → right; 30 > 20 → right.
d == null → new leaf 30 is returned and becomes 20's right child. The bf values on the path have changed.
rebalance(10): bf = −2 ✗ (lowest unbalanced ancestor). Right child 20 has bf = −1 → RR case.
rotateLeft at 10: 20 moves up and 10 becomes its left child. All bf ∈ {−1, 0, 1}.
AvlNode insert(AvlNode d, int k) { if (d == null) return new AvlNode(k); if (k < d.key) d.left = insert(d.left, k); else if (k > d.key) d.right = insert(d.right, k); else return d; // no duplicates return rebalance(d); }
Balance and AVL Trees · Step through the code
Tracing AVL Insertion (continued): 40, 50, 25
insert(root, 40) descends as in a BST: 40 > 20 → right; 40 > 30 → right.
d == null → new leaf 40 is returned and becomes 30's right child. The bf values on the path have changed.
Unwinding, each ancestor runs return rebalance(d): bf(30) = −1, bf(20) = −1 → all |bf| ≤ 1, no rotation.
insert(root, 50) descends as in a BST: 50 > 20 → right; 50 > 30 → right; 50 > 40 → right.
d == null → new leaf 50 is returned and becomes 40's right child. The bf values on the path have changed.
rebalance(30): bf = −2 ✗ (lowest unbalanced ancestor). Right child 40 has bf = −1 → RR case.
rotateLeft at 30: 40 moves up and 30 becomes its left child. All bf ∈ {−1, 0, 1}.
insert(root, 25) descends as in a BST: 25 > 20 → right; 25 < 40 → left; 25 < 30 → left.
d == null → new leaf 25 is returned and becomes 30's left child. The bf values on the path have changed.
rebalance(20): bf = −2 ✗ (lowest unbalanced ancestor). Right child 40 has bf = 1 → RL case.
Rotation 1: rotateRight at 40 → the triangle becomes a straight line (right-right). bf(20) is still −2.
Rotation 2: rotateLeft at 20: 30 moves up and 25 moves to 20's right. The tree is AVL again. All bf ∈ {−1, 0, 1}.
AvlNode insert(AvlNode d, int k) { if (d == null) return new AvlNode(k); if (k < d.key) d.left = insert(d.left, k); else if (k > d.key) d.right = insert(d.right, k); else return d; // no duplicates return rebalance(d); }
Red-Black Trees
Red-Black Trees
- Every node is red or black.
- The root is black.
- All leaves (NIL, the empty children) count as black.
- A red node's children are black: no two reds in a row.
- Every path from a node down to its NIL leaves has the same number of black nodes (the black height).
Example: every path from the root to a NIL has 2 black nodes (counting the root, not the NIL)
Red-Black Trees
Why Do the Rules Guarantee Balance?
- Shortest path: can be all black → b nodes (10–5 in the figure: b = 2).
- Longest path: reds cannot be adjacent, so at most one red per black → at most 2b nodes (10–20–30–40).
- Consequence: longest path ≤ 2 × shortest path, which gives h ≤ 2·log2(n + 1).
- Cost: search, insertion, deletion in O(log n); at most 2 rotations per insertion, 3 per deletion; the rest is recoloring.
TreeMap/TreeSet and C++'s std::map use it.Red-Black Trees
Inserting into a Red-Black Tree
Insert the new node as in a BST and color it red (leaving the black height intact). If its parent is red too, rule 4 breaks; look at the uncle (the parent's sibling):
- If the parent is black, nothing needs doing. At the end, the root is always colored black.
- A NIL uncle counts as black.
- Deletion is similar but has more cases (the "double black" fix).
Red-Black Trees
Fixing Up After Insertion
void fixUp(Node x) { // x: new, red while (x != root && isRed(x.parent)) { Node p = x.parent, g = p.parent; Node u = (p == g.left) ? g.right : g.left; if (isRed(u)) { // red uncle p.color = u.color = BLACK; g.color = RED; x = g; // move up to g } else { // black uncle if (isTriangle(x, p, g)) { // zigzag rotate(p, x); // make it a line Node t = x; x = p; p = t; } rotate(g, p); // line p.color = BLACK; g.color = RED; } } root.color = BLACK; }
// Insertion: add a leaf as in a BST, // link its parent, color it red: // x = bstInsert(key); // x.color = RED; fixUp(x); // NIL (null) counts as black boolean isRed(Node n) { return n != null && n.color == RED; } // is x–p–g a zigzag (triangle)? boolean isTriangle(Node x, Node p, Node g) { return (p == g.left) != (x == p.left); } // rotate(a, b): b is a child of a; // moves b into a's place (rotate // right if b is a left child, else // left; parent links included)
Red-Black Trees · Step through the code
Code Trace: fixUp — Inserting 10, 20, 30, 15
void fixUp(Node x) { // x: new, red while (x != root && isRed(x.parent)) { Node p = x.parent, g = p.parent; Node u = (p == g.left) ? g.right : g.left; if (isRed(u)) { // red uncle p.color = u.color = BLACK; g.color = RED; x = g; // move up to g } else { // black uncle if (isTriangle(x, p, g)) { // zigzag rotate(p, x); // make it a line Node t = x; x = p; p = t; } rotate(g, p); // line p.color = BLACK; g.color = RED; } } root.color = BLACK; }
10 is inserted as in a BST (as the root) and colored red; then fixUp(x).
x is the root → the loop condition is false; the loop is skipped.
root.color = BLACK: the root is always black (rule 2).
20 is inserted as in a BST (10's right child) and colored red; then fixUp(x).
x.parent (10) is black → no red-red pair; the loop ends. Line 18: the root is already black.
30 is inserted as in a BST (20's right child) and colored red; then fixUp(x).
x.parent (20) is red too: two reds in a row ✗ (rule 4) → enter the loop.
p = parent (20), g = grandparent (10), u = uncle = NIL (empty, counts as black).
The uncle is black → a rotation is needed. x–p–g is a straight line: isTriangle() is false; jump to line 14.
rotate(g, p): left rotation at grandparent 10 → p (20) moves up, g becomes its child.
Colors: p black, g red. No red-red pair remains; the black height is preserved.
x.parent (20) is black → no red-red pair; the loop ends. Line 18: the root is already black.
15 is inserted as in a BST (10's right child) and colored red; then fixUp(x).
x.parent (10) is red too: two reds in a row ✗ (rule 4) → enter the loop.
p = parent (10), g = grandparent (20), u = uncle = 30.
The uncle is red → just recolor: p and u black, g red. Every path keeps the same number of blacks.
x = g (20): g is now red, and its parent may be red too → the loop continues from there.
x is the root → the loop condition is false; the loop is skipped.
root.color = BLACK: the root is always black (rule 2). The black height grows by 1.
Red-Black Trees · Step through the code
Code Trace: fixUp — Inserting 12 and 5
void fixUp(Node x) { // x: new, red while (x != root && isRed(x.parent)) { Node p = x.parent, g = p.parent; Node u = (p == g.left) ? g.right : g.left; if (isRed(u)) { // red uncle p.color = u.color = BLACK; g.color = RED; x = g; // move up to g } else { // black uncle if (isTriangle(x, p, g)) { // zigzag rotate(p, x); // make it a line Node t = x; x = p; p = t; } rotate(g, p); // line p.color = BLACK; g.color = RED; } } root.color = BLACK; }
12 is inserted as in a BST (15's left child) and colored red; then fixUp(x).
x.parent (15) is red too: two reds in a row ✗ (rule 4) → enter the loop.
p = parent (15), g = grandparent (10), u = uncle = NIL (empty, counts as black).
The uncle is black → a rotation is needed. x–p–g is a triangle (zigzag): x and p are children on opposite sides.
rotate(p, x): a right rotation at 15 → x goes up, p goes down. The triangle is now a straight line (still two reds in a row).
x and p swap roles: now x = 15 (the lower one) and p = 12.
rotate(g, p): left rotation at grandparent 10 → p (12) moves up, g becomes its child.
Colors: p black, g red. No red-red pair remains; the black height is preserved.
x.parent (12) is black → no red-red pair; the loop ends. Line 18: the root is already black.
5 is inserted as in a BST (10's left child) and colored red; then fixUp(x).
x.parent (10) is red too: two reds in a row ✗ (rule 4) → enter the loop.
p = parent (10), g = grandparent (12), u = uncle = 15.
The uncle is red → just recolor: p and u black, g red. Every path keeps the same number of blacks.
x = g (12): g is now red, and its parent may be red too → the loop continues from there.
x.parent (20) is black → no red-red pair; the loop ends. Line 18: the root is already black.
Balanced Search Trees
Deletion in AVL and Red-Black Trees
| Structure | Upward pass | Rotations | Total |
|---|---|---|---|
| AVL | O(log n) | up to O(log n) | O(log n) |
| Red-black | O(log n) | at most 3 | O(log n) |
Insertion and deletion keep the BST order; rebalancing changes only the shape of the tree and its bookkeeping fields.
Red-Black Trees
AVL or Red-Black?
| Property | AVL | Red-black |
|---|---|---|
| Balance criterion | |bf| ≤ 1 at every node | color rules, equal black height |
| Maximum height | ≈ 1.44 log2 n | 2 log2(n + 1) |
| Search | O(log n), slightly faster (shorter tree) | O(log n) |
| Rotations per insertion | at most 2 (one single or one double) | at most 2 |
| Rotations per deletion | up to O(log n) | at most 3 |
| Extra data per node | height (or bf) | 1 color bit |
| Best for | search-heavy workloads | frequent updates, libraries |
Splay Trees
Splay Trees
- Sleator and Tarjan (1985): a self-adjusting BST.
- Every access (search, insertion, deletion) rotates the node to the root: the splay operation.
- Nodes store no balance information (no height, no color).
- One operation can take O(n), but any m operations take O(m log n): O(log n) amortized.
Splay Trees
Splay Steps: Zig, Zig-Zig, Zig-Zag
x: the node being splayed, p: its parent, g: its grandparent. Repeat until x is the root.
Zig: x's parent p is the root → a single rotation at p. (This can only be the last step.)
Zig-zig: x and p are children on the same side → rotate at g first, then at p. (Not the same as an AVL single rotation!)
Zig-zag: x and p are on opposite sides → rotate at p, then at g (exactly an AVL double rotation).
Splay Trees · Step through the code
Code Trace: splay(50), then splay(10)
void splay(Node x) { while (x != root) { Node p = x.parent, g = p.parent; if (g == null) // zig rotate(p, x); else if (sameSide(x, p, g)) { // zig-zig rotate(g, p); // grandparent first rotate(p, x); } else { // zig-zag rotate(p, x); // parent first rotate(g, x); } } }
splay(50): first an ordinary BST search, 80 → 40 → 60 → 50; the node found is x. (sameSide: are x and p children on the same side? rotate: the single rotation from the red-black code.)
x is not the root. p = 60, g = 40.
x and p are on opposite sides → zig-zag. The parent first: rotate right at 60.
Then the grandparent: rotate left at 40 → x becomes the parent of both p and g.
x is not the root. p = 80, g = null (p is the root).
g == null → zig: a right rotation at the root, 80; x becomes the root.
x == root → the loop ends. 50 is now the root; frequently accessed keys stay near the root.
splay(10): first an ordinary BST search, 50 → 40 → 20 → 10; the node found becomes x.
x is not the root. p = 20, g = 40.
x and p are on the same side → zig-zig. The grandparent first: rotate right at 40 (p moves up).
Then the parent: rotate right at 20 → x has risen two levels.
x is not the root. p = 50, g = null (p is the root).
g == null → zig: a right rotation at the root, 50; x becomes the root.
x == root → the loop ends. 10 is the root; 50, accessed just before, is still near it (depth 1). One operation may take O(n); amortized, O(log n).
Multiway Search Trees
B-Trees
- A balanced multiway search tree whose nodes hold several keys and many children.
- In a B-tree of order m, a node has at most m children and m − 1 keys; each non-root internal node has at least ⌈m/2⌉ children.
- Keys within a node are sorted, and all leaves are at the same depth.
- The height stays O(log n); insertions and deletions split or merge nodes.
A B-tree of order 4 (a 2-3-4 tree) is equivalent to a red-black tree: merge each black node with its red children and you get a 2-3-4 tree. Insertion and splitting step by step: B-tree simulator.
Multiway Search Trees
Search and Insertion in a B-Tree
1 · Search within the node. To find 30, compare it with the root's keys in order; 20 < 30 < 40 → descend to the middle child, which covers that gap.
30 is found in the leaf. Each level reads just one node (one disk block) → the cost is proportional to the height: O(logm n).
2 · Insert into the leaf. 33 follows the same path and goes into the leaf in sorted position. The leaf now holds 4 keys, over its capacity (m − 1 = 3).
3 · Split the overflow. The middle key (33) moves up to the parent; the rest split into two nodes (25 30 and 35). If the parent overflows too, splitting continues upward; if the root splits, a new root appears and the height grows by one.
Multiway Search Trees · Step by step
B-Tree Insertion and Splitting (m = 3)
Keys inserted in order: 10, 20, 30, 40, 50, 60, 70 · order m = 3 → at most 2 keys and 3 children per node.
Insert 10. It goes into the root leaf in sorted order; 1 key (≤ 2), no split.
Insert 20. It goes into the root leaf in sorted order; 2 keys (≤ 2), no split.
Insert 30 → node [10, 20, 30]: 3 keys, over capacity (m − 1 = 2) → split.
The root splits: the median 20 moves up as a new root; 10 and 30 become separate nodes. The height grows by one; all leaves stay at the same depth.
Insert 40. Go right from the root and add it to the leaf in sorted order; 2 keys (≤ 2), no split.
Insert 50 → node [30, 40, 50]: 3 keys, over capacity (m − 1 = 2) → split.
The median, 40, moves up to the parent; the rest split into two nodes, 30 and 50. The parent has room → the split stops here.
Insert 60. Go right from the root and add it to the leaf in sorted order; 2 keys (≤ 2), no split.
Insert 70 → node [50, 60, 70]: 3 keys, over capacity (m − 1 = 2) → split.
The median, 60, moves up to the parent and the node splits into 50 | 70. But the parent [20, 40, 60] now has 3 keys → the split propagates upward.
The root splits: the median 40 moves up as a new root; 20 and 60 become separate nodes. The height grows by one; all leaves stay at the same depth.
Result: 7 keys on 3 levels; every internal node has 2 children; all leaves at the same depth. A B-tree grows upward: its height increases only when the root splits.
Multiway Search Trees
B+ Trees
Most relational database indexes are B+ trees.
Multiway Search Trees · Step by step
Range Query in a B+ Tree: 10 to 45
Start at the root: 10 < 20 → the leaf holding the lower bound 10 is under the leftmost child. (Just like a search: O(log n) levels.)
First leaf, [5 10]: skip 5 < 10 and take 10. At the end of the leaf → follow the next-leaf link (without climbing back up).
Second leaf: 20, 25, and 30 are all in range → take them. Follow the link again.
Third leaf: take 40; 50 > 45 → stop. Result: 10 20 25 30 40.
Cost: one descent, O(log n), plus the leaves read. A B-tree would have to climb up and down through internal nodes again and again; the B+ tree's leaf chain gives O(log n + k) (k: number of results).
Summary
Summary: Operation Costs in Search Trees
| Structure | Height | Search | Insert | Delete | How it stays balanced |
|---|---|---|---|---|---|
| BST | log n … n | O(h) | O(h) | O(h) | it doesn't; worst case O(n) |
| AVL | ≤ 1.44 log n | O(log n) | O(log n) | O(log n) | bf ∈ {−1, 0, 1}; LL, RR, LR, RL rotations |
| Red-black | ≤ 2 log(n+1) | O(log n) | O(log n) | O(log n) | recoloring + at most 2–3 rotations |
| Splay | unbounded | O(log n)* | O(log n)* | O(log n)* | accessed node moves to the root by zig / zig-zig / zig-zag |
| B-tree / B+ | ≈ logm n | O(log n) | O(log n) | O(log n) | overflowing nodes split; all leaves at the same depth |
* Amortized; one operation can take O(n). In every structure, inorder traversal is O(n) and yields the keys sorted; min/max are O(h).
Summary
Interactive Simulators
Summary
Common Mistakes
delete(root, 40); breaks the tree when the root is deleted. Write root = delete(root, 40); (likewise for insert).isBST).==That compares references. Order keys with compareTo and test equality with equals.h(null) and isRed(null).Summary
Sample Problems
- Insert 50, 30, 70, 20, 40, 60, 80 in order into an empty BST. What is the height? What does an inorder traversal print?
- If the same keys arrive in the order 20, 30, 40, 50, 60, 70, 80, what are the height and the search cost?
- Delete the root 50 from the tree in problem 1 using the inorder successor. What does the tree look like?
- Insert 3, 2, 1 into an empty AVL tree. Which case arises, and which rotation fixes it?
- Insert 10, 30, 20 into an empty AVL tree. Which case arises, and what is the resulting tree?
- In a red-black tree, the longest root-to-NIL path is at most how many times the shortest? Why?
- In what order are the rotations done in a splay tree's zig-zig step?
Show answers
- h = 2 (perfectly balanced); inorder: 20 30 40 50 60 70 80.
- A chain to the right: h = 6; search is O(n) (7 comparisons in the worst case).
- The successor 60 is copied into the root; the 60 left of 70 is deleted: 60(30(20, 40), 70(—, 80)).
- bf = 2 at 3 and bf = 1 at its left child 2 → LL; a right rotation at 3 → 2(1, 3).
- bf = −2 at 10 and bf = +1 at its right child 30 → RL; right at 30, then left at 10 → 20(10, 30).
- Twice: every path has the same number of blacks, and reds cannot be adjacent.
- First at the grandparent (g), then at the parent (p).
Summary
Exercises
- Write
floor(root, x), which returns the largest key ≤ x (or null if there is none). - Write a method that finds the k-th smallest key in a BST. What is its time complexity?
- In what order should 1, 2, …, 7 be inserted into an empty BST to get a perfect tree of height 2?
- Insert 1, 2, …, 7 in order into an empty AVL tree. What is the final tree? How many rotations are done?
- What is the minimum number of nodes in an AVL tree of height 5?
- Why does
range(root, lo, hi)take O(h + s) rather than O(n)?
Answers / hints
- Mirror
successor: ifx >= root.data,candidate = root; root = root.right;, else go left. O(h). - An inorder traversal with a counter: the k-th node visited is the answer → O(h + k).
- The middle first, then the middles of the halves: e.g., 4, 2, 6, 1, 3, 5, 7.
- 4(2(1, 3), 6(5, 7)); inserting 3, 5, 6, and 7 each triggers one left rotation → 4 rotations.
- N(5) = 1 + N(4) + N(3) = 1 + 12 + 7 = 20.
- Left subtrees of nodes below lo and right subtrees of nodes above hi are never visited (pruning).
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
- Sedgewick and Wayne, Algorithms, 4th ed., 2011.