Binary Search Trees
A binary search tree keeps keys ordered so that every operation follows a single root-to-leaf path. We state the BST property, trace search, insert, successor, and all three delete cases on concrete trees, prove the inorder walk sorts, and note the drawback — every operation costs , and a carelessly built tree degrades to height , motivating balance.
╌╌╌╌
A hash table gives expected lookups but throws away order: it cannot tell you the smallest key, the next key after a given one, or every key in a range. A binary search tree (BST) keeps those queries fast by storing keys in a shape that records their order. Each node holds a key and pointers to a left child, a right child, and a parent; the keys are arranged so that the tree itself is a kind of decision diagram for searching. The result is a dynamic ordered dictionary supporting search, insert, delete, minimum, maximum, predecessor, successor, and in-order traversal, every one of them in time proportional to the tree's height.
The binary search tree property
The arrangement is governed by one local invariant, checked at every node :
Smaller keys live to the left, larger keys to the right, everywhere, not just between a node and its immediate children.1 This recursive constraint is what lets a search discard half the tree at each step.
Reading this tree: the root is ; everything in its left subtree () is and everything in its right subtree () is , and the same holds recursively at and .
Searching
To search for a key , start at the root and walk down. At each node, if equals the node's key we are done; if is smaller we go left, otherwise we go right. Each comparison drops us one level, so the search traces a single root-to-leaf path.
- 1if or then
- 2return
- 3if then
- 4return callk is in the left subtree
- 5else
- 6return callk is in the right subtree
The procedure is correct by the BST property: when , the property guarantees cannot be in 's right subtree, so discarding it loses nothing. The search visits one node per level and runs in time, where is the height of the tree.2
An unsuccessful search behaves the same way. Searching for in this tree
compares (go left), (go right), (go left), and finds
's left child is nil: the key is absent, and the search reports it after
comparisons, never more. The nil that ends a failed search is not
wasted information: it marks where the key would go, which is the
observation insertion is built on.
and are the degenerate
cases of search: follow left pointers until they run out to reach the smallest
key, or right pointers for the largest.
- 1Tree-Minimum(x):
- 2while do
- 3min is the leftmost node
- 4return
- 5Tree-Maximum(x):
- 6while do
- 7max is the rightmost node
- 8return
Inserting
Insertion reuses the search path. To insert key , walk down as if searching
for it; when the walk falls off the bottom of the tree (reaches a nil child),
that empty spot marks where belongs, preserving the BST property. We
attach a new leaf there, remembering the parent so we can hook it in.
- 1y trails x
- 2
- 3while do
- 4
- 5if then
- 6
- 7else
- 8
- 9
- 10if then
- 11tree was empty
- 12else if then
- 13
- 14else
- 15
Like search, insertion walks one root-to-leaf path and costs . New keys always enter as leaves, which keeps insertion simple but also lets the tree's shape degrade, as shown below.
Tracing with on our running tree:
starts at the root with . Since , the trailing
pointer moves to and descends left to . Since , moves
to and descends right to . Since , moves to and
descends left, to nil. The loop exits with ; because , the new
node becomes 's left child. Three comparisons, one pointer assignment, done.
Finding a successor
The successor of a node is the node with the smallest key greater than , the next key in sorted order. There are two cases, and neither needs a comparison of keys, only structure:
- If has a right subtree, the successor is the minimum of that subtree: the smallest key still larger than .
- If has no right subtree, the successor is the lowest ancestor whose left child is also an ancestor of ; we climb up until we move up a left link.
- 1if then
- 2return callmin of right subtree
- 3
- 4while and do
- 5climb while x is a right child
- 6
- 7return
Both cases follow a single vertical path, down into the right subtree or up through ancestors, so also runs in . Predecessor is the mirror image (left subtree's maximum, or climb until a right link).
Trace both cases on the running tree. For (case 1): is
non-nil, so we return of the subtree at : descend
left from to , and has no left child, so the successor is .
Correct: is the smallest key exceeding . For (case 2): has
no right subtree, so we climb. First iteration: and , so
, . Second test: , not a right child, so the
loop stops and returns , the first ancestor reached by moving up-and-right,
which is precisely the smallest key greater than everything in 's subtree.
The climb can also run off the top: for the loop ascends
(each a right child of its parent) and exits with ; is the
maximum and has no successor. A predecessor trace mirrors this: for ,
which has no left subtree, we climb while is a left child (,
so ), then stop because , returning .
Deleting a node
Deletion is the one operation that needs care, because removing an internal node leaves a hole that must be filled without disturbing the BST property. There are three cases, in increasing difficulty:
- has no children: just detach it from its parent.
- has one child: splice that child into 's position.
- has two children: 's successor is the minimum of its right subtree, so has no left child. Move into 's position; if was not 's direct child, first replace by its own right child.
All three reduce to a single primitive, , which replaces the subtree rooted at with the subtree rooted at :
- 1if then
- 2
- 3else if then
- 4
- 5else
- 6
- 7if then
- 8
- 1if then
- 2calllift the right child
- 3else if then
- 4calllift the left child
- 5else
- 6callsuccessor, no left child
- 7if then
- 8calldetach y, lift its right child
- 9
- 10
- 11cally into z's slot
- 12
- 13
The first two cases are pure pointer splices, and both are handled by the same
two branches of : when is nil we transplant
into 's place (this covers the leaf case too, transplanting
nil), and symmetrically when is nil. Concretely, in the tree
below, deleting the leaf calls :
since , the assignment detaches it and
nothing else moves. Deleting , which has only the child , calls
: since , we set
and , and rises one level with its
subtree intact; every key in it is still , so the BST property holds.
The two-child case is the subtle one. Replacing by its successor keeps every key in 's left subtree below the new root and every key in the right subtree above it, so the ordering survives:
Follow the pointer surgery step by step. We delete , the root. Both children exist, so the third branch runs: (from , one step left, then no further). Here , so the inner fix-up fires first: lifts 's right child into 's old slot (), then and hand 's entire right subtree to . Now makes the root, and , attach the untouched left subtree. The result is the tree on the right: sits where was, sits where was, and every ordering relation still holds because was the smallest key in the right subtree: everything remaining there is larger, and everything on the left was already smaller. When the successor is 's direct child (), the inner fix-up is skipped: 's right subtree is already in the correct position relative to , and the final transplant alone suffices. Why the successor and not some other key? Only 's successor or predecessor can replace without reordering: the replacement must be larger than all of 's left subtree and smaller than all of its right subtree except itself, and the successor (minimum of the right subtree) is one of exactly two keys with that property.
Each branch does a constant amount of pointer surgery plus at most one call, so runs in like the rest.
The order is already there: inorder walk
Because the BST property sorts keys left-to-right at every node, visiting the tree in order — left subtree, then the node, then right subtree — emits the keys in increasing order.3
- 1if then
- 2callsmaller keys first
- 3print
- 4callthen larger keys
The walk visits each of nodes once, so it runs in time. This gives a clean way to read out a sorted sequence, and shows that a BST is, in effect, a dynamic sorted list you can also splice into and search.
The catch: height is everything
Every operation above costs . So the BST is fast exactly when is small. The best case is a balanced tree, where the two subtrees of each node have nearly equal size; then and every operation is .
The worst case is a disaster. Suppose we insert keys in sorted order: . Each new key is larger than everything present, so it walks all the way right and attaches as the rightmost leaf. The tree degenerates into a single descending path, a glorified linked list:
Now , and search, insert, and successor all degrade to ,
no better than scanning an unsorted array.4 The very flexibility that made
insertion easy (new keys land as leaves wherever the path takes them) lets an
unlucky or adversarial insertion order ruin the shape. And sorted input is not a
contrived adversary: it is one of the most common inputs in practice: keys read
from a sorted file, timestamps arriving in order, sequential IDs from a database.
Reverse-sorted input produces the mirror-image left path, and nearly-sorted
input produces a tree that is nearly a path. Building a BST naively from data
that happens to be
ordered is a classic performance bug: the code is correct,
the tests pass on small shuffled inputs, and production slows to a crawl.
How bad is a typical tree, as opposed to a worst-case one? There is a
positive result here, with an important caveat about what typical
means. Call
a BST randomly built if it results from inserting distinct keys in
uniformly random order into an empty tree.
So if insertion order were genuinely random, plain BSTs would be fine on average: the expected height is within a constant factor of the optimal (the constant in the known bounds is roughly ). The caveats: first, the theorem randomizes over insertion orders, not over tree shapes; it is a statement about a random process, and real inputs (sorted, nearly sorted, adversarial) need not look anything like a random permutation. Second, the guarantee is only in expectation and says nothing once deletions mix into the workload; the classical analysis covers insertion-only sequences. Randomized structures such as treaps enforce the random-order behavior regardless of the actual arrival order, which is one principled fix. The other is to enforce balance structurally.
This is the central tension of binary search trees:
We cannot control the order in which keys arrive. So the fix is to make the tree rebalance itself as keys come and go, forcing no matter what. Randomized BSTs (and treaps) achieve height in expectation; balanced search trees — red-black trees, AVL trees, B-trees — guarantee height in the worst case by maintaining extra structural invariants and repairing them after each update. That repair machinery is the subject of the next lesson.
Augmenting the tree
A BST is not only a sorted set; once you hang extra information on each node, the same walk answers much richer queries. The general method, from CLRS's chapter on augmenting data structures, is to store a small summary in each node that can be recomputed from a node and its two children in , so rotations still repair it cheaply.
Order-statistic trees. Store in each node the size of its subtree. Then two new queries run in : select, find the -th smallest key, and rank, count how many keys are . Select descends by comparing against the left subtree's size; rank accumulates left-subtree sizes along the search path. This augmentation is what the LeetCode problem Kth Smallest Element in a BST wants, and what a balanced order-statistic tree gives in , the LeetCode Count of Smaller Numbers After Self is the same augmentation applied online. As a concrete trace: in a tree holding with at the root (left subtree size ), sees , subtracts the keys at-or-left of the root, and recurses for the st smallest in the right subtree , landing on .
Interval and other summaries. Store the maximum endpoint in each subtree and
the tree answers "does any stored interval overlap ?" in , the
interval tree, taken up in
Spatial Data Structures.
Store subtree sums and you get the ordered analogue of a
Fenwick tree. The lesson
is that a self-balancing BST is a substrate: rank/select, dynamic order
statistics, and stabbing queries are all one augmentation away, which is why
balanced BSTs, not hash tables, back the ordered-map type (std::map,
Java TreeMap) in standard libraries.6
Takeaways
- A binary search tree stores keys under the BST property (left subtree node right subtree, recursively), so searching follows one root-to-leaf path.
- Search, insert, minimum, maximum, successor, predecessor all walk a single vertical path and cost ; new keys enter as leaves.
- Delete has three cases — leaf, one child, two children — all built on the splice; the two-child case moves the successor into the deleted node's place, which preserves ordering because the successor is the minimum of the right subtree.
- An inorder walk emits the keys in sorted order in time; the order is baked into the shape.
- Performance hinges entirely on height: when balanced, but for a degenerate (e.g. sorted-insertion) tree. A randomly built BST has expected height , but that assumes random insertion order and no deletions; real inputs offer no such promise.
- Because we cannot control insertion order, we need trees that rebalance themselves to guarantee , the motivation for balanced search trees.
Footnotes
- CLRS, Ch. 12 — Binary Search Trees (§12.1): the BST property as a recursive ordering invariant. ↩
- Skiena, §3.4 — Binary Search Trees: search along a single root-to-leaf path in time. ↩
- Erickson, Ch. — Binary Search Trees: an inorder traversal emits the keys in sorted order. ↩
- CLRS, Ch. 12 — Binary Search Trees (§12.4): operations cost , degrading to for an unbalanced tree. ↩
- CLRS, Ch. 12 — Binary Search Trees (§12.4, Theorem 12.4): a randomly built BST on distinct keys has expected height ; the analysis assumes insertions only, in uniformly random order. ↩
- CLRS, Ch. 14 — Augmenting Data Structures: order-statistic trees (subtree sizes for select/rank) and interval trees (subtree max endpoint), and the general rule for augmenting a red-black tree with recomputable summaries. ↩
╌╌ END ╌╌