Search, insert, delete, traverse — the data structure that makes ordered data fast.
You are building a contact list for a phone. Users add names, search for names, and delete names. The list must always be in sorted order — the user scrolls through it alphabetically. How do you store this data?
Your first instinct: a sorted array. Searching is fast — binary search finds any name in O(log n) comparisons among n contacts. But inserting a new contact means shifting every element after the insertion point to make room. That is O(n) work. Delete a contact? Same problem — shift everything back. With 10,000 contacts, every insert or delete moves thousands of elements.
Fine, try a linked list. Inserting is fast — once you know where the new node goes, just rewire two pointers. O(1) for the pointer surgery. But finding where the node goes requires scanning from the head, because you cannot jump to the middle of a linked list. Search is O(n). You traded fast insertion for slow search.
| Operation | Sorted Array | Linked List | What we want |
|---|---|---|---|
| Search | O(log n) | O(n) | O(log n) |
| Insert | O(n) | O(1)* | O(log n) |
| Delete | O(n) | O(1)* | O(log n) |
| Min / Max | O(1) | O(n) | O(log n) |
| Sorted output | O(n) | O(n log n) | O(n) |
* O(1) only after you have found the position, which takes O(n).
The sorted array gives fast search but slow mutation. The linked list gives fast mutation but slow search. Neither gives O(log n) for everything. Can we combine the best of both?
The simulation below lets you feel the difference. Insert and search for keys across all three data structures. Watch how many steps each one takes.
Click "Insert" to add a random key. Click "Search" to look for a random key. Watch the step counts.
As the collection grows, sorted array insertion becomes visibly slower (shifting elements), linked list search becomes visibly slower (scanning nodes), but BST operations stay fast — each one traces a single path from root to leaf.
Here is the roadmap. We will build BSTs from scratch: the defining property (Chapter 1), searching and queries (Chapter 2), insertion (Chapter 3), deletion — the hardest part (Chapter 4), all four traversal orders (Chapter 5), performance analysis including the dreaded worst case (Chapter 6), real-world implementations and self-balancing variants (Chapter 7), and an interview problem arsenal (Chapters 8-9).
A binary search tree is a binary tree (each node has at most two children) where every node satisfies one rule:
Think of each node as a decision point. If you are looking for a value, the current node tells you which way to go: left if smaller, right if larger. This is exactly how binary search works on a sorted array — but now the "middle element" is a node, and the "halves" are subtrees.
Here is a concrete example. Consider the keys [2, 5, 7, 10, 12, 15, 20]. One valid BST is:
But this is NOT the only valid BST for those keys. The same set of keys can form many different tree shapes depending on insertion order. For example, inserting in sorted order [2, 5, 7, 10, 12, 15, 20] produces a completely different tree — a straight chain leaning right, like a linked list. We will see why this matters in Chapter 6.
Each node in a BST stores four things:
python class Node: def __init__(self, key): self.key = key # the value stored at this node self.left = None # pointer to left child (smaller keys) self.right = None # pointer to right child (larger keys) self.parent = None # pointer to parent (needed for successor)
The parent pointer is optional — some implementations omit it and use recursion or a stack to navigate upward. CLRS includes it because it simplifies the successor and delete algorithms.
Here is the most important consequence of the BST property: if you visit every node by going left-first, then the current node, then right (this is called inorder traversal), you visit the keys in sorted order.
This is not a coincidence. The BST property guarantees that everything to the left is smaller and everything to the right is larger. Inorder traversal respects this ordering at every level of recursion.
python def inorder(node): """Visit all nodes in sorted order. O(n) time.""" if node is None: return inorder(node.left) # visit left subtree (all smaller keys) print(node.key) # visit current node inorder(node.right) # visit right subtree (all larger keys)
The simulation below lets you build your own BST. Type a number and press Insert. Click any node to verify the BST property — the left subtree highlights in one color (all smaller), the right subtree in another (all larger). Press "Inorder Walk" to see the sorted traversal animated.
Insert keys to build a tree. Click a node to verify the BST property. Press "Inorder Walk" to animate the sorted traversal.
Now that we have a BST, the first question is: how do we find a specific key? The answer falls straight out of the BST property.
Start at the root. Compare the target key k with the current node's key. If k equals the node's key, you found it. If k is smaller, go left. If k is larger, go right. If you hit a null pointer (fell off the tree), the key is not present.
python def search(node, k): """Search for key k in BST rooted at node. O(h) time.""" if node is None or node.key == k: return node # found it, or it doesn't exist if k < node.key: return search(node.left, k) # k is smaller — go left else: return search(node.right, k) # k is larger — go right
This traces a single path from root to some node (or to null). The number of comparisons equals the depth of the target node. The worst case is the tree's height h. So search is O(h).
python def search_iter(node, k): while node is not None and node.key != k: if k < node.key: node = node.left else: node = node.right return node
The BST property tells us exactly where the smallest and largest keys live. The minimum is the leftmost node — keep going left until you cannot go further. The maximum is the rightmost node.
python def minimum(node): """Go all the way left. O(h).""" while node.left is not None: node = node.left return node def maximum(node): """Go all the way right. O(h).""" while node.right is not None: node = node.right return node
The successor of a node x is the node with the smallest key greater than x.key — the next node in sorted order. Finding it has two cases:
Case 1: If x has a right subtree, the successor is the minimum of that right subtree. Think about it: everything in the right subtree is larger than x, and the minimum of that subtree is the smallest among those larger values.
Case 2: If x has no right subtree, the successor is the lowest ancestor of x whose left child is also an ancestor of x (or x itself). In other words: go up until you turn right. The first time you go up from a left child, that parent is the successor.
python def successor(x): """Find the node with the smallest key > x.key. O(h).""" if x.right is not None: return minimum(x.right) # Case 1: min of right subtree # Case 2: go up until we turn right y = x.parent while y is not None and x == y.right: x = y y = y.parent return y # None if x is the maximum
The predecessor is symmetric: if x has a left subtree, it is the maximum of the left subtree. Otherwise, go up until you turn left.
The simulation below lets you search for keys and find successors. Click "Search" to trace the search path. Click any node, then "Find Successor" to see the successor algorithm in action.
Enter a key to search. Click a node then "Find Successor" to see the successor algorithm step by step.
Inserting a new key into a BST is beautifully simple: do a search for the key, and when you fall off the tree (hit a null pointer), that is where the new node goes. The new node is always added as a leaf — it never displaces existing nodes.
Walk down from the root, going left or right at each node just like search. Keep track of the trailing parent — the last non-null node you visited. When you reach null, create the new node and attach it as the left or right child of that trailing parent.
python def insert(tree, z): """Insert node z into BST. z.key is already set. O(h).""" y = None # trailing parent x = tree.root # current node, starts at root # Phase 1: Walk down to find the insertion point while x is not None: y = x # remember the parent if z.key < x.key: x = x.left # go left else: x = x.right # go right # Phase 2: Attach the new node z.parent = y if y is None: tree.root = z # tree was empty elif z.key < y.key: y.left = z # attach as left child else: y.right = z # attach as right child
Let us insert 13 into this tree:
The key insight: insertion preserves the BST property automatically. By following the same path that search would take, we guarantee that the new node ends up in the correct position relative to every ancestor.
The simulation below shows insertion in real time. Type a key and watch it flow down the tree to its final position.
Type a key and press Insert. Watch the new node trace its path from root to its leaf position.
Insertion traces a single root-to-leaf path, just like search. The work is O(h), where h is the height. For a balanced tree with n nodes, h = O(log n), so insertion is O(log n). For a degenerate chain, h = O(n), so insertion becomes O(n) — no better than a sorted array.
Deletion is the hardest BST operation. Inserting a node only adds a leaf, but deleting a node might remove an internal node with children — and those children need to be reconnected without breaking the BST property. There are three cases.
The simplest case. The node has no children, so just remove it. Update the parent's pointer (left or right) to null.
The node has exactly one child. Bypass the node: connect the node's child directly to the node's parent. The child "moves up" to take the deleted node's place.
Why does this work? Node 2 was in the left subtree of 10, so it is ≤ 10. Node 5 was the left child of 10 and 2 was in 5's subtree, so 2 was already in 10's left subtree. The BST property is preserved.
This is the tricky case. The node has both a left and right child. You cannot just remove it — both subtrees need a parent. The solution: find the node's successor (the smallest node in the right subtree), swap it into the deleted node's position, then delete the successor from its original position.
Why the successor? Because it is the one node that can sit between the left subtree (all smaller) and the right subtree (all larger) without violating the BST property. And crucially: the successor has at most one child (it has no left child, because if it did, that left child would be smaller and the successor would not be the minimum of the right subtree).
CLRS defines a helper function transplant that replaces one subtree with another:
python def transplant(tree, u, v): """Replace subtree rooted at u with subtree rooted at v.""" if u.parent is None: tree.root = v # u was the root elif u == u.parent.left: u.parent.left = v # u was a left child else: u.parent.right = v # u was a right child if v is not None: v.parent = u.parent # update v's parent pointer
Now the full delete algorithm:
python def delete(tree, z): """Delete node z from BST. O(h).""" if z.left is None: transplant(tree, z, z.right) # Case 1 or 2a: no left child elif z.right is None: transplant(tree, z, z.left) # Case 2b: no right child else: # Case 3: two children y = minimum(z.right) # y = successor of z if y.parent != z: # y is deeper in the right subtree, not z's direct child transplant(tree, y, y.right) # replace y with y's right child y.right = z.right # y takes over z's right subtree y.right.parent = y transplant(tree, z, y) # replace z with y y.left = z.left # y takes over z's left subtree y.left.parent = y
The simulation below lets you delete any node. Click a node to select it, then press "Delete." The algorithm identifies the case and shows the operation step by step.
Click a node to select it (yellow highlight), then press "Delete." The algorithm will show which case applies.
We saw inorder traversal in Chapter 1 — it visits nodes in sorted order. But there are three other important ways to walk through a tree, and each one has distinct applications.
| Traversal | Visit Order | Recursive Rule | Use Case |
|---|---|---|---|
| Inorder | left, root, right | Visit left subtree, then current, then right | Sorted output, BST validation |
| Preorder | root, left, right | Visit current, then left subtree, then right | Serialization, copy a tree |
| Postorder | left, right, root | Visit left subtree, then right, then current | Delete a tree (children first), expression evaluation |
| Level-order | top-to-bottom, left-to-right | BFS using a queue | Print tree by levels, find width |
For the example tree [10, 5, 15, 2, 7, 12, 20]:
python def preorder(node): if node is None: return print(node.key) # visit BEFORE children preorder(node.left) preorder(node.right) def postorder(node): if node is None: return postorder(node.left) postorder(node.right) print(node.key) # visit AFTER children from collections import deque def level_order(root): if root is None: return q = deque([root]) while q: node = q.popleft() print(node.key) # visit level by level if node.left: q.append(node.left) if node.right: q.append(node.right)
The recursive approaches use O(h) stack space (either from the call stack or an explicit stack). Morris traversal achieves inorder traversal using O(1) extra space by temporarily threading the tree — making the rightmost node of each left subtree point back to the current node.
python def morris_inorder(root): """Inorder traversal with O(1) extra space.""" current = root while current is not None: if current.left is None: print(current.key) # no left subtree — visit now current = current.right else: # Find inorder predecessor (rightmost in left subtree) pred = current.left while pred.right is not None and pred.right != current: pred = pred.right if pred.right is None: # Thread: make pred point back to current pred.right = current current = current.left else: # Thread already exists — we've visited left subtree pred.right = None # unthread (restore tree) print(current.key) # visit now current = current.right
The simulation below shows all four traversals on the same tree. Watch the visit order animate for each one.
Select a traversal order and watch nodes light up in visit order.
Everything in a BST — search, insert, delete, successor — runs in O(h) time, where h is the tree's height. So the fundamental question is: how tall is the tree?
A complete binary tree with n nodes has height h = floor(log₂ n). Every level is fully packed except possibly the last. With n = 1,000,000 nodes, h = 19. That means search takes at most 19 comparisons. Extraordinary.
If you insert keys in sorted order (1, 2, 3, 4, ..., n), each new key is larger than all existing keys, so it always goes to the rightmost position. The result is a straight chain — every node has only a right child. The height equals n - 1. Search becomes O(n), exactly like scanning a linked list. You have lost every advantage of the tree structure.
If you insert n keys in random order (each of the n! permutations equally likely), the expected height is O(log n). More precisely, the expected height is approximately 4.311 · ln(n) ≈ 2.99 · log₂(n). This was proven by Reed (2003) after decades of research.
The intuition: random insertion is unlikely to produce long chains. Most of the time, about half the remaining keys go left and half go right at each node, giving a roughly balanced split. It is not perfectly balanced, but close enough that the height grows logarithmically.
| Scenario | Insertion Order | Height | Search Time |
|---|---|---|---|
| Best case | Median first, then medians of halves | floor(log₂ n) | O(log n) |
| Average case | Random permutation | ~2.99 log₂ n | O(log n) |
| Worst case | Sorted or reverse-sorted | n - 1 | O(n) |
This is the showcase simulation. You can insert keys manually, or generate a batch of random, sorted, or reverse-sorted keys. Watch how the tree shape changes dramatically based on insertion order. The height metric updates in real time.
Insert keys manually or generate batches. Compare random vs sorted insertion. Watch the height.
Plain BSTs are a theoretical foundation. Nobody ships them to production. Every real-world BST is a self-balancing variant that guarantees O(log n) height regardless of insertion order. Let us survey the major ones.
An AVL tree maintains a strict balance invariant: for every node, the heights of the left and right subtrees differ by at most 1. After every insertion or deletion, the tree checks the balance factor (height of left subtree minus height of right subtree) at each ancestor of the modified node. If the factor exceeds ±1, the tree performs rotations — local restructurings that restore balance.
| Property | Value |
|---|---|
| Balance invariant | |height(left) - height(right)| ≤ 1 at every node |
| Height guarantee | h ≤ 1.44 log₂(n+2) - 0.328 |
| Search | O(log n) — slightly faster than Red-Black due to stricter balance |
| Insert / Delete | O(log n), but up to O(log n) rotations per operation |
| Used in | C++ libstdc++ std::set (older versions), databases, memory allocators |
A Red-Black tree uses a looser balance invariant based on coloring nodes red or black. The five properties guarantee that no root-to-leaf path is more than twice as long as any other, which bounds the height to h ≤ 2 log₂(n+1).
| Property | Value |
|---|---|
| Balance invariant | 5 color properties (see CLRS Ch 13) |
| Height guarantee | h ≤ 2 log₂(n + 1) |
| Search | O(log n) — slightly slower than AVL (taller tree) |
| Insert / Delete | O(log n), at most 2-3 rotations per operation (faster than AVL) |
| Used in | C++ std::map, Java TreeMap/TreeSet, Linux kernel (CFS scheduler), Linux vm_area_struct |
B-trees are the workhorse of databases and file systems. Unlike binary trees (2 children per node), B-trees have up to thousands of children per node. Each node stores multiple keys, so a single disk read fetches hundreds of keys at once. This minimizes disk I/O — the bottleneck in database operations.
| Property | Value |
|---|---|
| Node capacity | t to 2t-1 keys per node (t = minimum degree) |
| Height | O(logt n) — much shorter than binary trees |
| Used in | PostgreSQL, MySQL (InnoDB), SQLite, NTFS, ext4, HFS+ |
| Why it wins | Disk-optimized: one node = one disk page = one I/O operation |
BSTs are not always the right answer. Hash tables beat them for exact-match lookups.
| Operation | BST (balanced) | Hash Table | Winner |
|---|---|---|---|
| Exact lookup | O(log n) | O(1) expected | Hash table |
| Range query ("all keys in [10, 50]") | O(log n + k) | O(n) | BST |
| Ordered iteration | O(n) | O(n log n) — must sort | BST |
| Find min/max | O(log n) or O(1) with augmentation | O(n) | BST |
| Find k-th smallest | O(log n) with order statistics | O(n) | BST |
| Worst-case guarantee | O(log n) if balanced | O(n) with pathological hash | BST |
Both structures have the same keys. Try a range query [lo, hi] — BST finds them efficiently, hash table must scan everything.
| Operation | Time (balanced) | Time (worst) | Space |
|---|---|---|---|
| Search | O(log n) | O(n) | O(1) iterative, O(h) recursive |
| Insert | O(log n) | O(n) | O(1) |
| Delete | O(log n) | O(n) | O(1) |
| Min / Max | O(log n) | O(n) | O(1) |
| Successor / Predecessor | O(log n) | O(n) | O(1) |
| Inorder traversal | O(n) | O(n) | O(h) stack or O(1) Morris |
| Build from n keys | O(n log n) | O(n²) | O(n) |
Given a binary tree, determine if it is a valid BST. The naive approach — checking each node's children — is WRONG. You must check that each node's value is within the valid range defined by all its ancestors.
python def is_valid_bst(node, lo=float('-inf'), hi=float('inf')): """Validate BST by passing valid range down. O(n) time, O(h) space.""" if node is None: return True if node.key <= lo or node.key >= hi: return False return (is_valid_bst(node.left, lo, node.key) and is_valid_bst(node.right, node.key, hi))
Find the k-th smallest element in a BST. Inorder traversal visits nodes in sorted order, so the k-th node visited is the answer.
python def kth_smallest(root, k): """O(h + k) time — traverse inorder, stop at k-th.""" stack = [] current = root count = 0 while stack or current: while current: stack.append(current) current = current.left current = stack.pop() count += 1 if count == k: return current.key current = current.right return None
Follow-up: if you need to answer many kth-smallest queries and the tree is modified between queries, augment each node with a size field (count of nodes in its subtree). Then kth smallest is O(log n) by comparing k with the left subtree size.
Given two nodes p and q in a BST, find their lowest common ancestor. The BST property makes this easy: if both p and q are smaller than the current node, the LCA is in the left subtree. If both are larger, it is in the right. Otherwise, the current node IS the LCA (or one of p, q is the current node).
python def lca(root, p, q): """O(h) time — follow the BST property.""" while root: if p.key < root.key and q.key < root.key: root = root.left # both in left subtree elif p.key > root.key and q.key > root.key: root = root.right # both in right subtree else: return root # split point = LCA
Serialize a BST to a string and reconstruct it. For a BST (not a general binary tree), preorder traversal alone is sufficient — the BST property lets you reconstruct the tree without null markers.
python def serialize(root): """Preorder traversal. O(n).""" if not root: return [] return [root.key] + serialize(root.left) + serialize(root.right) def deserialize(preorder): """Reconstruct BST from preorder. O(n) with bounds.""" def build(lo, hi): if not preorder or preorder[0] < lo or preorder[0] > hi: return None val = preorder.pop(0) node = Node(val) node.left = build(lo, val) node.right = build(val, hi) return node return build(float('-inf'), float('inf'))
Given a preorder traversal of a BST, reconstruct the tree. This is the same as deserialize above. The first element is the root. All subsequent elements smaller than the root go to the left subtree; all larger go to the right subtree. Recurse.
"Our BST search returns the wrong result after deletion." Systematic diagnosis:
| Pattern | Problems | Key Technique |
|---|---|---|
| Validate | LC 98: Validate BST | Pass min/max bounds downward |
| Search / Query | LC 700: Search in BST, LC 230: Kth Smallest | Inorder traversal, augmented size |
| Construction | LC 1008: BST from Preorder, LC 108: Sorted Array to BST | Preorder + bounds, or binary divide |
| LCA | LC 235: LCA of BST | Split-point logic using BST property |
| Modify | LC 450: Delete Node, LC 701: Insert into BST | Transplant, successor replacement |
| Iterator | LC 173: BST Iterator | Controlled inorder with explicit stack |
The tree below may or may not be a valid BST. Click "Validate" to run the bounds-checking algorithm. Click "Scramble" to generate a new (possibly invalid) tree.
Binary search trees are the foundation on which much of computer science is built. Every self-balancing tree, every database index, every ordered collection in every standard library traces back to the ideas in this chapter.
| Topic | Key Takeaway |
|---|---|
| BST Property | Left subtree ≤ node ≤ right subtree — enables binary search on a linked structure |
| Search / Min / Max / Successor | All O(h) — trace one root-to-leaf path |
| Insertion | Search for the position, add as leaf. O(h). Insertion order determines shape. |
| Deletion | Three cases: leaf, one child (bypass), two children (replace with successor). O(h). |
| Traversals | Inorder = sorted, Preorder = serialization, Postorder = safe deletion, Level-order = BFS |
| Performance | h = O(log n) average, O(n) worst. Self-balancing variants guarantee O(log n). |
| Extension | What It Adds | Chapter / Lesson |
|---|---|---|
| Red-Black Trees | Guaranteed O(log n) height via color properties and rotations | CLRS Ch 13 |
| AVL Trees | Strictest balance (height difference ≤ 1), fastest lookups | Often covered alongside Red-Black |
| B-Trees | Disk-optimized, thousands of keys per node, powers all databases | CLRS Ch 18 |
| Augmented BSTs | Store extra info (subtree size, sum) for rank queries, interval queries | CLRS Ch 14 |
| Treaps & Skip Lists | Randomized alternatives with simpler rebalancing logic | Advanced DS courses |
CLRS Ch 11: Hash Tables — the alternative when you only need exact-match lookups. O(1) expected but no ordered operations.
CLRS Ch 7: Quicksort — quicksort's partition is the sorting analog of BST insertion. Inserting n random keys into a BST produces the same comparisons as quicksort on those keys.
CLRS Ch 15: Dynamic Programming — optimal BST construction (Section 15.5) uses DP to find the tree shape that minimizes expected search cost.
"An algorithm must be seen to be believed." — Donald Knuth