DSA Trees
Binary Search Trees
Brute force finds any product SKU in a catalog of n = 10⁷. The time limit is not amused. You need membership, predecessor, and range listing without rebuilding a sorted array on every insert.
A BST encodes order as shape. The lets each comparison discard an entire subtree. Speed still depends on : short trees win; insertion chains become linked lists.
Algorithm journey
Duplicate policy
Reject, count, or equals-to-one-side: pick before coding insert.
All left descendants < key < all right descendants.
Follow one path
Search/insert/delete examine O(h) nodes along a root-to-null walk.
Sorted enumeration and successor/predecessor queries.
What actually happens
Naive idea: keep an unsorted list and scan, or rebuild a sorted array after every write. Correct for tiny n; at 10⁷ keys and frequent inserts, either TLE or Θ(n) reshuffles.
Better idea: store keys so each node splits lower keys from higher keys. Search follows one path because the other branch is provably empty of the target. then streams keys sorted.
The improved design still fails on sorted insertion order (height becomes n − 1), on parent-only validation that misses an ancestor bound, and when delete copies a key but drops the rest of a map payload. Those break the proof or the API, not just style.
Invariant
For every node v (strict unique-key policy): every key in v's left subtree is < v.key, and every key in v's right subtree is > v.key. The same claim holds in every subtree. Validation must use , not child-only checks.
How to choose a BST
1. Need ordered dynamics? Predecessor, range, rank, or sorted iteration besides membership: a BST (or ordered map) fits. Exact-key only: prefer a hash table.
2. Need worst-case log n? A plain BST has no height guarantee. Use AVL, red-black, B-tree, or another balancing scheme.
3. Static data? A sorted array plus binary search often wins on locality if updates are rare.
Quantitative check
Balanced height ≈ log₂(10⁷) ≈ 23 comparisons per lookup. A right spine from sorted inserts costs up to 10⁷ comparisons for one miss, and building that spine by n inserts is Θ(n²). Same ADT, different shapes, different deadlines.
Ordering
global bounds
All descendants, not only children, must fit.
Search work
one path
O(h), with worst-case Θ(h).
Sorted view
inorder
left, node, right emits ordered keys.
The ordering invariant is global
Under a strict unique-key policy, every node v satisfies: all keys reachable throughv.left are less than v.key, and all keys reachable through v.right are greater. Each subtree must satisfy the same rule. Checking only the two immediate children is insufficient: a deep descendant can be on the correct side of its parent but the wrong side of an ancestor.
Subtree interval
Each recursive call owns an allowed lower/upper key interval.
Acyclic ownership
Each reachable node has one structural parent; no child link creates a cycle.
Metadata agreement
Stored size, parent, count, or aggregate fields equal values implied by children.
Root reachability
Exactly the logical members are reachable from root; empty means root is null.
Choose a duplicate-key policy before writing insert
“Duplicates go somewhere” is not a complete contract. The choice affects bounds, deletion, rank, rotations, and whatsize means. A set normally rejects equality; a map updates the existing value; a multiset commonly keeps a count in the unique-key node.
| Policy | Insertion rule | Consequence |
|---|---|---|
| Reject / update | One structural node per key; equality is a no-op or updates its value. | Strict bounds: every left key < node.key < every right key. This is the cleanest set/map policy. |
| Store a count | One structural node per distinct key plus a multiplicity field. | Ordering remains strict; insert/delete adjust count before changing structure. Works well with rotations. |
| Equals go left/right | Choose one side consistently and encode an inclusive bound on that side. | Search/delete semantics become more subtle, and ordinary rotations can violate the chosen side rule. |
Interactive: build a BST
This original visualization shows how inserted values descend through comparisons and become leaves. Try sorted input as well as mixed input and compare the resulting heights.
Interactive: narrow bounds, delete safely, validate globally
Search for 65 while tracking inherited bounds, remove a node with two children via its inorder successor, then inspect a tree that passes parent-only checks but violates an ancestor bound.
BST invariant laboratory
A comparison narrows a global key interval
Step 1 of 4
Start at 50
bounds (−∞, +∞)Action
Compare target 65 with the root. Since 65 > 50, only the right subtree can contain it.
Invariant
If 65 exists, it is in the current subtree and satisfies the current bounds.
Observable state
Current node 50; no candidates have been discarded yet.
Keyboard: focus within the demo, then use ← →, Home, or End.
Search and insertion: preserve the candidate-subtree invariant
Search initialization
The root subtree contains every stored key, so it contains the target if the target exists.
Search maintenance
At key x, target < x excludes the entire right subtree; target > x excludes the entire left subtree.
Search termination
Equality returns the node. A null link proves the narrowed candidate subtree is empty.
Insertion endpoint
The first null link on the search path is the unique leaf position that preserves strict bounds.
Python
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def search(root, target):
current = root
while current is not None:
if target == current.key:
return current
current = current.left if target < current.key else current.right
return None
def insert(root, key):
if root is None:
return Node(key)
if key < root.key:
root.left = insert(root.left, key)
elif key > root.key:
root.right = insert(root.right, key)
# equality is rejected under this set policy
return rootJava
final class Bst {
static final class Node {
int key;
Node left, right;
Node(int key) { this.key = key; }
}
static Node search(Node root, int target) {
Node current = root;
while (current != null) {
if (target == current.key) return current;
current = target < current.key ? current.left : current.right;
}
return null;
}
static Node insert(Node root, int key) {
if (root == null) return new Node(key);
if (key < root.key) {
root.left = insert(root.left, key);
} else if (key > root.key) {
root.right = insert(root.right, key);
}
// equality is rejected under this set policy
return root;
}
}Recursive code uses O(h) call-stack space. The iterative search uses Θ(1) auxiliary space; iterative insertion can do the same if it retains the parent of the null link. Neither form prevents a height-n recursion failure on a chain.
Deletion has three structural cases
No children
return nullRemoving a leaf simply clears its parent link. If it was the root, the tree becomes empty.
One child
return the childSplice the sole child into the removed node's position; every key stays inside the same ancestor bounds.
Two children
successor / predecessorUse the minimum of the right subtree or maximum of the left, then delete that source node recursively.
The inorder successor is safe because it is greater than every key in the left subtree and no greater than any other key in the right subtree. As the minimum of the right subtree, it has no left child, so its own removal reduces to an easier case. A predecessor-based implementation is the symmetric alternative.
Python
def delete(root, key):
if root is None:
return None
if key < root.key:
root.left = delete(root.left, key)
elif key > root.key:
root.right = delete(root.right, key)
else:
if root.left is None:
return root.right
if root.right is None:
return root.left
successor = root.right
while successor.left is not None:
successor = successor.left
root.key = successor.key
root.right = delete(root.right, successor.key)
return rootJava
final class BstDelete {
static final class Node {
int key;
Node left, right;
Node(int key) { this.key = key; }
}
static Node delete(Node root, int key) {
if (root == null) return null;
if (key < root.key) {
root.left = delete(root.left, key);
} else if (key > root.key) {
root.right = delete(root.right, key);
} else {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
Node successor = root.right;
while (successor.left != null) {
successor = successor.left;
}
root.key = successor.key;
root.right = delete(root.right, successor.key);
}
return root;
}
}Payload caution: for a key-value node, move/copy the complete record, not the key alone. With parent pointers, subtree sizes, counts, or aggregates, repair every affected field while unwinding. External references to node objects also make “copy key then delete successor” observably different from transplanting the successor node.
Validation requires inherited bounds
Carry the range allowed by all ancestors. A left edge tightens the upper bound to the parent key; a right edge tightens the lower bound. The strict code below matches the reject-duplicates policy. Inclusive endpoints must change if the chosen policy places equals on a side.
Python
def is_valid_bst(root):
def valid(node, low, high):
if node is None:
return True
if low is not None and node.key <= low:
return False
if high is not None and node.key >= high:
return False
return (valid(node.left, low, node.key)
and valid(node.right, node.key, high))
return valid(root, None, None)Java
final class BstValidation {
static final class Node {
int key;
Node left, right;
Node(int key) { this.key = key; }
}
static boolean isValidBst(Node root) {
return valid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
static boolean valid(Node node, long low, long high) {
if (node == null) return true;
if (node.key <= low || node.key >= high) return false;
return valid(node.left, low, node.key)
&& valid(node.right, node.key, high);
}
}For arbitrary comparable keys, use optional bound objects rather than numeric sentinels. An alternative validator checks that inorder output is strictly increasing, but it must remember the previous key and apply the same duplicate policy.
Complexity is a function of height
Let height h be the number of edges on the longest root-to-leaf path (empty tree −1, leaf 0). A path operation examines at most h + 1 nodes, so it is O(h), with worst-case Θ(h) when the path reaches the deepest level. A balanced tree has h = Θ(log n); an insertion chain has h = n − 1.
| Operation | Height form | Balanced | Chain | Why |
|---|---|---|---|---|
| Search / contains | O(h) · worst Θ(h) | O(log n) · worst Θ(log n) | O(n) · worst Θ(n) | At most one root-to-leaf path. |
| Insert | O(h) · worst Θ(h) | O(log n) · worst Θ(log n) | O(n) · worst Θ(n) | Plus metadata updates on that path. |
| Delete | O(h) · worst Θ(h) | O(log n) · worst Θ(log n) | O(n) · worst Θ(n) | Successor/predecessor also lies on one downward path. |
| Min / max | O(h) · worst Θ(h) | O(log n) · worst Θ(log n) | O(n) · worst Θ(n) | Follow left or right links respectively. |
| Full inorder traversal | Θ(n) | Θ(n) | Θ(n) | Every node must be visited. |
| Range [lo, hi], k outputs | O(h + k) | O(log n + k) | O(n) | Prune subtrees outside the range. |
| Rank / select with subtree sizes | O(h) · worst Θ(h) | O(log n) · worst Θ(log n) | O(n) · worst Θ(n) | Requires correct size metadata. |
Degeneracy: a BST does not balance itself
Favorable shape
When each subtree holds a comparable share of the keys, height is Θ(log n). Searching then discards a large region per comparison. Random insertion order of distinct keys yields logarithmic expected height, but only under that randomness assumption.
Insertion chain
Inserting already sorted distinct keys into a plain BST makes every node a right child. The height becomes n − 1, one search becomes Θ(n), and building the tree by n such insertions takes Θ(n²).
Need worst-case bounds?
Use AVL, red-black, B-tree, treap with stated randomness, or another balancing scheme.
Keys already sorted?
Build a balanced tree from the middle recursively in Θ(n), rather than repeated plain insertion.
Static membership only?
A sorted array may be smaller and more cache-friendly while retaining binary search.
Ordered queries are the reason to choose a BST
Floor / ceiling
O(h)Track the best ancestor while descending toward the target.
Successor
O(h)Minimum of the right subtree, or first ancestor reached from its left side.
Range output
O(h + k)Skip left when key < lo and skip right when key > hi; emit inorder.
Rank / select
O(h)Store subtree size so one comparison also reveals how many smaller keys lie left.
Language-neutral pseudocode: order-statistic select
# select(node, r): key with zero-based rank rwhile node is not null:leftSize = size(node.left)if r < leftSize:node = node.leftelse if r == leftSize:return nodeelse:r = r - leftSize - 1node = node.rightreturn NOT_FOUND# Mutation obligation:# node.size = 1 + size(node.left) + size(node.right)
Common failure modes
Checking only children
Validate every node against bounds inherited from all ancestors.
Mismatched duplicate behavior
Make insert, search, delete, validation, traversal, and rotations share one explicit policy.
Losing the returned root
Insert/delete can replace a subtree root; assign the function's return value to the parent link and top-level root.
Deleting key but not payload
Move the complete entry or transplant the successor node; preserve map semantics and external-handle expectations.
Claiming O(log n) unconditionally
State O(h), with worst-case Θ(h). A plain BST has no worst-case logarithmic-height guarantee.
Stale augmentation
Recompute size, height, sums, or parent links after every structural change on the affected path.
Simple rule of thumb
Choose a BST when you need a dynamic ordered set or map: predecessor/successor, sorted iteration, rank, or range queries in addition to membership. State the duplicate policy and height guarantee. If only exact-key lookup matters, a hash table is usually simpler; if the data is static, a sorted array often has better locality; if worst-case logarithmic updates matter, choose a self-balancing BST rather than hoping a plain BST stays short.
Exercises
- Build a tree that passes every parent–child comparison but violates the global . Which catches it?
- Insert 1..10⁵ in order into a plain BST, then search for 10⁵. Estimate comparisons. How would you rebuild a height-balanced tree from the same keys in Θ(n)?
- Delete a two-child node using its . Why is the successor guaranteed to have no left child, and what breaks if you copy only the key of a key–value node?
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): binary search trees (search, insert, delete, successor).
- Sedgewick & Wayne, Algorithms: BST symbol tables and the cost of ordered inserts.
- Guibas & Sedgewick: red-black trees (how balancing restores logarithmic height when a plain BST fails).
Takeaway
A BST is order encoded as shape under a global search-tree invariant. Path operations cost O(h); balance is optional machinery you must add when worst-case log n matters.