Topic 02

Binary Search Trees

Data Structures

A binary search tree keeps every key smaller than a node in that node's left subtree and every larger key in its right subtree. Each comparison on the way down therefore discards everything on one side, and a lookup among a million keys takes about 20 comparisons, when the tree is balanced. Everything in this topic turns on that condition.

The ordering rule buys what a hash table cannot give: keys in order, ranges, and the nearest key below or above a value. It also has a failure mode that every engineer eventually feeds it. Insert the keys in sorted order and the tree becomes a linked list, with every cost of one.

The Ordering Rule

For every node, all keys in its left subtree are smaller and all keys in its right subtree are larger. The rule holds for whole subtrees, not only between a parent and its two children, and that is what lets one comparison discard an entire subtree. When a lookup for 60 finds 50 at the root and goes right, it has ruled out 30, 20 and 40 without reading them, because the rule promises that nothing on the left can be larger than 50.

One lookup in a seven-node tree
2040306080705060 > 50: go right60 < 70: go leftfound in 3 comparisons: the left subtree of 50 is never read

Lookup and Insert

A lookup starts at the root, compares, goes left or right, and stops either at the key or at an empty child, which means the key is absent. An insert is a lookup that fails: the new key goes exactly where the search fell off the tree, as a new leaf. Nothing else moves.

Both operations make one comparison per level they pass through, so the price of every operation is the height of the tree. A tree of 20 levels answers in at most 20 comparisons whether it holds a thousand keys or a million. A tree of a million levels answers in up to a million.

Delete

Deleting a leaf removes it. Deleting a node with one child puts that child in its place, and the ordering rule still holds, because the child's whole subtree was already on the correct side of the parent above.

The third case is where hand-written trees go wrong. A node with two children cannot be cut out, because its parent has room for only one of them. Instead it is replaced by its in-order successor, the smallest key in its right subtree: go right once, then left until there is no left child. That key is larger than everything on the left and smaller than everything else on the right, so it can take the deleted node's place without breaking the rule. It is copied up, and then removed from where it was, which is always the easy case, because the smallest key in a subtree has no left child.

Deleting a node with two children
20373545403070501. Delete 30: two children20373545403570502. Copy successor 35 up203745403570503. Remove old 35; 37 moves upthe successor is the smallest key in the right subtree: right once, then left to the end

What Order Buys

An in-order walk, from the previous topic, yields every key in sorted order. The minimum is the leftmost node and the maximum the rightmost. A range such as every year after 1970 up to and including 1980 is one descent to the first year above 1970 and then a walk that stops after 1980, so it costs the height plus the number of keys in the range. "The largest key not above x", the question behind rounding a timestamp down to the last recorded event, is one descent.

None of these questions can be answered by a hash table, the structure of Chapter 5, because a hash deliberately scatters neighbouring keys across the table. Every one of them costs a hash table a full scan. That is why ordered maps exist beside dictionaries in every standard library that has them, and why a database offers an ordered index at all.

Sorted Input Makes a List

Insert 1, 2, 3 and onward in order. Each new key is larger than everything already in the tree, so it becomes the right child of the previous key, and the tree grows as a diagonal line with no left children at all. A million sorted inserts build a chain a million levels deep, and every lookup scans it like a linked list.

The same seven keys in two insertion orders
Inserted as 4 6 2 5 1 7 33 levelsInserted as 1 2 3 4 5 6 7132576476543217 levels: a linked list

Random order is kind to the plain tree. The textbook figure for a randomly built tree is an average depth of about 1.39 times the base-2 logarithm of n, near 28 for a million keys. Measured on Python 3.15 by inserting a million shuffled keys, the average lookup took 25.7 comparisons and the deepest key sat 51 levels down: two and a half times the ideal, but nowhere near a million.

Real data arrives sorted far more often than random. Timestamps, auto-increment ids and a sorted export all arrive in order, and partly sorted data grows long chains. The chain also breaks code, not only speed: a recursive insert on Python 3.15 fed sorted keys raised RecursionError on the 999th key, because each insert recursed once per level of a chain that was already 998 levels long.

Memory, Misses and Input Order

Even balanced, a pointer-based tree pays for its shape in memory. Every key is a separately allocated node, the heap allocation of Chapter 4, and every level of a descent follows a pointer to wherever the allocator put the next node. That makes each of the 20 levels over a million keys a potential cache miss at about 100 nanoseconds, about 2 microseconds of waiting per lookup, where a hash table pays one or two misses.

Unbalanced, the tree is a correctness risk disguised as a performance one. The input order that breaks it is chosen by whoever feeds it: a sort order in an export, a sequence of ids, or someone who has read the same textbook and sends keys in order on purpose. Production code therefore never uses the plain binary search tree. It uses the balanced trees of the next topic in memory, or the B-trees at the end of this chapter on disk.

Misconceptions
  • "A binary search tree gives O(log n) lookups." It gives O(height). The height is about log n only when the tree is balanced, and it is n when the keys arrive in order.
  • "Real data is random enough to keep the tree balanced." Timestamps, sequence ids and sorted exports arrive in order, and partly sorted data grows long chains. For this structure the worst case is the common case.
  • "A hash table does everything a binary search tree does, only faster." A hash table cannot return keys in order, answer a range or find the nearest key. Each of those costs it a full scan.
  • "Deleting from a binary search tree is removing the node." A node with two children must be replaced by its successor. Skipping that case breaks the ordering rule for a whole subtree, and later lookups miss keys that are still there.
  • "A balanced tree is as fast as binary search over a sorted array." Both are O(log n), but the tree pays a potential cache miss per level, while the array keeps its last probes inside a few cache lines (Chapter 7). For data that is read far more than it changes, the array wins.
Why It Matters
  • Never feed a plain binary search tree input whose order you do not control. Use a balanced tree, whose height does not depend on the order of arrival.
  • Use an ordered structure when the questions include ranges, neighbours or sorted output, and a hash table when they do not. Order is the one thing a hash gives up on purpose.
  • Prefer a sorted array with binary search for data that is read far more often than it is changed. It answers the same ordered questions with no pointers and far fewer cache misses.
  • Test tree code with sorted and reverse-sorted input, not only random input. A random test shows the tree at its best; production shows it at its worst.
RelatedBinary search the same halving over a sorted array, without pointers (Chapter 7)Balanced trees remove the dependence on input orderQuicksort's worst case the same failure on sorted input, from the same cause (Chapter 7)

Knowledge Check

A plain binary search tree receives a million keys in ascending order. How many levels does it have afterwards?

  • About 20, because each comparison halves the remaining keys
  • About 28, the average depth of a randomly built tree
  • A million, because each key becomes the right child of the one before
  • About 40, since the tree rebalances itself once it is twice too tall

A balanced tree holds a million events keyed by date. What does a query for every event between 1970 and 1980 cost?

  • One descent of about 20 steps, plus one step per event in the range
  • A walk over all million keys, checking each one against the range
  • About 20 steps in total, however many events fall inside the range
  • One hash probe per year from 1970 to 1980, eleven probes in all

Why does deleting a node with two children use its in-order successor?

  • The successor is always a leaf, so removing it never needs any work
  • It is larger than the left side and smaller than the rest of the right
  • It is the node closest to the root, so moving it costs the fewest steps
  • Any key from the right subtree works; the successor is only a convention

A service needs to answer "exactly this user id" and nothing else, a million times a minute. Which structure fits best?

  • A balanced tree, because its lookups are guaranteed to be O(log n)
  • A hash table, because no query needs order, ranges or neighbours
  • A plain binary search tree, because user ids arrive in random order
  • A sorted linked list, because it keeps ids in order for fast lookups

A read-mostly catalogue of a million ids is queried by exact id and by range, and changes once a night. What should hold it?

  • A balanced tree, because only a tree can answer both kinds of query
  • A hash table, rebuilt every night, because exact lookups dominate
  • A plain binary search tree, loaded in id order every night
  • A sorted array searched by halving, and rebuilt once a night

You got correct