Topic 03

Balanced Trees

Data Structures

A binary search tree costs its height, and its height depends on the order the keys arrived in. A balanced tree removes that dependence. After every insert and delete it checks a local rule, and when the rule is broken it repairs the shape with a rotation, which moves nodes around without changing the order of the keys.

The result is a guaranteed O(log n) for lookup, insert and delete, whatever the input. It is the structure under Java's TreeMap, C++'s std::map and the Linux scheduler's queue of runnable tasks, and it is the answer to the sorted-input disaster of the previous topic.

A Guarantee on Height

Every operation on a search tree walks one path from the root down, so a guarantee on height is a guarantee on everything. "Balanced" never means perfect. Keeping a tree perfectly balanced would mean rebuilding large parts of it on some inserts, at a cost of O(n). A balanced tree only promises that its height stays within a constant factor of the base-2 logarithm of n.

That promise is enough. It turns the million-level chain of the previous topic into a tree somewhere between 20 and 40 levels deep, depending on the design. Measured on Python 3.15 with a textbook red-black tree, a million keys inserted in sorted order, the worst input for a plain tree, produced 37 levels. The same million keys in random order produced 24.

Rotations

A rotation lifts a child into its parent's place. In a right rotation, the left child x rises above its parent y, y becomes x's right child, and the subtree that sat between them, everything larger than x and smaller than y, moves across to become y's left child. Read the tree in order before and after and the sequence is the same: subtree A, then x, then subtree B, then y, then subtree C.

A rotation changes the shape, not the order
yxCABxyABCright rotationleft rotationin order: A x B y Cin order: A x B y CB changes parent; the order of keys does not

So a rotation makes one side shorter and the other taller while the ordering rule keeps holding. It rewrites three pointers and costs O(1), whatever the size of the subtrees it moves. Every balanced binary tree is a rule about when to rotate.

AVL Trees: Strict Balance

The first balanced tree was published in 1962 by Georgy Adelson-Velsky and Evgenii Landis, and it carries their initials. An AVL tree keeps the heights of every node's two subtrees within one of each other. Each node stores its height or the difference between its children's heights, and an insert that breaks the rule is repaired on the way back up.

The strict rule holds the height below about 1.44 times the base-2 logarithm of n, so a million keys need at most 29 levels. Lookups are as short as a binary tree gets. An insert needs at most one single or double rotation to restore the rule. A delete is less tidy: it may need a rotation at every level on the way back to the root.

Red-Black Trees: Looser Balance, Cheaper Changes

A red-black tree colours every node red or black and keeps three promises. The root is black. No red node has a red child. Every path from a node down to the empty links below it passes the same number of black nodes. The design came from Leonidas Guibas and Robert Sedgewick in 1978, building on Rudolf Bayer's symmetric binary B-trees of 1972.

A red-black tree built from sorted input
4213 black33 black653 black873 black9103 blackkeys 1 to 10 inserted in sorted order: 5 levels, not 10thick ring: black nodered: red node

Together the rules keep the longest path within twice the shortest, so the height stays below twice the base-2 logarithm of n plus one: about 40 levels for a million keys. In exchange for that looser shape, an insert needs at most two rotations and a delete at most three, plus some recolouring on the way up. Bounded, small repair work on every update is why most standard libraries choose red-black trees.

Where They Run

Java's TreeMap and TreeSet are red-black trees, and so are the tree bins inside Java's HashMap: once one bucket grows past eight entries in a table of at least 64 buckets, the bucket turns its list into a small red-black tree, so a flood of colliding keys (Chapter 5) costs log n per lookup instead of n. C++'s std::map and std::set are red-black trees in all three major standard libraries: GCC's, LLVM's and Microsoft's. The Linux kernel keeps runnable tasks in a red-black tree ordered by a key computed from each task's virtual time, which Chapter 10 returns to, and uses the same tree for many other indexes.

Python ships no balanced tree in its standard library. A dict keeps insertion order, not key order. The popular third-party sortedcontainers package is not a tree at all: it keeps a list of sorted lists, because contiguous lists of about a thousand items each are kinder to the cache than a node per key, for the reasons Chapter 4 gives.

The Cost in a System You Run

The guarantee is paid in memory and in misses. Each node carries its key, two child pointers and a colour or a height, all in its own allocation, so a million-key ordered index in a language like Java or C++ spends tens of megabytes on pointers and bookkeeping before counting the keys. Each of the 20 to 40 levels of a lookup is a pointer hop to wherever that node was allocated, and each hop can be a cache miss of about 100 nanoseconds.

A balanced binary tree is the right answer when the input order is hostile or unknown and updates are frequent: an in-memory index taking inserts and deletes all day, a scheduler, a timer list that needs ordered traversal. It is the wrong answer when data is read far more often than written, where a sorted array searched by halving (Chapter 7) is denser and faster, and when data lives on disk, where the B-tree at the end of this chapter replaces 20 page reads with three.

AVL vs Red-Black

An AVL tree keeps its height below about 1.44 times the base-2 logarithm of n, so lookups are shorter, and pays with more rebalancing work on deletes. Choose it for read-heavy in-memory indexes.

A red-black tree allows up to twice the logarithm and bounds the rotations of every update to a small constant. Choose it for mixed workloads, which is why standard libraries ship it. Both are O(log n); the difference is constants, and a B-tree beats both as soon as a node can be a whole page.

Misconceptions
  • "Balanced means perfectly balanced." AVL stays within about 1.44 times the optimal height and red-black within twice. Perfect balance would cost O(n) on some updates, which is the cost balancing exists to avoid.
  • "Rebalancing makes every insert expensive." An insert pays the descent it needed anyway, some recolouring and, in a red-black tree, at most two rotations. Each rotation rewrites three pointers.
  • "AVL and red-black trees have different Big-O." Both are O(log n) for lookup, insert and delete. They differ in constants: how tall the tree may grow and how often it rotates.
  • "A balanced tree is the fastest ordered structure." On real hardware a sorted array wins for read-mostly data and a B-tree wins at scale, because both touch fewer, denser pieces of memory than a tree with a node per key.
  • "Python has a TreeMap somewhere in the standard library." It has none. A dict keeps insertion order, and ordered lookups use a sorted list with the standard binary-search module or a third-party package.
Why It Matters
  • Use a balanced tree when order matters and the input order is outside your control. Its height, and so every operation, no longer depends on who sent the keys.
  • Take the language's ordered map instead of writing a balanced tree. Rotation bugs are subtle, and the library's tree has been tested for decades.
  • Choose a sorted array for read-mostly ordered data and a B-tree for data on disk. Fewer, denser nodes beat balanced pointers on every rung of the memory hierarchy.
  • Budget two pointers and a flag per key when sizing an in-memory ordered index. The guarantee is paid for in memory, before a single key is counted.
RelatedSkip lists the same expected O(log n) from randomized linked levels instead of rotations (Chapter 5)B-trees stay balanced by splitting wide nodes instead of rotating narrow onesHeaps keep a weaker order and pay less for it

Knowledge Check

A red-black tree holds a million keys. What is the most its height can be?

  • About 20 levels, because red-black trees are perfectly balanced
  • About 40 levels, twice the base-2 logarithm of n plus one
  • About 1,000 levels, the square root of the number of keys
  • A million levels if the keys happen to arrive in sorted order

Why does a rotation never break the ordering of keys in a search tree?

  • It swaps the keys of two nodes so that each lands on the correct side
  • It only runs on leaves, which have no subtrees whose order could change
  • The moved subtree lies between the two nodes, before and after the move
  • It rebuilds the affected subtree from a sorted copy of its keys

An in-memory index takes a constant stream of inserts and deletes and is read about as often as it is written. Which tree fits best?

  • A red-black tree, whose updates need only a few rotations each
  • An AVL tree, because its stricter shape makes every update cheaper
  • A plain binary search tree, because rebalancing wastes time on updates
  • A sorted array, because it is denser in memory than any pointer-based tree

Why is the popular third-party sortedcontainers package for Python not built from a balanced tree?

  • Python cannot represent the parent and child pointers a tree needs
  • Balanced trees cannot keep duplicate keys, and sorted lists need to
  • Its author wanted to avoid the patents that cover red-black trees
  • Lists of sorted lists keep items contiguous, which the cache rewards

When does a balanced binary tree lose to a sorted array searched by halving?

  • When the input keys arrive in a random order rather than sorted
  • When the data is read far more often than it is changed
  • When the keys are strings rather than integers of a fixed width
  • When the data set is too large to fit in the memory of one machine

You got correct