Part III · Data Structures Chapter 13 Extra depth

Red-Black Trees

One extra bit per node and five invariants, which together force the height below 2 lg(n+1) on every input — so O(lg n) stops being a hope and becomes a guarantee.

Chapter 12 left a binary search tree that is excellent when balanced and useless when not, with no mechanism to tell the difference. A red-black tree is a BST that stores one bit of colour per node and maintains five properties which no sequence of insertions or deletions is allowed to break. Those properties bound the height at 2 lg(n+1), so every O(h) operation from Chapter 12 becomes O(lg n) in the worst case. Search, minimum, maximum, successor, predecessor, and the in-order walk are inherited unchanged. Only insertion and deletion need new machinery, and that machinery is this chapter.

4th edition note. The structure is unchanged, but the presentation is cleaner: the T.nil sentinel is used throughout, and the insert and delete fixup cases are laid out more explicitly. This is also the chapter where reading the sentinel discussion from Chapter 10 pays off — the fixup code would be far uglier with NIL tests.

Contents

  1. The five properties
  2. The sentinel
  3. Why the height is O(lg n)
  4. Rotations
  5. Insertion
  6. The three insert cases
  7. Deletion
  8. The four delete cases
  9. Costs, and what it is good for
  10. Recap

The five properties

Each node carries an extra attribute color, either RED or BLACK, alongside key, left, right, and p. A binary search tree is a red-black tree if it satisfies all five:

  1. Every node is either red or black.
  2. The root is black.
  3. Every leaf (NIL) is black.
  4. If a node is red, then both its children are black. (No two reds in a row.)
  5. For each node, all simple paths from that node to descendant leaves contain the same number of black nodes.

Properties 4 and 5 are the working ones. Property 4 says red nodes cannot stack up; property 5 says black nodes are distributed perfectly evenly. Together they mean the longest root-to-leaf path can be at most twice the shortest: the shortest is all black, and the longest alternates red and black.

The black-height of a node x, written bh(x), is the number of black nodes on any simple path from x down to a leaf, not counting x itself. Property 5 is exactly the statement that this is well defined — every downward path from x gives the same count.
26 17 41 14 21 30 47 every leaf is the black sentinel T.nil bh=2bh=1bh=1 bh=1bh=1bh=1bh=1 black red check property 5: every root-to-leaf path passes exactly 3 black nodes including T.nil
Figure 13.1 — A red-black tree. Both children of every red node are black (property 4), and every downward path has the same black count (property 5).

The sentinel

Property 3 requires every NIL to be a black leaf, and the algorithms constantly need to read x.p or x.color for nodes at the boundary. Rather than scatter NIL tests everywhere, CLRS uses the Chapter 10 trick: a single sentinel object T.nil.

This is not cosmetic. The delete fixup routine reads x.p when x may be T.nil, and that only works because the sentinel is a real object whose parent pointer the code has arranged. Trying to implement red-black deletion with genuine null pointers is where most from-scratch attempts fall apart.

Why the height is O(lg n)

Lemma 13.1. A red-black tree with n internal nodes has height at most 2 lg(n + 1).

The proof runs in two steps and is worth carrying because it shows exactly which property does which job.

Step 1: a subtree rooted at x contains at least 2bh(x) - 1 internal nodes. By induction on height. If x is a leaf, bh(x) = 0 and the subtree has 2⁰ - 1 = 0 internal nodes. Otherwise each child of x has black-height bh(x) or bh(x) - 1, depending on its own colour, so by the inductive hypothesis each child’s subtree has at least 2bh(x)-1 - 1 internal nodes. Adding the two children plus x:

(2bh(x)-1 - 1) + (2bh(x)-1 - 1) + 1 = 2bh(x) - 1

Step 2: at least half the nodes on any root-to-leaf path are black. This is property 4 doing its work. Red nodes cannot be adjacent, so on a path of length h at most h/2 nodes are red, and therefore bh(root) ≥ h/2.

Combine them:

n ≥ 2bh(root) - 1 ≥ 2h/2 - 1 n + 1 ≥ 2h/2 lg(n + 1) ≥ h/2 h ≤ 2 lg(n + 1)
What each property contributes. Property 5 (equal black-heights) forces the tree to be bushy — it cannot have a long thin branch, because that branch would need as many black nodes as every other path. Property 4 (no two reds) caps how much the red nodes can stretch a path beyond its black skeleton, at a factor of 2. Neither alone is enough; together they pin the height.

The immediate payoff: every Chapter 12 query — SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR — runs unmodified on a red-black tree and now costs O(lg n) in the worst case. They never inspect the colours at all.

Rotations

Insertion and deletion break the properties, and fixing them requires changing the tree’s shape. The primitive for that is the rotation: a local restructuring that changes pointers among three nodes while preserving the binary-search-tree property.

x y α β γ LEFT-ROTATE(T, x) RIGHT-ROTATE(T, y) y x α β γ in-order on both sides: α x β y γ — unchanged, which is why the BST property survives
Figure 13.2 — A rotation. The subtree β changes parent, everything else just re-links. Left and right rotations are exact inverses.
LEFT-ROTATE(T, x) 1 y = x.right // set y 2 x.right = y.left // turn y's left subtree into x's right subtree 3 if y.left ≠ T.nil 4 y.left.p = x 5 y.p = x.p // link x's parent to y 6 if x.p == T.nil 7 T.root = y 8 elseif x == x.p.left 9 x.p.left = y 10 else x.p.right = y 11 y.left = x // put x on y's left 12 x.p = y

O(1) — a fixed number of pointer writes, independent of subtree size. RIGHT-ROTATE is the mirror image, with left and right exchanged throughout.

Rotations change only pointers, never keys, and preserve the in-order sequence exactly. That is what makes them safe: any number of rotations leaves you with a valid BST. They change the shape, and therefore the height, which is the entire point.

Insertion

Insert as an ordinary BST would, colour the new node red, then repair.

RB-INSERT(T, z) 1 x = T.root 2 y = T.nil 3 while x ≠ T.nil // descend to a leaf position 4 y = x 5 if z.key < x.key 6 x = x.left 7 else x = x.right 8 z.p = y 9 if y == T.nil 10 T.root = z 11 elseif z.key < y.key 12 y.left = z 13 else y.right = z 14 z.left = T.nil 15 z.right = T.nil 16 z.color = RED // always red — see below 17 RB-INSERT-FIXUP(T, z)

Lines 1–13 are TREE-INSERT from Chapter 12 with NIL replaced by T.nil. Lines 14–17 are new.

Why the new node is red. Colouring it black would add one to the black-height of every path through it, violating property 5 immediately — a hard problem affecting the whole tree. Colouring it red can only violate property 4, and only if its parent happens to be red — a local problem affecting two adjacent nodes. Choose the violation you can repair locally.

So after insertion, exactly one property can be broken:

The three insert cases

The fixup loop pushes the violation up the tree until it can be resolved. The controlling question at each step is the colour of z’s uncle — the sibling of z’s parent.

RB-INSERT-FIXUP(T, z) 1 while z.p.color == RED 2 if z.p == z.p.p.left 3 y = z.p.p.right // y is z's uncle 4 if y.color == RED // ---- case 1 5 z.p.color = BLACK 6 y.color = BLACK 7 z.p.p.color = RED 8 z = z.p.p // move the violation up two levels 9 else 10 if z == z.p.right // ---- case 2 11 z = z.p 12 LEFT-ROTATE(T, z) // turn case 2 into case 3 13 z.p.color = BLACK // ---- case 3 14 z.p.p.color = RED 15 RIGHT-ROTATE(T, z.p.p) // and we are done 16 else (same as lines 3–15 with "right" and "left" exchanged) 17 T.root.color = BLACK
CaseUncleShapeActionResult
1 RED either Recolour only. Parent and uncle become black, grandparent becomes red. Violation moves up two levels. Loop continues.
2 BLACK z is a right child (a “zig-zag”) Left-rotate on z.p to straighten the line. Becomes case 3.
3 BLACK z is a left child (a straight line) Recolour parent black and grandparent red, then right-rotate on the grandparent. Done. Loop exits.
Case 1: uncle is RED C A D z uncle red-red violation recolour no rotation C A D z new z violation now two levels higher, or gone Black-heights are unchanged: A and D each gained a black, C lost one, so every path is even.
Figure 13.3 — Insert case 1. Pure recolouring, no rotation. The problem is not solved, only relocated upward — which is why case 1 is the only case that loops.
Why the loop terminates in O(lg n). Only case 1 continues the loop, and it moves z two levels up each time. So there are at most h/2 = O(lg n) iterations. Cases 2 and 3 each perform a rotation and then exit. Consequently RB-INSERT performs at most two rotations in total, no matter how large the tree.

The loop invariant CLRS maintains is worth noting: at the start of each iteration, z is red, and if z.p is the root then z.p is black, and at most one property is violated — either 2 or 4, never both. Line 17 unconditionally blackens the root, which fixes property 2 if case 1 reddened the root on its last pass.

Deletion

Deletion is the harder half, for a structural reason: removing a black node reduces the black-height of every path through it, breaking property 5, and property 5 is global.

The chapter needs a modified transplant that uses the sentinel and, crucially, sets v.p unconditionally:

RB-TRANSPLANT(T, u, v) 1 if u.p == T.nil 2 T.root = v 3 elseif u == u.p.left 4 u.p.left = v 5 else u.p.right = v 6 v.p = u.p // no NIL test — this is why we need T.nil

Compare with TRANSPLANT in Chapter 12, which guarded line 6 with if v ≠ NIL. Here v may be T.nil and we assign its parent anyway, deliberately, because the fixup needs to climb from it.

RB-DELETE(T, z) 1 y = z 2 y-original-color = y.color 3 if z.left == T.nil 4 x = z.right 5 RB-TRANSPLANT(T, z, z.right) 6 elseif z.right == T.nil 7 x = z.left 8 RB-TRANSPLANT(T, z, z.left) 9 else 10 y = TREE-MINIMUM(z.right) // the successor 11 y-original-color = y.color 12 x = y.right 13 if y.p == z 14 x.p = y 15 else 16 RB-TRANSPLANT(T, y, y.right) 17 y.right = z.right 18 y.right.p = y 19 RB-TRANSPLANT(T, z, y) 20 y.left = z.left 21 y.left.p = y 22 y.color = z.color // y inherits z's colour 23 if y-original-color == BLACK 24 RB-DELETE-FIXUP(T, x) // only then is anything broken
The three things to understand about RB-DELETE. First, y is the node actually removed from its position — either z itself, or z’s successor when z has two children. Second, line 22 gives y the colour of z, so the tree’s colouring at that position is unchanged; what matters is the colour y had at its old position, saved in y-original-color. Third, if that colour was red, nothing is broken and no fixup runs — removing a red node cannot change any black-height or create two adjacent reds.

The four delete cases

When a black node is removed, the fix is conceptual: pretend the node x that moved into its place carries an extra black. That restores property 5 arithmetically but makes x “doubly black”, which is not a real colour. The fixup’s job is to push that extra black up the tree until it can be discarded.

The controlling node is w, the sibling of x. Assuming x is a left child (the right-child cases mirror exactly):

CaseCondition on sibling wActionResult
1 w is red Recolour w black and x.p red, then left-rotate on x.p. New sibling is black. Converts to case 2, 3, or 4.
2 w black, both of w’s children black Recolour w red, move x up to x.p. Extra black moves up one level. Loop continues.
3 w black, w.left red, w.right black Recolour w.left black and w red, right-rotate on w. Becomes case 4.
4 w black, w.right red Copy x.p’s colour to w, blacken x.p and w.right, left-rotate on x.p, set x = T.root. Done. Loop exits.
RB-DELETE-FIXUP(T, x) 1 while x ≠ T.root and x.color == BLACK 2 if x == x.p.left 3 w = x.p.right // sibling 4 if w.color == RED // case 1 5 w.color = BLACK 6 x.p.color = RED 7 LEFT-ROTATE(T, x.p) 8 w = x.p.right 9 if w.left.color == BLACK and w.right.color == BLACK 10 w.color = RED // case 2 11 x = x.p 12 else 13 if w.right.color == BLACK // case 3 14 w.left.color = BLACK 15 w.color = RED 16 RIGHT-ROTATE(T, w) 17 w = x.p.right 18 w.color = x.p.color // case 4 19 x.p.color = BLACK 20 w.right.color = BLACK 21 LEFT-ROTATE(T, x.p) 22 x = T.root // terminate 23 else (same with "right" and "left" exchanged) 24 x.color = BLACK
Only case 2 loops, and it moves x up one level, so there are at most O(lg n) iterations. Cases 1, 3, and 4 each do at most one rotation and either transform into another case or terminate. Total: at most three rotations for a deletion, and O(lg n) time.

Line 24 is the discharge. If the loop exits because x is red, colouring it black absorbs the extra black and restores everything. If it exits because x is the root, the extra black simply disappears — removing one black from every path preserves property 5.

Do not memorise the delete cases. Nobody reproduces RB-DELETE-FIXUP from memory, and there is no reason to. What is worth carrying is the structure: an extra black is pushed up the tree, the sibling’s colour and its children’s colours select the case, only one case loops, and the whole thing is O(lg n) with at most three rotations. If you need the code, look it up — or, far better, use the balanced tree your standard library already ships.

Costs, and what it is good for

OperationRed-black treePlain BSTHash table
SEARCHO(lg n) worstO(n) worstO(1) expected
INSERTO(lg n) worstO(n) worstO(1) expected
DELETEO(lg n) worstO(n) worstO(1) expected
MINIMUM / MAXIMUMO(lg n)O(n) worstImpossible
SUCCESSOR / PREDECESSORO(lg n)O(n) worstImpossible
Sorted traversalΘ(n)Θ(n)Requires a sort
Range queryO(lg n + k)O(n) worstImpossible
Rotations per update≤ 2 insert, ≤ 3 delete0
When to reach for a balanced tree over a hash table. When you need order: iterate in sorted order, find the nearest key, answer a range query, or keep a leaderboard. When you need a worst-case guarantee rather than an expected one — a real-time system cannot tolerate an occasional O(n) rehash. Otherwise the hash table wins on constant factors, and by a lot.
Where you have already used one. C++ std::map and std::set, Java TreeMap and TreeSet, and the Linux kernel’s scheduler, memory manager, and epoll implementation are all red-black trees. The alternatives are AVL trees (more rigidly balanced, so faster lookups and more rotations per update) and B-trees from Chapter 18 (better when nodes live on disk). Red-black trees sit at a sweet spot of update cost versus balance quality, which is why they are the default.

Recap

The ten things to carry forward

Where this goes next

Part III is complete: hash tables for unordered speed, balanced trees for ordered guarantees. Part IV changes register entirely, from structures to design techniques. Chapter 14 covers dynamic programming, which solves optimisation problems by identifying overlapping subproblems and solving each exactly once. The red-black tree returns in Chapter 17, where augmenting it with extra fields per node yields order-statistic trees and interval trees at no asymptotic cost.


Ch 12 — Binary Search Trees Ch 14 — Dynamic Programming