Binary Search Trees
A binary search tree keeps smaller keys left and larger keys right, so search, insert and order statistics walk a single path. Learn the operations, the correct way to validate a BST, how languages expose balanced trees, and watch range validation, k-th smallest and BST LCA animated.
🎯 What you'll learn
- Search, insert and delete in O(h)
- Validating with inherited ranges (not just parent–child)
- Inorder order statistics and balanced construction
- Six LeetCode problems with animated walkthroughs
📚 Before you start
A binary search tree keeps an ordering invariant: everything in a node's left subtree is smaller, everything in its right subtree is larger. That turns search, insert and delete into a single walk down one path, O(h) for height h, and makes an inorder traversal produce sorted order. Interview questions test whether you can exploit that invariant (skip half the tree at every node) and whether you validate it correctly.
1Operations
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function search(root: TreeNode | null, target: number): TreeNode | null {
let node = root;
while (node && node.val !== target) node = target < node.val ? node.left : node.right;
return node;
}
function insert(root: TreeNode | null, val: number): TreeNode {
if (!root) return new TreeNode(val);
if (val < root.val) root.left = insert(root.left, val);
else root.right = insert(root.right, val);
return root;
}
function isValid(node: TreeNode | null, low = -Infinity, high = Infinity): boolean {
if (!node) return true;
if (!(low < node.val && node.val < high)) return false;
return isValid(node.left, low, node.val) && isValid(node.right, node.val, high);
}
function bstOps(level: (number | null)[], x: number): any[] {
const root = _tree(level);
insert(root, x);
return [search(root, x) !== null, isValid(root), _level(root)];
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def search(root, target):
node = root
while node and node.val != target:
node = node.left if target < node.val else node.right
return node
def insert(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert(root.left, val)
else:
root.right = insert(root.right, val)
return root
def is_valid(node, low=float("-inf"), high=float("inf")):
if not node:
return True
if not (low < node.val < high):
return False
return is_valid(node.left, low, node.val) and is_valid(node.right, node.val, high)
def bst_ops(level, x):
root = _tree(level)
insert(root, x)
return [search(root, x) is not None, is_valid(root), _level(root)]| Operation | Balanced BST | Degenerate (sorted inserts) |
|---|---|---|
| search / insert / delete | O(log n) | O(n) |
| min / max | O(log n): walk left/right | O(n) |
| inorder (sorted) iteration | O(n) | O(n) |
| k-th smallest | O(h + k), or O(log n) with subtree sizes | O(n) |
Delete has three cases: a leaf (remove it), one child (splice the child up), two children (replace the value with the inorder successor, the minimum of the right subtree, then delete that successor).
2Validating a BST Correctly
Checking only left.val < node.val < right.val is wrong: a node deep in the left subtree must also be smaller than every ancestor it sits to the left of. Pass a valid range (low, high) down the tree: going left tightens high to the parent's value, going right tightens low. Equivalently, an inorder traversal must be strictly increasing.
3Using the Invariant
- Search-like problems (LCA in a BST, closest value, floor/ceiling): at each node decide left or right, so you only walk one path.
- Order statistics (k-th smallest, two-sum in BST, recover swapped nodes): use inorder traversal, often iteratively so you can stop early.
- Construction (sorted array → balanced BST): take the middle element as root, recurse on halves.
4Balanced BSTs and Sorted Containers
Real systems keep BSTs balanced with rotations: AVL trees (strict height balance) or red-black trees (looser, fewer rotations). You'll rarely implement one in an interview, but know what your language provides:
| Language | Ordered set / map |
|---|---|
| Python | none built in; bisect on a sorted list (O(n) insert), or sortedcontainers.SortedList |
| JavaScript / TypeScript | none built in; a sorted array + binary search, or a library |
| C# | SortedSet<T>, SortedDictionary<K,V> (red-black trees), SortedList<K,V> (array) |
| Java | TreeSet, TreeMap |
Under the HoodWhy Databases Use B-Trees, Not BSTs▾
A BST node holds one key, so a million keys need about 20 levels, which means up to 20 random disk reads per lookup. A B-tree node holds hundreds of keys (one disk page), so the same million keys fit in about 3 levels. Same invariant, wider nodes: see databases/indexing-storage/02-b_tree.
⚠️ Common Pitfalls
- Validating only parent–child pairs instead of the full ancestor range.
- Using
intmin/max as sentinels when node values can equal them; useNone/nullor ±infinity. - Duplicates: decide (and say) whether they go left, right, or are disallowed.
- Assuming O(log n) for an unbalanced BST; sorted inserts make it a linked list.
- Forgetting to return the (possibly new) root from recursive insert/delete.
📋 Quick Reference
search: while node and node.val != x: node = left if x < node.val else right
validate: f(node, low, high): low < val < high; left gets (low, val), right gets (val, high)
inorder of a BST = sorted order · delete with 2 children → inorder successor
O(h): log n balanced, n degenerateSix problems that exploit the BST invariant: one-path walks, range validation, inorder order statistics and balanced construction.
1Search in a Binary Search Tree (LeetCode 700)
🟢 Easy. Return the subtree rooted at the node with value val, or null.
root = [4,2,7,1,3], val = 2 → [2,1,3]Approach. Walk down: go left if val is smaller than the current node, right if larger.
Complexity. O(h) time, O(1) space iteratively.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function searchBST(root: TreeNode | null, val: number): TreeNode | null {
while (root && root.val !== val) root = val < root.val ? root.left : root.right;
return root;
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def search_bst(root, val):
while root and root.val != val:
root = root.left if val < root.val else root.right
return root2Validate Binary Search Tree (LeetCode 98)
🟠 Medium. Return true if the tree is a valid BST (strictly smaller on the left, strictly larger on the right, for every ancestor).
[5,4,6,null,null,3,7] → false (3 is in 5's right subtree but smaller than 5)Approach. Pass the allowed open interval (low, high) down the tree. The left child inherits (low, node.val), the right child (node.val, high).
Complexity. O(n) time, O(h) stack.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function isValidBST(root: TreeNode | null): boolean {
const ok = (node: TreeNode | null, low: number, high: number): boolean => {
if (!node) return true;
if (!(low < node.val && node.val < high)) return false;
return ok(node.left, low, node.val) && ok(node.right, node.val, high);
};
return ok(root, -Infinity, Infinity);
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def is_valid_bst(root):
def ok(node, low, high):
if not node:
return True
if not (low < node.val < high):
return False
return ok(node.left, low, node.val) and ok(node.right, node.val, high)
return ok(root, float("-inf"), float("inf"))3Kth Smallest Element in a BST (LeetCode 230)
🟠 Medium. Return the k-th smallest value (1-indexed) in a BST.
[5,3,6,2,4,null,null,1], k = 3 → 3Approach. Iterative inorder traversal visits values in sorted order; stop at the k-th visit. Follow-up: if the tree changes often and k-th queries are frequent, store subtree sizes in each node for O(h) queries.
Complexity. O(h + k) time, O(h) space.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function kthSmallest(root: TreeNode | null, k: number): number {
const stack: TreeNode[] = [];
let node = root;
while (true) {
while (node) { stack.push(node); node = node.left; }
node = stack.pop()!;
if (--k === 0) return node.val;
node = node.right;
}
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def kth_smallest(root, k):
stack, node = [], root
while True:
while node:
stack.append(node); node = node.left
node = stack.pop()
k -= 1
if k == 0:
return node.val
node = node.right4Lowest Common Ancestor of a BST (LeetCode 235)
🟠 Medium. Return the lowest common ancestor of nodes p and q in a BST.
[6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 → 6Approach. Use the ordering: if both values are smaller than the node, the LCA is in the left subtree; if both are larger, in the right; otherwise the node splits them and is the LCA.
Complexity. O(h) time, O(1) space.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function lcaBst(root: TreeNode | null, p: number, q: number): number {
let node = root;
while (node) {
if (p < node.val && q < node.val) node = node.left;
else if (p > node.val && q > node.val) node = node.right;
else return node.val;
}
return -1;
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def lca_bst(root, p, q):
node = root
while node:
if p < node.val and q < node.val:
node = node.left
elif p > node.val and q > node.val:
node = node.right
else:
return node.val5Insert into a Binary Search Tree (LeetCode 701)
🟠 Medium. Insert val (not already present) into the BST and return the root.
[4,2,7,1,3], val = 5 → [4,2,7,1,3,5]Approach. Walk down as in search until you fall off the tree; attach the new node there. The recursive version returns the (possibly new) subtree root.
Complexity. O(h) time.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function insertIntoBST(root: TreeNode | null, val: number): TreeNode {
if (!root) return new TreeNode(val);
if (val < root.val) root.left = insertIntoBST(root.left, val);
else root.right = insertIntoBST(root.right, val);
return root;
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def insert_into_bst(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert_into_bst(root.left, val)
else:
root.right = insert_into_bst(root.right, val)
return root6Convert Sorted Array to Binary Search Tree (LeetCode 108)
🟢 Easy. Build a height-balanced BST from a sorted array.
[-10,-3,0,5,9] → [0,-3,9,-10,null,5] (one valid answer)Approach. The middle element becomes the root (so both halves have equal size); recursively build the left half and the right half. Several balanced answers are valid.
Complexity. O(n) time, O(log n) stack.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function _tree(level: (number | null)[]): TreeNode | null {
if (!level.length || level[0] === null) return null;
const root = new TreeNode(level[0]); const q: TreeNode[] = [root]; let i = 1;
while (q.length && i < level.length) {
const node = q.shift()!;
if (i < level.length && level[i] !== null) { node.left = new TreeNode(level[i]!); q.push(node.left); }
i++;
if (i < level.length && level[i] !== null) { node.right = new TreeNode(level[i]!); q.push(node.right); }
i++;
}
return root;
}
function _level(root: TreeNode | null): (number | null)[] {
const out: (number | null)[] = []; const q: (TreeNode | null)[] = [root];
while (q.length) { const n = q.shift()!; if (n) { out.push(n.val); q.push(n.left, n.right); } else out.push(null); }
while (out.length && out[out.length - 1] === null) out.pop();
return out;
}
function sortedArrayToBST(nums: number[]): TreeNode | null {
const build = (lo: number, hi: number): TreeNode | null => {
if (lo > hi) return null;
const mid = (lo + hi) >> 1;
return new TreeNode(nums[mid], build(lo, mid - 1), build(mid + 1, hi));
};
return build(0, nums.length - 1);
}class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def _tree(level):
if not level or level[0] is None:
return None
root = TreeNode(level[0]); q = [root]; i = 1
while q and i < len(level):
node = q.pop(0)
if i < len(level) and level[i] is not None:
node.left = TreeNode(level[i]); q.append(node.left)
i += 1
if i < len(level) and level[i] is not None:
node.right = TreeNode(level[i]); q.append(node.right)
i += 1
return root
def _level(root):
out, q = [], [root]
while q:
n = q.pop(0)
if n:
out.append(n.val); q.append(n.left); q.append(n.right)
else:
out.append(None)
while out and out[-1] is None:
out.pop()
return out
def sorted_array_to_bst(nums):
def build(lo, hi):
if lo > hi:
return None
mid = (lo + hi) // 2
return TreeNode(nums[mid], build(lo, mid - 1), build(mid + 1, hi))
return build(0, len(nums) - 1)🧠 Key takeaways
- BST invariant: left < node < right for every ancestor, so operations walk one path, O(h).
- Validate with an inherited (low, high) range or a strictly increasing inorder.
- Inorder = sorted order: k-th smallest, min difference, recover swapped nodes.
- Balanced trees (AVL/red-black) keep h = O(log n); C# has
SortedSet/SortedDictionary.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.