Introduction to Algorithms (CLRS) — Chapter 12

Binary Search Trees

Search, insert, delete, traverse — the data structure that makes ordered data fast.

Prerequisites: Binary trees + Recursion. That's it.
10
Chapters
9+
Simulations
5
Interview Dimensions

Chapter 0: The Problem

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.

OperationSorted ArrayLinked ListWhat we want
SearchO(log n)O(n)O(log n)
InsertO(n)O(1)*O(log n)
DeleteO(n)O(1)*O(log n)
Min / MaxO(1)O(n)O(log n)
Sorted outputO(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 answer is a tree. A binary search tree (BST) organizes data in a tree structure where each node stores a key, and the tree's shape encodes the sort order. Searching works like binary search — at each node, go left or right. Inserting works like linked list insertion — just add a new leaf. The result: O(h) for search, insert, and delete, where h is the tree's height. If the tree is balanced, h = O(log n), and we get the best of both worlds.

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.

Sorted Array vs Linked List vs BST

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).

Concept check: You need a data structure that supports search, insert, and delete — all in O(log n) time — and can also produce all elements in sorted order in O(n). Which data structure achieves this?

Chapter 1: The BST Property

A binary search tree is a binary tree (each node has at most two children) where every node satisfies one rule:

The BST property. For every node x in the tree: all keys in x's left subtree are ≤ x.key, and all keys in x's right subtree are ≥ x.key. This is not just about the immediate children — it applies to the entire left and right subtrees, all the way down.

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:

10 / \ 5 15 / \ / \ 2 7 12 20 Check node 10: left subtree {2,5,7} all ≤ 10, right subtree {12,15,20} all ≥ 10. ✓ Check node 5: left subtree {2} ≤ 5, right subtree {7} ≥ 5. ✓ Check node 15: left subtree {12} ≤ 15, right subtree {20} ≥ 15. ✓

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.

The Node Structure

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.

Inorder Traversal Produces Sorted Output

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.

// Inorder traversal of the tree above: Left of 10 → Left of 5 → 25 → Right of 5 → 710 → Left of 15 → 1215 → Right of 15 → 20 Output: 2, 5, 7, 10, 12, 15, 20 ← sorted!

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)
Why inorder traversal is O(n). Each node is visited exactly once (printed once), and each edge is traversed exactly twice (once going down, once going back up). A tree with n nodes has exactly n-1 edges. Total work: n prints + 2(n-1) edge traversals = O(n). No wasted work.

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.

Interactive BST Builder

Insert keys to build a tree. Click a node to verify the BST property. Press "Inorder Walk" to animate the sorted traversal.

Concept check: In a BST, node X has key 15. Its left child has key 10, and 10's right child has key 17. Is this a valid BST?

Chapter 2: Search, Min, Max, Successor, Predecessor

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.

Search

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).

Iterative version (preferred in practice). Recursion uses O(h) stack space. The iterative version uses O(1) space — just a while loop and a pointer. Same logic, no stack overhead.
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

Minimum and Maximum

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

Successor and Predecessor

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.

// Example: successor of 7 in this tree 10 / \ 5 15 / \ / \ 2 7 12 20 Node 7 has no right subtree (Case 2). Go up: 7 is the RIGHT child of 5 — keep going. Go up: 5 is the LEFT child of 10 — stop! Successor of 7 is 10. ✓ (next in sorted order: 2,5,7,10,12,15,20)
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.

All queries are O(h). Search, min, max, successor, predecessor — every single one traces at most one root-to-leaf path. The running time is O(h), where h is the height of the tree. If the tree is balanced, h = O(log n). If the tree is a chain, h = O(n). This is why tree height is everything.

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.

Search & Successor Explorer

Enter a key to search. Click a node then "Find Successor" to see the successor algorithm step by step.

Concept check: In a BST, node 25 has a right child 30 which has a left child 27 which has a left child 26. What is the successor of 25?

Chapter 3: Insertion

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.

The Algorithm

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

Worked Example

Let us insert 13 into this tree:

10 / \ 5 15 / \ / \ 2 7 12 20 Step 1: Compare 13 with root 10. 13 > 10, go right. Step 2: Compare 13 with 15. 13 < 15, go left. Step 3: Compare 13 with 12. 13 > 12, go right. Step 4: 12's right child is null. Insert 13 here. Result: 10 / \ 5 15 / \ / \ 2 7 12 20 \ 13

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.

Insertion order determines tree shape. Inserting [10, 5, 15, 2, 7, 12, 20] builds a balanced tree of height 2. Inserting the same keys in sorted order [2, 5, 7, 10, 12, 15, 20] builds a chain of height 6 (a degenerate tree that is just a linked list). Same keys, same BST property, wildly different performance. This is the fundamental weakness of plain BSTs — they have no mechanism to stay balanced.

The simulation below shows insertion in real time. Type a key and watch it flow down the tree to its final position.

Animated BST Insertion

Type a key and press Insert. Watch the new node trace its path from root to its leaf position.

Time Complexity

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.

Concept check: You insert keys [3, 1, 5, 2, 4] into an empty BST in that order. What is the resulting tree structure?

Chapter 4: Deletion

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.

Case 1: Deleting a Leaf (No Children)

The simplest case. The node has no children, so just remove it. Update the parent's pointer (left or right) to null.

Delete 7: Before: After: 10 10 / \ / \ 5 15 5 15 / \ / \ / / \ 2 7 12 20 2 12 20 Node 7 is a leaf. Just remove it. Parent 5's right becomes null.

Case 2: Deleting a Node with One Child

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.

Delete 5 (one child, left=2): 10 10 / \ / \ 5 15 2 15 / / \ / \ 2 12 20 12 20 Node 5 has one child (2). Replace 5 with 2. Parent 10's left now points to 2.

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.

Case 3: Deleting a Node with Two Children

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).

Delete 10 (two children): 10 12 / \ / \ 5 15 5 15 / \ / \ / \ \ 2 7 12 20 2 7 20 Step 1: Successor of 10 is 12 (min of right subtree). Step 2: 12 has no left child, so it can be spliced out easily. Step 3: Replace 10 with 12 in the tree.

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
Why not the predecessor? You could use the predecessor (max of left subtree) instead of the successor. Both work. CLRS uses the successor by convention. Some implementations randomly choose between successor and predecessor to avoid systematic imbalance when many deletions happen.

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.

BST Deletion: All Three Cases

Click a node to select it (yellow highlight), then press "Delete." The algorithm will show which case applies.

Concept check: You delete a node with two children. The successor has a right child. What happens to that right child?

Chapter 5: Tree Traversals

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.

The Four Traversal Orders

TraversalVisit OrderRecursive RuleUse Case
Inorderleft, root, rightVisit left subtree, then current, then rightSorted output, BST validation
Preorderroot, left, rightVisit current, then left subtree, then rightSerialization, copy a tree
Postorderleft, right, rootVisit left subtree, then right, then currentDelete a tree (children first), expression evaluation
Level-ordertop-to-bottom, left-to-rightBFS using a queuePrint tree by levels, find width

For the example tree [10, 5, 15, 2, 7, 12, 20]:

10 / \ 5 15 / \ / \ 2 7 12 20 Inorder: 2, 5, 7, 10, 12, 15, 20 ← sorted Preorder: 10, 5, 2, 7, 15, 12, 20 ← root first Postorder: 2, 7, 5, 12, 20, 15, 10 ← root last Level-order: 10, 5, 15, 2, 7, 12, 20 ← breadth-first
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)

Morris Traversal: O(1) Space Inorder

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
When to use Morris traversal. In practice, you rarely need it — O(h) stack space is fine for most trees. But it appears in interviews as a "can you do this with O(1) space?" follow-up, and it is used in embedded systems where stack depth is severely limited.

The simulation below shows all four traversals on the same tree. Watch the visit order animate for each one.

Four Traversals Side-by-Side

Select a traversal order and watch nodes light up in visit order.

Concept check: You need to delete an entire BST and free each node's memory. Which traversal order is correct: inorder, preorder, or postorder?

Chapter 6: BST Performance

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?

Best Case: Balanced 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.

// Height of a complete binary tree n = 2h+1 - 1 (for a full tree with all levels complete) h = floor(log₂ n) n = 7 → h = 2 n = 15 → h = 3 n = 1023 → h = 9 n = 106 → h = 19

Worst Case: Degenerate Tree

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.

// Insert 1, 2, 3, 4, 5 in order: 1 \ 2 \ 3 \ 4 \ 5 h = 4 = n - 1 ← worst case: a linked list!

Average Case: Random Insertions

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.

The critical insight. A plain BST has no defense against adversarial input. If someone inserts keys in sorted order (which is extremely common — think auto-incrementing database IDs, timestamps, alphabetically sorted names), the tree degenerates. This is why production systems never use plain BSTs. They use self-balancing BSTs (Red-Black trees, AVL trees) that guarantee h = O(log n) regardless of insertion order. Chapter 13 of CLRS covers Red-Black trees.
ScenarioInsertion OrderHeightSearch Time
Best caseMedian first, then medians of halvesfloor(log₂ n)O(log n)
Average caseRandom permutation~2.99 log₂ nO(log n)
Worst caseSorted or reverse-sortedn - 1O(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.

THE BIG SIM: Tree Shape vs Insertion Order

Insert keys manually or generate batches. Compare random vs sorted insertion. Watch the height.

Batch size 15
Concept check: You are given n = 1024 keys. In the worst case (sorted insertion), how many comparisons does searching for a key take? In the best case (perfectly balanced)?

Chapter 7: BSTs in Practice

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.

AVL Trees (Adelson-Velsky and Landis, 1962)

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.

PropertyValue
Balance invariant|height(left) - height(right)| ≤ 1 at every node
Height guaranteeh ≤ 1.44 log₂(n+2) - 0.328
SearchO(log n) — slightly faster than Red-Black due to stricter balance
Insert / DeleteO(log n), but up to O(log n) rotations per operation
Used inC++ libstdc++ std::set (older versions), databases, memory allocators

Red-Black Trees (Bayer 1972, Guibas & Sedgewick 1978)

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).

PropertyValue
Balance invariant5 color properties (see CLRS Ch 13)
Height guaranteeh ≤ 2 log₂(n + 1)
SearchO(log n) — slightly slower than AVL (taller tree)
Insert / DeleteO(log n), at most 2-3 rotations per operation (faster than AVL)
Used inC++ std::map, Java TreeMap/TreeSet, Linux kernel (CFS scheduler), Linux vm_area_struct
AVL vs Red-Black: the trade-off. AVL trees are more strictly balanced, so lookups are slightly faster (shorter tree). But they require more rotations on insertion/deletion. Red-Black trees are more relaxed — slightly taller, but insertions and deletions are faster because rebalancing requires at most 2-3 rotations (vs up to O(log n) for AVL). In practice, Red-Black wins for workloads with many insertions/deletions (operating systems, databases). AVL wins for read-heavy workloads (lookup-heavy indexes).

B-Trees (Bayer & McCreight, 1972)

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.

PropertyValue
Node capacityt to 2t-1 keys per node (t = minimum degree)
HeightO(logt n) — much shorter than binary trees
Used inPostgreSQL, MySQL (InnoDB), SQLite, NTFS, ext4, HFS+
Why it winsDisk-optimized: one node = one disk page = one I/O operation

When NOT to Use BSTs

BSTs are not always the right answer. Hash tables beat them for exact-match lookups.

OperationBST (balanced)Hash TableWinner
Exact lookupO(log n)O(1) expectedHash table
Range query ("all keys in [10, 50]")O(log n + k)O(n)BST
Ordered iterationO(n)O(n log n) — must sortBST
Find min/maxO(log n) or O(1) with augmentationO(n)BST
Find k-th smallestO(log n) with order statisticsO(n)BST
Worst-case guaranteeO(log n) if balancedO(n) with pathological hashBST
Rule of thumb. Use a hash table when you only need exact-match lookups (dictionary, set membership, deduplication). Use a BST when you need ordered operations: range queries, sorted iteration, predecessor/successor, or rank queries. If you need both, many databases use a B-tree index for ordered queries and a hash index for exact lookups.
BST vs Hash Table: Range Query

Both structures have the same keys. Try a range query [lo, hi] — BST finds them efficiently, hash table must scan everything.

to
Concept check: You are building an in-memory database that needs to support: (1) exact key lookup, (2) range queries, and (3) ordered iteration. Which data structure?

Chapter 8: Interview Arsenal

Operation Cheat Sheet

OperationTime (balanced)Time (worst)Space
SearchO(log n)O(n)O(1) iterative, O(h) recursive
InsertO(log n)O(n)O(1)
DeleteO(log n)O(n)O(1)
Min / MaxO(log n)O(n)O(1)
Successor / PredecessorO(log n)O(n)O(1)
Inorder traversalO(n)O(n)O(h) stack or O(1) Morris
Build from n keysO(n log n)O(n²)O(n)

The Five Core Interview Problems

1. Validate BST

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))
The classic mistake. Checking only "left child < node < right child" is insufficient. Consider: root=10, left=5, and 5's right child=17. 17 > 5 passes the local check, but 17 is in 10's left subtree, violating the global BST property. You MUST pass bounds downward.

2. Kth Smallest Element

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.

3. Lowest Common Ancestor (LCA)

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

4. Serialize and Deserialize BST

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'))

5. Construct BST from Preorder

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.

Debug Scenario

"Our BST search returns the wrong result after deletion." Systematic diagnosis:

1. Validate BST property
Run is_valid_bst() on the tree after deletion. If it fails, the delete operation broke the BST property — check the transplant logic and parent pointer updates.
2. Check deletion case 3
The most common bug: when deleting a node with two children, forgetting to update the successor's parent pointer, or not handling the case where the successor is not the direct right child.
3. Check parent pointers
After every transplant, verify that v.parent = u.parent is set correctly. Missing parent pointer updates cause successor/predecessor to loop or crash.
4. Check for duplicates
If the BST stores duplicates, search might return a different copy than the one you expect. Decide on a policy: all duplicates go left, all go right, or store a count at each node.

LeetCode Pattern Map

PatternProblemsKey Technique
ValidateLC 98: Validate BSTPass min/max bounds downward
Search / QueryLC 700: Search in BST, LC 230: Kth SmallestInorder traversal, augmented size
ConstructionLC 1008: BST from Preorder, LC 108: Sorted Array to BSTPreorder + bounds, or binary divide
LCALC 235: LCA of BSTSplit-point logic using BST property
ModifyLC 450: Delete Node, LC 701: Insert into BSTTransplant, successor replacement
IteratorLC 173: BST IteratorControlled inorder with explicit stack
BST Validation Tester

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.

Concept check: To validate a BST, why is it wrong to only check that left child < node < right child at every node?

Chapter 9: Connections

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.

What We Covered

TopicKey Takeaway
BST PropertyLeft subtree ≤ node ≤ right subtree — enables binary search on a linked structure
Search / Min / Max / SuccessorAll O(h) — trace one root-to-leaf path
InsertionSearch for the position, add as leaf. O(h). Insertion order determines shape.
DeletionThree cases: leaf, one child (bypass), two children (replace with successor). O(h).
TraversalsInorder = sorted, Preorder = serialization, Postorder = safe deletion, Level-order = BFS
Performanceh = O(log n) average, O(n) worst. Self-balancing variants guarantee O(log n).

Where BSTs Lead

ExtensionWhat It AddsChapter / Lesson
Red-Black TreesGuaranteed O(log n) height via color properties and rotationsCLRS Ch 13
AVL TreesStrictest balance (height difference ≤ 1), fastest lookupsOften covered alongside Red-Black
B-TreesDisk-optimized, thousands of keys per node, powers all databasesCLRS Ch 18
Augmented BSTsStore extra info (subtree size, sum) for rank queries, interval queriesCLRS Ch 14
Treaps & Skip ListsRandomized alternatives with simpler rebalancing logicAdvanced DS courses

Related Lessons

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.

The big picture. BSTs teach you a universal principle: the shape of a data structure determines its performance. The same set of keys can give O(log n) or O(n) operations depending on how the tree is built. This tension between structure and efficiency appears everywhere: balanced vs unbalanced trees, hash table load factors, cache-oblivious data structures, and neural network architectures. The solution is always the same: add constraints that prevent degenerate shapes.

"An algorithm must be seen to be believed." — Donald Knuth

Final check: Why do production systems use Red-Black trees instead of plain BSTs?