Binary Search Trees (BST)
BST ordering invariant, search and insertion mechanics, and the 3-case deletion algorithm (no child, 1 child, 2 children).
Reinforce your mental model: run, pause, and inspect live pointers with our interactive visualizer:
A Binary Search Tree (BST) bridges dynamic pointer-based structures and binary search efficiency by enforcing a strict recursive partitioning invariant across every node. Mastering its pointer surgery, three-case deletion mechanics, and ancestor-climbing navigation establishes the foundational mechanics for all balanced search tree engines.
1. Executive Summary & Learning Objectives
#A Binary Search Tree (BST) is an explicitly ordered hierarchical data structure where every node enforces a symmetric comparison invariant between its key and all keys residing in its subtrees.
By the end of this chapter, you will be able to:
- Formulate the Binary Search Tree invariant and prove that an inorder traversal produces keys in strictly increasing order.
- Implement pointer-based search, insertion, and the three canonical deletion cases with zero memory leaks.
- Trace search, insert, and delete executions step-by-step through a concrete dry-run trace table.
- Identify the structural conditions causing tree height to degrade to and motivate self-balancing variants (AVL, Red-Black).
- Compute inorder successors and predecessors across both ancestor-climbing and subtree-descending configurations.
2. Structural Invariant & The Inorder Monotonicity Theorem
#Definition: The Binary Search Property
#Let be a node in a binary search tree. If is a node in the left subtree of , then . If is a node in the right subtree of , then . In strict BST models with unique keys:
Canonical Binary Search Tree Topology
#The table below describes the topological layout of a balanced sample binary search tree containing keys :
| Node Key | Parent | Left Child | Right Child | Subtree Key Range | Node Classification |
|---|---|---|---|---|---|
| 50 | null | 30 | 70 | Root Node | |
| 30 | 50 | 20 | 40 | Internal Node (Left Branch) | |
| 70 | 50 | 60 | 80 | Internal Node (Right Branch) | |
| 20 | 30 | null | null | Leaf Node | |
| 40 | 30 | null | null | Leaf Node | |
| 60 | 70 | null | null | Leaf Node | |
| 80 | 70 | null | null | Leaf Node |
Inorder Traversal Sequence: (Monotonically Increasing).
Theorem: Inorder Traversal Monotonicity
#An inorder tree walk on a binary search tree visits the keys in monotonically non-decreasing order.
Proof by Structural Induction:
- Base Case: If is empty, the sequence is trivially empty and sorted.
- Inductive Hypothesis: Assume inorder traversal correctly outputs keys in non-decreasing order for all trees with nodes.
- Inductive Step: Consider a tree of size with root . Inorder traversal visits:
- All nodes in in non-decreasing order (by induction).
- The root key .
- All nodes in in non-decreasing order (by induction). By the binary search property, every key in is , and is every key in . Therefore, the combined sequence is strictly sorted.
3. Search and Insertion Mechanics
#Search Algorithm ( time)
#Starting from root, we compare search key with current node :
- If or , return .
- If , transition to .
- If , transition to .
export class BSTNode<T> {
key: T;
left: BSTNode<T> | null = null;
right: BSTNode<T> | null = null;
parent: BSTNode<T> | null = null;
constructor(key: T, parent: BSTNode<T> | null = null) {
this.key = key;
this.parent = parent;
}
}
export function searchBST<T>(root: BSTNode<T> | null, target: T): BSTNode<T> | null {
let curr = root;
while (curr !== null && curr.key !== target) {
curr = target < curr.key ? curr.left : curr.right;
}
return curr;
}Insertion Algorithm ( time)
#A new node is always inserted as a new leaf:
- Scan down the tree using two pointers:
currand its trailingparent. - Determine whether the new node attaches as the left or right child of
parent. - Allocate and link the new node.
export function insertBST<T>(root: BSTNode<T> | null, key: T): BSTNode<T> {
const newNode = new BSTNode(key);
if (root === null) return newNode;
let parent: BSTNode<T> | null = null;
let curr: BSTNode<T> | null = root;
while (curr !== null) {
parent = curr;
if (key < curr.key) {
curr = curr.left;
} else if (key > curr.key) {
curr = curr.right;
} else {
return root; // Duplicate key: ignore or handle frequency count
}
}
newNode.parent = parent;
if (parent !== null) {
if (key < parent.key) {
parent.left = newNode;
} else {
parent.right = newNode;
}
}
return root;
}4. Deletion: The Three Canonical Structural Cases
#When deleting node , three topological cases arise depending on the child degree of :
| Case | Node Topology | Surgical Pointer Action | Example in Sample Tree | Time Complexity |
|---|---|---|---|---|
| Case 1: Zero Children | is a leaf (z.left == null and z.right == null) | Set the corresponding child pointer of 's parent to null. Deallocate node . | Deleting node : set . | post-search |
| Case 2: One Child | has exactly one non-null child | Splice out by connecting 's parent directly to . Reassign . | Deleting node if absent: link . | post-search |
| Case 3: Two Children | has both left and right children | Find 's inorder successor (). Replace with . Delete from right subtree (which has at most one child, falling into Case 1 or 2). | Deleting root : successor is . Overwrite , delete from right subtree. |
export function deleteBST<T>(root: BSTNode<T> | null, target: T): BSTNode<T> | null {
if (root === null) return null;
if (target < root.key) {
root.left = deleteBST(root.left, target);
} else if (target > root.key) {
root.right = deleteBST(root.right, target);
} else {
// Case 1 & Case 2: 0 or 1 child
if (root.left === null) return root.right;
if (root.right === null) return root.left;
// Case 3: 2 children — Find inorder successor (minimum key in right subtree)
let successor = root.right;
while (successor.left !== null) {
successor = successor.left;
}
root.key = successor.key;
root.right = deleteBST(root.right, successor.key);
}
return root;
}5. Complete Step-by-Step Dry Run State Trace Table
#Consider the tree populated with keys .
Execution: Delete Node 50 (Case 3: Two Children)
#| Step | Current Operation | Active Pointers | Comparison / Invariant Check | State Mutation |
|---|---|---|---|---|
| 1 | Locate target key | curr = root (50) | Target node located. Both curr.left (30) and curr.right (70) are non-NULL (Case 3). | |
| 2 | Locate Inorder Successor | succ = curr.right (70) | Check succ.left | succ.left points to node . Advance succ = succ.left (60). |
| 3 | Successor Terminal Check | succ = 60 | Check succ.left | Node has left == NULL. Node is the minimum key in the right subtree. |
| 4 | Key Replacement | curr.key | Overwrite root key | root.key updated from . |
| 5 | Splice Successor Out | root.right | Delete key from right subtree | Node is a leaf (Case 1). Subtree parent () updates . |
| 6 | Verification | Inorder Traversal | Check sorted invariant | Resulting traversal: . Invariant maintained. |
6. Mathematical Invariants: Minimum, Maximum, Successor, Predecessor
#Inorder Successor Rules
#To find the successor of node without an inorder traversal:
- If has a non-empty right subtree: The successor is the node with the minimum key in .
- If has an empty right subtree: The successor is the lowest ancestor of whose left child is also an ancestor of .
export function treeSuccessor<T>(node: BSTNode<T>): BSTNode<T> | null {
if (node.right !== null) {
let curr = node.right;
while (curr.left !== null) curr = curr.left;
return curr;
}
let curr: BSTNode<T> | null = node;
let ancestor = node.parent;
while (ancestor !== null && curr === ancestor.right) {
curr = ancestor;
ancestor = ancestor.parent;
}
return ancestor;
}Successor Ancestor-Walk Trace: Find Successor of Node 40
#In our sample tree, node has no right child (40.right == null):
| Iteration | Current Pointer curr | Ancestor Pointer ancestor | Condition Check (curr === ancestor.right) | Action Taken |
|---|---|---|---|---|
| Init | node = 40 | ancestor = 40.parent (30) | (True) | Node is a right child: climb up to . |
| 1 | curr = 30 | ancestor = 30.parent (50) | (False; ) | Loop terminates! Lowest ancestor where branch was left child is . |
| Result | — | ancestor = 50 | — | Successor of is . |
Inorder Predecessor Rules
#- If has a non-empty left subtree: The predecessor is the node with the maximum key in .
- If has an empty left subtree: The predecessor is the lowest ancestor of whose right child is also an ancestor of .
7. Asymptotic Complexity & Height Degradation
#The running time of Search, Insert, and Delete is , where is the height of the tree.
| Scenario | Tree Topology | Height | Search Time | Insertion Time | Deletion Time |
|---|---|---|---|---|---|
| Best Case | Complete / Perfectly Balanced | ||||
| Average Case | Randomly Built BST | ||||
| Worst Case | Strictly Skewed (Degenerate) |
Why Simple BSTs Degrade to
#If keys are inserted in strictly ascending order () or strictly descending order (), each node is appended as a right-only or left-only child. The tree topology degenerates into a linear singly linked list, mirroring the worst-case partitioning behavior of Quicksort. This limitation directly motivates self-balancing binary search trees (AVL Trees and Red-Black Trees).
8. Common Traps, Edge Cases & Implementation Pitfalls
#- Root Node Deletion:
- When deleting the root node in Case 1 or Case 2, the caller's root pointer must be reassigned. Failure to update the reference causes memory leaks or orphaned subtrees.
- Duplicate Key Management:
- Standard mathematical BSTs disallow duplicate keys. If duplicates are required, store a
countfrequency field within each node rather than allocating redundant identical nodes, which unbalances tree height.
- Standard mathematical BSTs disallow duplicate keys. If duplicates are required, store a
- Integer Overflow in Comparison:
- Computing key differences via subtraction (
a.key - b.key) causes signed integer overflow when comparing large positive and negative values. Always use explicit comparisons (a.key < b.key ? -1 : 1).
- Computing key differences via subtraction (
- Stray Parent Pointers:
- In implementations maintaining parent pointers, splicing a node out without updating its child's parent pointer creates circular references or invalid ancestor walks.
9. Real-World Applications & Practice Problems
#Production Systems
#- Virtual Memory Region Management: The Linux kernel manages process virtual memory areas (
vm_area_struct) using balanced BSTs to quickly check whether a faulting memory address belongs to a mapped memory page. - Relational Database Indices: Early indexing engines used in-memory BSTs prior to the adoption of block-oriented B-trees for secondary storage.
Standard Practice Roadmap
#- Validate Binary Search Tree (LeetCode 98) — Verify range bounds recursively.
- Lowest Common Ancestor of a BST (LeetCode 235) — Exploit BST ordering property in time without hash sets.
- Kth Smallest Element in a BST (LeetCode 230) — Inorder traversal with counter or augmented subtree sizes.
- Delete Node in a BST (LeetCode 450) — Implementation of the 3 canonical deletion cases.
10. References & Academic Attribution
#- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.), Chapters 12–13 (BSTs and Red-Black Trees) & Chapter 18 (B-Trees). MIT Press.
- Bayer, R., & McCreight, E. (1972). Organization and maintenance of large ordered indices. Acta Informatica, 1(3), 173–189.
- Sleator, D. D., & Tarjan, R. E. (1985). Self-adjusting binary search trees. Journal of the ACM (JACM), 32(3), 652–686.