A Binary Search Tree is a rooted binary tree where every node satisfies the BST invariant:
class Node:
key: int
value: any
left: Node | None
right: Node | None
The invariant gives us O(log n) expected search time — each comparison eliminates roughly half the remaining nodes, just like binary search on a sorted array.
Starting at the root, compare the target key k with the current node's key:
k == node.key — found, return nodek < node.key — recurse into left subtreek > node.key — recurse into right subtreenode == null — key not presentdef search(node, key):
if node is None:
return None
if key == node.key:
return node
elif key < node.key:
return search(node.left, key)
else:
return search(node.right, key)
def search_iterative(node, key):
while node is not None:
if key == node.key:
return node
elif key < node.key:
node = node.left
else:
node = node.right
return None
Iterative search avoids call-stack overhead — preferred in production for deep trees.
Searching for key 7 in the example tree:
3 comparisons — O(log 9) ≈ 3.2
Insertion follows the same path as search. When we reach a null pointer, we create a new node there.
def insert(node, key, value):
if node is None:
return Node(key, value)
if key < node.key:
node.left = insert(node.left, key, value)
elif key > node.key:
node.right = insert(node.right, key, value)
else:
node.value = value # duplicate: update
return node
Key insight: the shape of a BST is entirely determined by insertion order. This is the fundamental motivation for self-balancing trees.
[4, 2, 6, 1, 3, 5, 7]
4
/ \
2 6
/ \ / \
1 3 5 7
Height = 2
[1, 2, 3, 4, 5, 6, 7]
1
\
2
\
3
\
...
Height = 6
No children — remove the node directly.
Replace the node with its single child.
Replace with in-order successor, then delete the successor.
Deletion with two children is O(h) where h is the tree height — finding the successor requires walking down the right subtree.
def delete(node, key):
if node is None:
return None
if key < node.key:
node.left = delete(node.left, key)
elif key > node.key:
node.right = delete(node.right, key)
else:
if node.left is None:
return node.right
if node.right is None:
return node.left
# Two children: in-order successor
succ = find_min(node.right)
node.key = succ.key
node.value = succ.value
node.right = delete(
node.right, succ.key)
return node
The node with the smallest key greater than a given node's key.
| Case | Method |
|---|---|
| Right subtree exists | Leftmost node in right subtree |
| No right subtree | Walk up until node is in a left subtree |
def find_successor(node):
if node.right:
return find_min(node.right)
parent = node.parent
while parent and node == parent.right:
node = parent
parent = parent.parent
return parent
The mirror: largest key smaller than the given node.
| Case | Method |
|---|---|
| Left subtree exists | Rightmost node in left subtree |
| No left subtree | Walk up until node is in a right subtree |
Successor and predecessor are essential for deletion (two-child case), range queries, and iterator design.
| Order | Visit sequence | Output (example tree) |
|---|---|---|
| In-order | left, node, right | 1, 3, 4, 6, 7, 8, 10, 13, 14 |
| Pre-order | node, left, right | 8, 3, 1, 6, 4, 7, 10, 14, 13 |
| Post-order | left, right, node | 1, 4, 7, 6, 3, 13, 14, 10, 8 |
| Level-order | BFS by depth | 8, 3, 10, 1, 6, 14, 4, 7, 13 |
def inorder(node, result):
if node is None:
return
inorder(node.left, result)
result.append(node.key)
inorder(node.right, result)
In-order traversal of a BST always produces keys in ascending sorted order. This is the foundation of tree-sort.
O(n log n) average — balanced insertionsO(n2) worst — degenerate treeO(n) in-order traversal after constructionGiven a sorted array, build a height-balanced BST (minimise height).
Pick the middle element as root. Recursively build the left subtree from the left half and the right subtree from the right half.
def sorted_array_to_bst(arr, lo, hi):
if lo > hi:
return None
mid = (lo + hi) // 2
node = Node(arr[mid])
node.left = sorted_array_to_bst(
arr, lo, mid - 1)
node.right = sorted_array_to_bst(
arr, mid + 1, hi)
return node
| Metric | Value |
|---|---|
| Time | O(n) — each element visited once |
| Space | O(log n) — recursion stack |
| Result height | floor(log2(n)) |
This is the optimal BST construction for uniformly distributed queries. For skewed distributions, see optimal BSTs (Knuth's algorithm).
| Operation | Best | Average | Worst |
|---|---|---|---|
| Search | O(1) | O(log n) | O(n) |
| Insert | O(1) | O(log n) | O(n) |
| Delete | O(1) | O(log n) | O(n) |
| Min / Max | O(1) | O(log n) | O(n) |
| Successor | O(1) | O(log n) | O(n) |
| In-order traversal | O(n) | O(n) | O(n) |
O(n) for n nodesO(h) stack spaceh = O(log n)h = O(n)A BST built from n random insertions has expected height O(log n). Specifically, the expected depth of a node is approximately 1.39 × log2(n) — the same constant as quicksort's expected comparisons.
The O(log n) average case assumes random insertion order. In practice, data often arrives sorted or nearly sorted, producing the worst case. This is why we need balancing.
When keys are inserted in sorted order, each new node becomes a right child. The tree degenerates into a linked list.
1
\
2
\
3
\
4
\
5
Height = n - 1
3
/ \
1 4
\ \
2 5
Height = 2
| Tree shape | Height | Search |
|---|---|---|
| Perfectly balanced | log2(n) | O(log n) |
| Random insertions | ~1.39 × log2(n) | O(log n) |
| Sorted insertions | n - 1 | O(n) |
A degenerate BST offers no advantage over a linked list but uses more memory (two child pointers per node).
A balanced BST maintains h = O(log n) after every insertion and deletion, ensuring worst-case O(log n) operations.
| Structure | Criterion | Height bound |
|---|---|---|
| AVL tree | Child heights differ by ≤ 1 | 1.44 × log2(n) |
| Red-Black | No path > 2× shortest | 2 × log2(n) |
| B-tree | All leaves same depth | logB(n) |
| Treap | Random priorities | O(log n) expected |
| Splay tree | No explicit criterion | O(log n) amortised |
Balancing requires extra work on each insert/delete — typically rotations:
One pointer swap — O(1)
Two pointer swaps — O(1)
Up to O(log n) rotations — amortised
The overhead is small: rotations are constant-time pointer operations. The payoff is enormous: guaranteed logarithmic performance regardless of input order.
If insertion order determines tree shape, and random order gives O(log n) expected height, can we simulate random order?
Randomly permute the keys before insertion. Simple but requires all keys up front (offline).
On inserting a key into a subtree of size n, with probability 1/(n+1) insert it at the root of the subtree (using rotations). Otherwise, recurse normally.
def rand_insert(node, key):
if node is None:
return Node(key)
if random() < 1.0 / (size(node) + 1):
return insert_at_root(node, key)
if key < node.key:
node.left = rand_insert(
node.left, key)
else:
node.right = rand_insert(
node.right, key)
return node
O(log n)Randomised BSTs are elegant in theory. In practice, treaps achieve the same probabilistic guarantees with a cleaner implementation.
A treap is a BST where each node has a key (BST order) and a random priority (heap order). The tree simultaneously satisfies:
| Operation | Method | Time |
|---|---|---|
| Insert | BST insert, rotate up to restore heap | O(log n) |
| Delete | Set priority → -∞, rotate to leaf, remove | O(log n) |
| Split | Insert key with priority ∞ | O(log n) |
| Merge | Compare root priorities, recurse | O(log n) |
Treaps are simpler to implement than AVL or Red-Black trees. Used in competitive programming and functional data structures.
An augmented BST stores additional information in each node to support extra queries efficiently. The augmented data must be maintainable during rotations in O(1).
Each node stores size — the number of nodes in its subtree (including itself).
| Operation | Description | Time |
|---|---|---|
select(k) | Find the k-th smallest element | O(log n) |
rank(key) | Find the rank (position) of a key | O(log n) |
def select(node, k):
left_size = size(node.left)
if k == left_size + 1:
return node
elif k <= left_size:
return select(node.left, k)
else:
return select(node.right,
k - left_size - 1)
Given a set of intervals [lo, hi], efficiently find all intervals that overlap a query interval or point.
Each node stores an interval [lo, hi] as its key (ordered by lo), plus max_hi — the maximum hi value in its subtree.
def query_overlap(node, lo, hi):
if node is None:
return []
results = []
if overlaps(node.interval, (lo, hi)):
results.append(node.interval)
if node.left and node.left.max_hi >= lo:
results += query_overlap(
node.left, lo, hi)
if node.right and \
node.right.interval.lo <= hi:
results += query_overlap(
node.right, lo, hi)
return results
Calendar conflict detection
Overlapping gene regions
Segment intersection
IP range lookups
Overlap queries: O(log n + k) where k = matches. Without augmentation: O(n).
An AVL tree (Adelson-Velsky and Landis, 1962) is a BST where for every node, the heights of the left and right subtrees differ by at most 1.
balance_factor(node) = height(left) - height(right)
# Valid values: -1, 0, +1
# Any other value triggers rebalancing
h < 1.44 × log2(n + 2)O(log n) worst caseO(log n) — at most 2 rotationsO(log n) — up to O(log n) rotations| Imbalance | Rotation |
|---|---|
| Left-Left | Right rotation (single) |
| Right-Right | Left rotation (single) |
| Left-Right | Left then right (double) |
| Right-Left | Right then left (double) |
AVL trees provide stricter balance than Red-Black trees — searches are slightly faster. Trade-off: more rotations on delete.
Longest path (alternating red-black) is at most 2× the shortest (all black):
h ≤ 2 × log2(n + 1)
Red-Black trees are the default balanced BST in most standard libraries: std::map (C++), TreeMap (Java), SortedDictionary (C#).
| Feature | AVL | Red-Black |
|---|---|---|
| Height bound | 1.44 log n | 2 log n |
| Search speed | Slightly faster | Slightly slower |
| Insert rotations | Up to 2 | Up to 2 |
| Delete rotations | O(log n) | Up to 3 |
| Best for | Read-heavy | Insert/delete-heavy |
A self-adjusting BST that moves the most recently accessed node to the root via rotations called splaying. No balance metadata stored.
| Pattern | Name | Action |
|---|---|---|
| Child of root | Zig | Single rotation |
| Same side (LL/RR) | Zig-zig | Rotate parent first, then node |
| Opposite side (LR/RL) | Zig-zag | Rotate node twice |
O(log n)m operations on n-node tree: O(m × log n) totalO(log n)No height, colour, size, or priority fields needed.
Frequently accessed elements stay near the root — automatically adapts to access patterns.
Accessing k distinct elements in a window costs O(log k) amortised, not O(log n).
Splay trees excel in caches and access-pattern-skewed workloads. Suboptimal for uniform random access due to constant restructuring.
Provide an iterator that returns BST elements in sorted (in-order) order, using O(h) space and O(1) amortised time per next() call.
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def has_next(self):
return len(self.stack) > 0
def next(self):
node = self.stack.pop()
self._push_left(node.right)
return node.key
| Metric | Value |
|---|---|
| Space | O(h) — stack holds at most one node per level |
Amortised next() | O(1) — each node pushed and popped once |
Worst single next() | O(h) — descending a left spine |
Uses O(1) extra space by temporarily modifying tree pointers (threading). Restores the tree after traversal. Not suitable for concurrent access.
The stack-based iterator is a classic interview question and the standard implementation in most BST libraries. It decouples traversal from processing.
BSTs implement the ordered symbol table ADT: put, get, delete, min, max, floor, ceiling, rank, select.
Used in compilers, interpreters, configuration stores.
B-trees (a generalisation of BSTs with high branching factor) are the default index structure in all major relational databases.
BST concepts — invariant, rotations, balance — directly underpin B-tree operations.
BSTs on strings support prefix-based lookup. "What words come between X and Y?" in O(log n + k).
Tries are a related BST-family structure.
BSTs are not just an academic exercise. They are the backbone of ordered data access in virtually every system you use.
O(log n) search, insert, and delete — but only when balancedO(n) operations — insertion order is everythingO(log n) guarantees at minimal overheadO(log n) heightO(log n) through access-driven restructuringO(h) space, O(1) amortised traversal| Source | Description |
|---|---|
| Sedgewick & Wayne | Algorithms, 4th ed. — definitive BST treatment with Java implementations |
| CLRS | Introduction to Algorithms — formal proofs for Red-Black trees, augmented BSTs |
| Skiena | The Algorithm Design Manual — practical perspective on when to use which tree |
| Sleator & Tarjan | "Self-Adjusting Binary Search Trees" — the original splay tree paper (1985) |
| MIT 6.006 | Erik Demaine's lectures on BSTs — free on YouTube, excellent visualisations |