Binary Search Trees
Order in, log-time out
Binary search tree · path-walking search
init
Try it: Insert keys; search highlights the comparison path. Then hit break it: inserting sorted values collapses the tree into a linked list — height jumps from log n to n.
Why it matters
BSTs combine the fast lookup of arrays with the flexible insertion of linked lists. They're the foundation for sets, maps, and databases. Understanding BSTs is key to understanding balanced trees and efficient searching.
The idea
Remember the number guessing game? "Is it higher or lower?" Each answer eliminates half the possibilities.
A BST is that game in tree form. Every node follows one rule: left children are smaller, right children are larger.
To find a value:
- Start at root
- If value < node, go left. If value > node, go right.
- Repeat until found or reach null.
Each step eliminates half the tree - that's O(log n)... if the tree is balanced. If you insert sorted data, you get a linked list and O(n). That's why balanced trees exist.
Key takeaways
- Left subtree < node < right subtree (BST property)
- Search, insert, delete are O(h) where h is height
- Balanced tree: h = O(log n). Unbalanced: h = O(n)
- In-order traversal gives sorted sequence
The BST invariant
Every operation on a BST must preserve one rule. State it once, you can prove correctness of every algorithm by checking the rule holds before and after:
- BST invariant
- For every node
n: every key inn.left's subtree is strictly less thann.key, and every key inn.right's subtree is strictly greater. - Corollary — in-order traversal yields sorted order
- Visit
leftrecursively, thenself, thenrightrecursively. The recursive call onleftreturns keys all less thanself.key; the call onrightreturns keys all greater. So the concatenation is sorted. - What "preserve" means in practice
- Every insert/delete must end with the invariant still true everywhere. Insert is easy — you only add at a leaf, so the rule already holds. Delete is the hard case; see below.
Going deeper
Deletion — the three cases, walked through
This is the operation that makes engineers reach for AVL or Red-Black trees. Removing a node while preserving the invariant takes care.
Case 1 — leaf. Just detach it from its parent. The invariant is trivially preserved because no other node depended on this one.
5 5
/ \ / \
3 8 → 3
/
6 (delete 6 from right of 3)
Wait, that example removes 6 from the left of 8 — point is, leaves are free.
Case 2 — one child. Splice the child up into the deleted node's slot. The deleted node's parent now points at the child; the child's subtree was already invariant-compliant, so it stays compliant in the new position.
5 5
/ \ / \
3 8 → 3 6
/
6 (delete 8: it had only a left child, 6)
Case 3 — two children. You can't just promote either child — both have their own subtrees you'd have to merge somehow. The trick: find the in-order successor — the smallest key in the right subtree (equivalently, the leftmost leaf of right). Copy its value into the deleted node, then delete that successor from the right subtree (recursive call, but the successor itself has at most one child, so the recursion bottoms out at case 1 or 2).
5 6
/ \ / \
3 8 → 3 8
/ \ / \
6 9 7 9
\
7 (delete 5: successor is 6;
copy 6 up, then delete the
original 6 — case 2 with right
child 7)
Why successor and not predecessor? Either works — the predecessor is the largest key in the left subtree. Most implementations alternate or pick one and stay consistent. The invariant is preserved because the successor was strictly greater than everything in the left subtree (so it's still greater after promotion) and strictly less than everything else in the right subtree (so the right subtree is still valid).
Why this all collapses without balance
The demo's break it button inserts already-sorted values. Every new node goes to the rightmost position — the tree becomes a linked list, height climbs to n, and search degrades to O(n). This is the motivation for the balanced trees lesson: the algorithms there rebalance after every insert and delete, so the height stays O(log n) no matter what order the inputs arrive.
Math details
- Time Complexity (height h)
Search: O(h)Insert: O(h)Delete: O(h)Min/Max: O(h)- Height bounds
Balanced: h = O(log n)Unbalanced: h = O(n)
Implementation
BST Search
fn search(node: Option<&Box<BstNode>>, key: i32)
-> bool
{
match node {
None => false,
Some(n) => {
if key < n.data {
search(n.left.as_ref(), key)
} else if key > n.data {
search(n.right.as_ref(), key)
} else {
true // Found!
}
}
}
}
Deletion Cases
- Leaf: Remove directly
- One child: Replace with child
- Two children: Replace with successor
Practice
Three problems built on the BST invariant:
- Validate BST — medium · why local checks aren't enough; the
(min, max)window pattern - Lowest Common Ancestor in a BST — easy · O(h) walk that exploits the BST ordering — no auxiliary structure
- Kth Smallest in a BST — medium · iterative in-order with an explicit stack; the template behind every ordered-index cursor