Introduction to Algorithms (CLRS) — Chapter 13

Red-Black Trees

Self-balancing BSTs — guaranteed O(log n) with coloring rules and rotations.

Prerequisites: BSTs + Tree rotations. That's it.
10
Chapters
9+
Simulations
5
Interview Dimensions

Chapter 0: The Problem

You learned in Chapter 12 that a binary search tree gives O(h) search, insert, and delete, where h is the tree's height. When the tree is balanced, h = O(log n), and life is good. But what happens when you insert sorted data?

Try it. Insert the keys 1, 2, 3, 4, 5, 6, 7 into an empty BST. Each new key is larger than everything already in the tree, so it always goes to the right child. The result is not a tree — it is a linked list dangling to the right. Height = n - 1. Every operation is O(n). You paid for a tree and got a list.

This is not a rare pathology. Sorted and nearly-sorted inputs are common: alphabetical names, sequential IDs, time-ordered events, auto-incrementing primary keys. Any BST that cannot protect itself against bad input order is unreliable.

The core problem. A plain BST's shape depends on the insertion order, not the data. The same n keys can form a perfectly balanced tree (height log n) or a degenerate chain (height n-1). We need a BST that rebalances itself after every insert and delete, guaranteeing O(log n) height regardless of input order. That is what a red-black tree does.

How Bad Can It Get?

Let us quantify the damage. Consider n keys inserted in sorted order into a plain BST.

nBST height (sorted)Balanced heightRatio
10933x
10099616x
1,00099910100x
1,000,000999,9992050,000x

At a million keys, the degenerate BST needs 50,000 times more comparisons per search than a balanced tree. This is the difference between a microsecond lookup and a 50-millisecond one. In a database serving 10,000 queries per second, that means the difference between a responsive system and an unresponsive one.

The Solution: Enforce Balance

The idea is simple: after every insert or delete, check if the tree has become unbalanced, and if so, fix it. The question is how. There are many self-balancing BST schemes:

SchemeYearBalance criterion
AVL trees1962Height difference ≤ 1 at every node
2-3 trees1970All leaves at same depth, nodes have 2 or 3 children
Red-black trees1978Coloring rules that limit longest-to-shortest path ratio to 2:1
Splay trees1985No explicit criterion; amortized O(log n) via splaying
Treaps1989Random priorities maintain expected O(log n) height

Red-black trees have become the most widely used balanced BST in practice. They are the engine behind Java's TreeMap, C++'s std::map, and the Linux kernel's CFS scheduler. This chapter teaches you exactly how they work.

The simulation below lets you feel the difference viscerally. On the left, a plain BST. On the right, a red-black tree. Both receive the same keys in the same order. Watch the heights diverge.

BST vs Red-Black Tree — Same Keys, Same Order

Click "Insert Sorted" to add keys 1,2,3,... or "Insert Random" for shuffled keys. Watch the BST degenerate while the RB tree stays balanced.

With sorted input, the plain BST reaches height 15 after 15 inserts. The red-black tree stays at height 4 or 5. That is the difference between 15 comparisons per search and 5. At n = 1,000,000, it is the difference between a million comparisons and twenty.

The Idea in One Sentence

A red-black tree is a BST where every node is painted red or black, and five simple coloring rules force the tree to stay approximately balanced. When an insert or delete violates a rule, we fix it with rotations (restructuring the tree locally) and recoloring (flipping node colors). The fix-up takes O(log n) time in the worst case, so every operation remains O(log n).

Think of the colors as a balance budget. Red nodes are "free" — they do not count toward the tree's balance constraint. Black nodes are the structural backbone. By controlling where red nodes can appear (never two in a row), we ensure that the tree never gets too lopsided. The coloring rules are loose enough that fix-ups are cheap (few rotations), but tight enough that the height never exceeds 2 log(n+1).

The Key Innovation: Local Fixes for Global Balance

What makes red-black trees brilliant is not the coloring rules themselves — it is that violations can always be fixed locally. When you insert a node and create a red-red violation, you can fix it by either:

In contrast, imagine a "naive" approach to balancing: after every insert, compute the ideal balanced tree and rebuild. That would be O(n) per operation — useless. Red-black trees achieve the same result with O(log n) work per operation because the fix-up only touches nodes on a single root-to-leaf path.

This locality is the reason red-black trees scale to millions of nodes. The cost of maintaining balance is proportional to the tree height, not the tree size.

Concept check: You insert the keys 1, 2, 3, ..., 1000 into an empty plain BST. What is the height of the resulting tree?

Chapter 1: Red-Black Properties

A red-black tree is a BST with one extra bit per node: its color, either red or black. The tree also has sentinel NIL nodes — every null pointer is replaced by a special black NIL node. This simplifies the rules and the code (you never have to check for null when examining a node's uncle or sibling — it is always a valid node with a well-defined color).

In practice, we use a single sentinel node T.nil shared by all null pointers. The root's parent is T.nil. Every leaf's children are T.nil. T.nil is always black. This is not just a convenience — it is a critical implementation technique.

Without the sentinel, every time the fix-up code checks a node's uncle, sibling, or sibling's children, it must first check whether that pointer is null. With the sentinel, every pointer is valid and points to a node with a well-defined color (BLACK for T.nil). This eliminates a cascade of null checks that would otherwise clutter every case in the fix-up.

// The sentinel node: a single shared object
T.nil.color = BLACK
T.nil.key = 0       // doesn't matter — never compared
T.nil.left = T.nil   // self-referential — safe to dereference
T.nil.right = T.nil
T.nil.parent = T.nil

// Every internal node's "null" children point to T.nil
// The root's parent points to T.nil
// When fix-up code reads uncle.color → reads T.nil.color → BLACK
// No null pointer exception, no special case needed

The magic comes from five properties that every red-black tree must satisfy at all times:

#PropertyIntuition
1Every node is either red or blackBinary label — that is all
2The root is blackAnchors the tree; changing root color from red to black adds 1 to every path's black-height uniformly, so Property 5 is not violated
3Every leaf (NIL) is blackSentinels are always black — this gives a well-defined base case for counting black-height
4If a node is red, both its children must be blackNo two reds in a row on any path — this limits how many "free" nodes can be chained
5For each node, all paths from that node to its descendant NILs contain the same number of black nodesBlack-balanced — every path "looks the same" in black nodes

Property 4 is sometimes called the red rule or the no-double-red rule: no red node can have a red parent. Property 5 defines the black-height of a node: the number of black nodes on any path from that node down to a NIL (not counting the node itself). Because of Property 5, every node has a single well-defined black-height.

Why these rules guarantee balance. Property 5 says every root-to-leaf path has the same number of black nodes. Call that bh. Property 4 says you cannot have two consecutive reds, so the longest possible path alternates red-black-red-black and has length 2 · bh. The shortest possible path is all black, length bh. Therefore: the longest path is at most twice the shortest path. A tree where no path is more than twice any other is approximately balanced. Specifically, a red-black tree with n internal nodes has height at most 2 · log2(n+1).

Proving the Height Bound

Let us prove rigorously that a red-black tree with n internal nodes has height h ≤ 2 · log2(n+1). This is the fundamental theorem that makes red-black trees useful.

Claim 1: Any subtree rooted at node x contains at least 2bh(x) - 1 internal nodes, where bh(x) is the black-height of x.

Proof by induction on the height of x.

Base case: If x is a NIL leaf (height 0), then bh(x) = 0, and the subtree has 20 - 1 = 0 internal nodes. Correct.

Inductive step: Node x has two children. Consider a child c of x. If c is black, then bh(c) = bh(x) - 1. If c is red, then bh(c) = bh(x) (red nodes do not count). Either way, bh(c) ≥ bh(x) - 1. By the inductive hypothesis, each child's subtree has at least 2bh(x)-1 - 1 internal nodes. So x's subtree has at least:

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

Claim 2: bh(root) ≥ h/2.

Proof: On any root-to-leaf path of length h, at least half the nodes must be black. Why? Property 4 forbids consecutive reds, so between any two red nodes there must be a black node. The worst case is alternating red-black, which gives exactly h/2 black nodes. So bh(root) ≥ h/2.

Combining:

n ≥ 2bh(root) - 1 ≥ 2h/2 - 1
n + 1 ≥ 2h/2
log2(n + 1) ≥ h/2
h ≤ 2 · log2(n + 1)

// For n = 1,000,000: h ≤ 2 × 20 = 40
// Plain BST worst case: h = 999,999
// That's a 25,000x improvement in worst-case path length

Black-Height: A Worked Example

Consider this red-black tree (B = black, R = red):

      7(B)               bh = 2
     / \
   3(R)  18(R)         bh = 2 (red nodes don't reduce bh)
   / \   / \
  1(B) 5(B) 10(B) 22(B)  bh = 1

// Path from root to any NIL:
// 7(B) → 3(R) → 1(B) → NIL: 2 black nodes (7 and 1) ✓
// 7(B) → 18(R) → 22(B) → NIL: 2 black nodes (7 and 22) ✓
// All paths have 2 black nodes → black-height of root = 2 ✓

Notice: the red nodes (3, 18) do not count toward black-height. They are "free" additions that increase the actual height without violating the balance constraint. This is why the height bound is 2 · log(n+1), not log(n+1) — the factor of 2 accounts for the red nodes.

Which Properties Can Insertion Violate?

When we insert a new node z colored red, which properties might be violated?

PropertyViolated?Why or why not
1. Every node is red or blackNoz is red. Always satisfied.
2. Root is blackMaybeOnly if z is the root (tree was empty). Fixed by coloring root black.
3. NIL leaves are blackNoz's children are set to T.nil (black). Always satisfied.
4. Red node has black childrenMaybeIf z's parent is red, we have a red-red violation. This is the main case.
5. Equal black-heightsNoz is red, so it does not change any path's black count. Always satisfied.

So insertion can only violate Properties 2 (trivially fixed) and 4 (the red-red case). This is why the fix-up only needs to handle the red-red case — the other properties are automatically preserved.

Which Properties Can Deletion Violate?

PropertyViolated?Why or why not
1. Every node is red or blackNoWe do not change any node's color set. (The "double-black" is conceptual only.)
2. Root is blackMaybeIf the deleted node's replacement becomes the root and was red.
3. NIL leaves are blackNoT.nil is always black.
4. Red node has black childrenMaybeIf a red node becomes the child of another red node after restructuring.
5. Equal black-heightsMaybeIf a black node was removed, paths through its position have one fewer black node. This is the main case (double-black).

Deletion can violate Properties 2, 4, and 5. Property 5 is the "hard" violation that requires the four-case analysis. Properties 2 and 4 can be fixed as side effects of the fix-up or by a final recoloring.

The simulation below shows a red-black tree. Insert values and click any node to see its black-height. Verify that all five properties hold.

Red-Black Tree — Property Verifier

Insert values to build the tree. The property checker runs after every insert and reports any violations.

All 5 properties satisfied.
Concept check: A red-black tree has a root with black-height 4. What is the minimum number of internal nodes it can have?

Chapter 2: Rotations

Before we can fix red-black violations, we need a tool: rotations. A rotation is a local restructuring operation that changes the shape of the tree without breaking the BST property. It runs in O(1) — just pointer rewiring. No keys are compared, no data is copied.

Why do we need them? When an insertion creates a red-red violation, we cannot always fix it by recoloring alone. Sometimes we need to physically move nodes up or down to restore balance. Rotations are the mechanism for that movement.

There are two rotations, mirror images of each other:

Left Rotation on node x

Precondition: x has a right child y. The left rotation "pulls y up and pushes x down to the left." Think of it as pivoting the edge between x and y: y rises to x's position, x drops to become y's left child.

// Before left-rotate(x):
      x
     / \
    α   y
       / \
      β   γ

// After left-rotate(x):
      y
     / \
    x   γ
   / \
  α   β

// BST property preserved: α < x < β < y < γ
// This ordering is the same before and after

The key insight: subtree β (which was y's left child) becomes x's right child. It is the "orphan" that changes parents during the rotation. The in-order traversal (α, x, β, y, γ) remains identical.

Let us trace the pointer changes step by step:

Step 1
Set x.right = y.left (β). This detaches β from y and attaches it to x.
Step 2
If β is not NIL, set β.parent = x. (Update the parent pointer.)
Step 3
Set y.parent = x.parent. (y takes x's place in the tree.)
Step 4
Update x's parent to point to y (as left or right child, or as root).
Step 5
Set y.left = x and x.parent = y. Done. O(1) total — just pointer rewiring.

Right Rotation on node y

The exact mirror: y has a left child x, we pull x up and push y down to the right. Subtree β moves from x's right child to y's left child.

python
def left_rotate(T, x):
    y = x.right              # y is x's right child (must exist)
    x.right = y.left         # turn y's left subtree into x's right subtree
    if y.left != T.nil:
        y.left.parent = x   # update parent pointer for β
    y.parent = x.parent      # y takes x's position
    if x.parent == T.nil:    # x was the root
        T.root = y
    elif x == x.parent.left:
        x.parent.left = y
    else:
        x.parent.right = y
    y.left = x               # x becomes y's left child
    x.parent = y

def right_rotate(T, y):
    x = y.left               # exact mirror of left_rotate
    y.left = x.right
    if x.right != T.nil:
        x.right.parent = y
    x.parent = y.parent
    if y.parent == T.nil:
        T.root = x
    elif y == y.parent.left:
        y.parent.left = x
    else:
        y.parent.right = x
    x.right = y
    y.parent = x
Rotations preserve BST order, not color properties. A rotation does not change any node's color. It only restructures the tree shape. We will use rotations combined with recoloring to fix red-black violations after insert and delete. Think of rotations as the structural tool and recoloring as the cosmetic tool — both are needed.
Why rotations preserve BST property. Before and after a left rotation on x (with right child y), the in-order sequence is α, x, β, y, γ. Since in-order traversal gives sorted output for a BST, and this sequence is unchanged, the BST property is preserved. No keys move; only the tree's shape changes. This is why rotations are safe to apply at any time.

Rotation and Height: The Counterintuitive Effect

A left rotation on x decreases the height of x's right subtree by 1 and increases the height of x's left subtree by 1 (or leaves them unchanged, depending on subtree shapes). Think of it as "balancing a seesaw" — moving weight from the heavier (taller) right side to the lighter left side.

More precisely:

// Before left-rotate(x): x has depth d, y has depth d+1
// After left-rotate(x): y has depth d, x has depth d+1
// y moved UP one level, x moved DOWN one level
// Subtree β moved from depth d+2 (under y) to depth d+2 (under x) — same depth!
// Subtree α moved from depth d+1 (under x) to depth d+2 (under x under y) — one level deeper
// Subtree γ stayed at depth d+2 (under y) → depth d+1 (under y) — one level shallower

This is exactly what we want: when the right subtree is too tall, a left rotation brings it down and pushes the left subtree (which was shorter) down. The tree becomes more balanced.

How Many Rotations Does Each Operation Need?

OperationRotations (worst case)Why
Insert≤ 2Case 1 (recolor) repeats but does 0 rotations. Cases 2+3 do 1-2 rotations and terminate.
Delete≤ 3Case 1 does 1, Case 3 does 1, Case 4 does 1. Case 2 repeats but does 0.
Search0Read-only operation — no structural changes.

The bounded rotation count is crucial for performance because rotations, while O(1), are the most expensive part of the fix-up (they modify 5-6 pointers and potentially invalidate cached pointers in concurrent settings). The O(log n) recolorings are cheaper because they only modify a single field per node.

The simulation below lets you perform rotations interactively. Click a node, then click "Left Rotate" or "Right Rotate." Watch how the subtrees move while the in-order sequence stays the same.

Interactive Rotations

Click a node to select it (highlighted yellow). Then click Left/Right Rotate. The in-order sequence below the tree never changes.

Click a node to select it.
Concept check: After a left rotation on node x (where y = x.right), which subtree changes parents?

Chapter 3: Insertion

Inserting into a red-black tree has two phases. Phase 1 is ordinary BST insertion — walk down the tree, find the right spot, attach the new node as a leaf. Phase 2 is the fix-up — restore any red-black properties that were violated.

Phase 1: Insert as Red

We insert the new node z exactly as in a regular BST, then color it red. Why red? Think about what each color choice would break.

If we color z black: every path through z now has one more black node than before. But paths that do not go through z are unchanged. Property 5 (all paths have equal black-height) is violated, and it is violated globally — every path through z's parent is affected. This is a nightmare to fix.

If we color z red: red nodes do not count toward black-height, so Property 5 is preserved automatically. The only property we might violate is Property 4 (no red-red), and only if z's parent is also red. This is a local violation involving just two adjacent nodes.

Key insight: we trade a hard violation for an easy one. A black-height violation (Property 5) is catastrophic — it means the entire tree's balance invariant is broken globally. A red-red violation (Property 4) is local — it involves just two adjacent nodes. We always choose the local violation because it is easier to fix. This is a recurring pattern in algorithm design: choose the formulation that creates the least damage.

When Is There a Violation?

After inserting z as red, we check: is z's parent red? If not, all properties hold and we are done. If z's parent is red, we have a red-red violation. Note that z's grandparent must be black (because the tree was valid before our insert, and z's parent is red, so the grandparent cannot also be red by Property 4).

Phase 2: Fix-up Cases

The fix-up depends on the color of z's uncle — the sibling of z's parent. Let p = z's parent and g = z's grandparent.

Case 1: Uncle is RED
Recolor. Set parent and uncle to black, grandparent to red. This fixes the local red-red violation without changing any path's black-height (we removed one black from the uncle's side, added one to the parent's side, and moved the red up to grandparent). But now grandparent might be red-red with its parent. Move z up to grandparent and repeat.
↓ if uncle is black
Case 2: Uncle is BLACK, z is an "inner" child
Rotate z's parent to convert into Case 3. If p is a left child of g and z is p's right child, left-rotate p. Now z and p swap positions, and z becomes an "outer" child. (Mirror: if p is right child and z is left child, right-rotate p.)
Case 3: Uncle is BLACK, z is an "outer" child
Rotate grandparent and recolor. If p is g's left child, right-rotate g. Then recolor: p becomes black, g becomes red. The red-red violation is resolved and black-heights are preserved. Done.
What is an "inner" vs "outer" child? If z's parent p is a left child of grandparent g, then z is "outer" if z is also a left child (left-left = outer), and "inner" if z is a right child (left-right = inner). If p is a right child, mirror it: right-right = outer, right-left = inner. Case 2 straightens the bend (inner → outer) so Case 3 can do a clean rotation.

Case 1 may repeat up the tree (O(log n) times in the worst case), but Cases 2 and 3 each perform at most one or two rotations and then terminate. So insertion does at most 2 rotations total. The rest of the fix-up is pure recoloring, which is O(1) per level.

Worked Example: Insert 7, 3, 18, 10, 22, 8, 11

Let us trace every step. We use the notation key(B) for black and key(R) for red.

// Insert 7: tree is empty → 7 becomes root → color BLACK (Property 2)
  7(B)

// Insert 3: 3 < 7, goes left. Color RED. Parent 7 is black → no violation.
    7(B)
   /
  3(R)

// Insert 18: 18 > 7, goes right. Color RED. Parent 7 is black → no violation.
    7(B)
   / \
  3(R) 18(R)

// Insert 10: path is 7→18→left. Color RED.
// Parent 18 is RED → violation! Uncle 3 is RED → Case 1!
// Recolor: 18→B, 3→B, 7→R. But 7 is root → force 7→B (Property 2).
    7(B)
   / \
  3(B) 18(B)
     /
   10(R)

// Insert 22: path is 7→18→right. Color RED. Parent 18 is black → no violation.
     7(B)
    / \
   3(B) 18(B)
      / \
    10(R) 22(R)

// Insert 8: path is 7→18→10→left. Color RED.
// Parent 10 is RED → violation! Uncle 22 is RED → Case 1!
// Recolor: 10→B, 22→B, 18→R.
// Now check 18: parent 7 is BLACK → no further violation. Done.
     7(B)
    / \
   3(B) 18(R)
      / \
    10(B) 22(B)
    /
   8(R)

// Insert 11: path is 7→18→10→right. Color RED.
// Parent 10 is BLACK → no violation. Done.
      7(B)
     / \
    3(B) 18(R)
       / \
     10(B) 22(B)
     / \
    8(R) 11(R)

In this example, we never hit Cases 2 or 3 — every violation was fixed by Case 1 (recoloring). Let us trace one more example that triggers rotations.

Worked Example: Insert 1, 2, 3 (Sorted — Triggers Rotation)

// Insert 1: root, color BLACK.
1(B)

// Insert 2: 2 > 1, goes right. Color RED. Parent 1 is black → OK.
1(B)
 \
  2(R)

// Insert 3: 3 > 2, goes right. Color RED.
// Parent 2 is RED → violation! Uncle is NIL (black) → not Case 1.
// z=3 is right child, parent 2 is right child → outer child → Case 3!
// Left-rotate grandparent 1. Recolor: 2→B, 1→R.
  2(B)
 / \
1(R) 3(R)
// A perfectly balanced tree! One rotation fixed the sorted-order degeneracy.
python
def rb_insert(T, z):
    # Phase 1: standard BST insert
    y = T.nil
    x = T.root
    while x != T.nil:
        y = x
        if z.key < x.key:
            x = x.left
        else:
            x = x.right
    z.parent = y
    if y == T.nil:
        T.root = z          # tree was empty
    elif z.key < y.key:
        y.left = z
    else:
        y.right = z
    z.left = T.nil
    z.right = T.nil
    z.color = RED             # always insert as red
    rb_insert_fixup(T, z)     # Phase 2: fix violations

def rb_insert_fixup(T, z):
    while z.parent.color == RED:           # violation exists
        if z.parent == z.parent.parent.left:  # parent is left child
            y = z.parent.parent.right         # y = uncle
            if y.color == RED:                # Case 1: uncle red
                z.parent.color = BLACK
                y.color = BLACK
                z.parent.parent.color = RED
                z = z.parent.parent           # move up
            else:
                if z == z.parent.right:       # Case 2: inner child
                    z = z.parent
                    left_rotate(T, z)         # straighten to outer
                z.parent.color = BLACK        # Case 3: outer child
                z.parent.parent.color = RED
                right_rotate(T, z.parent.parent)
        else:                                 # mirror: parent is right child
            y = z.parent.parent.left          # uncle
            if y.color == RED:                # Case 1
                z.parent.color = BLACK
                y.color = BLACK
                z.parent.parent.color = RED
                z = z.parent.parent
            else:
                if z == z.parent.left:        # Case 2
                    z = z.parent
                    right_rotate(T, z)
                z.parent.color = BLACK        # Case 3
                z.parent.parent.color = RED
                left_rotate(T, z.parent.parent)
    T.root.color = BLACK                      # Property 2: root always black

Worked Example: Insert 1, 2, 3, 4, 5, 6, 7 (All Sorted — Worst Case for BST)

This is the killer sequence for plain BSTs. Let us watch the RB tree handle it gracefully.

// Insert 1: root → 1(B)

// Insert 2: right of 1. 2(R). Parent black → OK.
1(B)─2(R)

// Insert 3: right of 2. 3(R). Parent 2 is RED → violation!
// Uncle is NIL (black). z=3, parent=2 (right child), z is right child → outer → Case 3.
// Left-rotate grandparent 1. Recolor: 2→B, 1→R.
  2(B)
 / \
1(R) 3(R)

// Insert 4: right of 3. 4(R). Parent 3 is RED → violation!
// Uncle is 1 (RED) → Case 1! Recolor: 3→B, 1→B, 2→R. 2 is root → 2→B.
  2(B)
 / \
1(B) 3(B)
      \
     4(R)

// Insert 5: right of 4. 5(R). Parent 4 is RED → violation!
// Uncle is NIL (black). z=5, outer child → Case 3.
// Left-rotate 3. Recolor: 4→B, 3→R.
   2(B)
  / \
 1(B) 4(B)
     / \
   3(R) 5(R)

// Insert 6: right of 5. 6(R). Parent 5 is RED → violation!
// Uncle is 3 (RED) → Case 1! Recolor: 5→B, 3→B, 4→R.
// Now 4 is RED, parent 2 is BLACK → no violation. Done.
   2(B)
  / \
 1(B) 4(R)
     / \
   3(B) 5(B)
         \
        6(R)

// Insert 7: right of 6. 7(R). Parent 6 is RED → violation!
// Uncle is NIL (black). z=7, outer child → Case 3.
// Left-rotate 5. Recolor: 6→B, 5→R.
    2(B)
   / \
  1(B) 4(R)
      / \
    3(B) 6(B)
         / \
       5(R) 7(R)

// Final height: 4. Plain BST height would be 6.
// 4 rotations total for 7 insertions. Most fix-ups were recoloring only.

Notice the pattern: every other insertion triggers a fix-up, alternating between Case 1 (recolor) and Case 3 (rotate). The tree never degenerates. After 7 sorted insertions, the height is 4 instead of the BST's 6.

Complete Implementation: RB Node and Tree Class

python
RED, BLACK = 0, 1

class RBNode:
    __slots__ = ['key', 'color', 'left', 'right', 'parent']
    def __init__(self, key=0, color=BLACK):
        self.key = key
        self.color = color
        self.left = None     # will be set to T.nil
        self.right = None
        self.parent = None

class RedBlackTree:
    def __init__(self):
        self.nil = RBNode()  # sentinel: color=BLACK, key=0
        self.nil.left = self.nil
        self.nil.right = self.nil
        self.nil.parent = self.nil
        self.root = self.nil

    def search(self, key):
        x = self.root
        while x != self.nil:
            if key == x.key: return x
            x = x.left if key < x.key else x.right
        return None  # not found

    def minimum(self, x=None):
        if x is None: x = self.root
        while x.left != self.nil: x = x.left
        return x

    def inorder(self, x=None):
        if x is None: x = self.root
        if x == self.nil: return []
        return self.inorder(x.left) + [x.key] + self.inorder(x.right)

    # insert, delete, rotations, fix-ups as shown above...
Insertion complexity summary. BST insertion: O(log n) to walk down the tree. Fix-up: O(log n) in the worst case (Case 1 can repeat up to the root), but at most 2 rotations total (Cases 2+3). The recoloring in Case 1 is O(1) per level. Total: O(log n) time, O(1) rotations amortized.
Insertion — Build Your Own RB Tree

Enter a key and click Insert. The tree rebalances automatically. Try inserting sorted keys (1, 2, 3, ...) to see rotations fire.

Tree is empty. Insert a key to begin.
Concept check: Why do we color a newly inserted node red instead of black?

Chapter 4: Deletion

Deletion is the most complex operation in a red-black tree. If insertion is learning to ride a bicycle, deletion is learning to unicycle while juggling. The core idea is the same — delete as in a regular BST, then fix any violations — but the case analysis is more involved.

Phase 1: BST Deletion with Tracking

Before diving into the RB-specific details, let us recall BST deletion from Chapter 12. When we delete a node z, there are three cases based on z's children:

// Case A: z has no children (leaf)
// Just remove z. Replace z with NIL in z's parent.

// Case B: z has one child
// Splice z out: replace z with its only child.
// The child takes z's position in the tree.

// Case C: z has two children
// Find z's in-order successor y (leftmost node in z's right subtree).
// y has no left child (otherwise it wouldn't be leftmost).
// Copy y's key into z's position, then delete y (which is Case A or B).
// Alternatively (CLRS approach): physically move y to z's position.

In the red-black version, we use the CLRS approach of physically moving the successor. This means we need to track which node was actually spliced out and what color it had. The critical variables:

VariableWhat it isWhy we need it
yThe node actually removed or moved within the treeIts original color tells us whether we violated Property 5
y-original-colorThe color y had before the operationIf it was red, no violation. If black, we need fix-up.
xThe node that takes y's position (might be T.nil)This is where we start the fix-up — x is the "double black" node

When Do We Need Fix-Up?

If the removed/moved node y was red, no properties are violated. Why?

If the removed/moved node y was black, we have a problem: every path through y's former position now has one fewer black node. Property 5 is violated. We fix this with the "double-black" technique.

The "double-black" concept. When a black node is removed, its replacement x conceptually becomes doubly black: it counts as two black nodes for Property 5 purposes. This is a bookkeeping trick — no node actually stores "double-black" as a color. The fix-up procedure's job is to convert this double-black into a regular single black by redistributing the extra blackness through rotations and recoloring.

The simplest resolution: if x is red, just color it black. The extra blackness is absorbed. Done. The complex cases arise when x is already black (making it "double-black"), requiring case analysis.

Deletion Fix-Up: Four Cases

In all four cases, x is the double-black node and w is its sibling. We assume x is a left child (the right-child case is a symmetric mirror). The goal: push the extra blackness up or eliminate it via rotation+recoloring.

Case 1: Sibling w is RED
Swap colors of w and x's parent (w→black, parent→red), then left-rotate parent. Now x's new sibling is black, converting to Cases 2, 3, or 4. (Case 1 is a "prep" case.)
Case 2: w is BLACK, both of w's children are BLACK
Recolor w to red (removing one black from both x's side and w's side). Push the extra black up to x's parent. If the parent was red, color it black and we're done. If the parent was already black, it becomes the new double-black and we repeat from there.
↓ if w has a red child
Case 3: w is BLACK, w's far child is BLACK, near child is RED
Swap colors of w and w's near child (w→red, near child→black), then rotate w toward the far side. Now w's far child is red, transforming into Case 4.
Case 4: w is BLACK, w's far child is RED
Copy parent's color to w, color parent black, color w's far child black, then rotate parent toward x. The extra blackness is absorbed. Done.
Rotation count for deletion. Case 1 does 1 rotation and falls through to Case 2, 3, or 4. Case 3 does 1 rotation and falls through to Case 4. Case 4 does 1 rotation and terminates. Case 2 does zero rotations but may repeat upward. So deletion does at most 3 rotations total — the rest of the fix-up is recoloring. This is remarkable: no matter how large the tree, deletion never needs more than 3 rotations.

Worked Example: Deletion Triggering Case 4

Consider a tree with keys [1, 2, 3, 4, 5] after RB insertion. Delete key 1 (a black leaf).

// Before deletion (after inserting 1,2,3,4,5):
    2(B)
   / \
  1(B) 4(B)
      / \
    3(R) 5(R)

// Delete 1: it's a black leaf with no children.
// Replace with NIL. x = NIL (black). y-orig-color = BLACK → need fix-up.
// x = NIL (left child of 2). Sibling w = 4 (BLACK).
// w's children: 3(R) left, 5(R) right.
// w's far child (5) is RED → Case 4!
// Copy parent(2)'s color(B) to w(4): 4→B. Parent 2→B. Far child 5→B.
// Left-rotate parent 2.

    4(B)
   / \
  2(B) 5(B)
   \
   3(R)
// Done! One rotation, all properties restored.
python
def rb_transplant(T, u, v):
    """Replace subtree rooted at u with subtree rooted at v."""
    if u.parent == T.nil:
        T.root = v
    elif u == u.parent.left:
        u.parent.left = v
    else:
        u.parent.right = v
    v.parent = u.parent    # even if v is T.nil (this is why sentinel is useful)

def rb_delete(T, z):
    y = z
    y_orig_color = y.color
    if z.left == T.nil:          # no left child
        x = z.right
        rb_transplant(T, z, z.right)
    elif z.right == T.nil:       # no right child
        x = z.left
        rb_transplant(T, z, z.left)
    else:                        # two children: use successor
        y = tree_minimum(T, z.right)
        y_orig_color = y.color
        x = y.right
        if y.parent == z:
            x.parent = y     # important when x is T.nil
        else:
            rb_transplant(T, y, y.right)
            y.right = z.right
            y.right.parent = y
        rb_transplant(T, z, y)
        y.left = z.left
        y.left.parent = y
        y.color = z.color    # inherit deleted node's color
    if y_orig_color == BLACK:
        rb_delete_fixup(T, x) # fix the double-black at x

def rb_delete_fixup(T, x):
    while x != T.root and x.color == BLACK:
        if x == x.parent.left:
            w = x.parent.right              # sibling
            if w.color == RED:              # Case 1: red sibling
                w.color = BLACK
                x.parent.color = RED
                left_rotate(T, x.parent)
                w = x.parent.right          # new sibling is black
            if w.left.color == BLACK and w.right.color == BLACK:  # Case 2
                w.color = RED
                x = x.parent               # push double-black up
            else:
                if w.right.color == BLACK:  # Case 3: near child red
                    w.left.color = BLACK
                    w.color = RED
                    right_rotate(T, w)
                    w = x.parent.right
                w.color = x.parent.color    # Case 4: far child red
                x.parent.color = BLACK
                w.right.color = BLACK
                left_rotate(T, x.parent)
                x = T.root                  # done — exit loop
        else:                               # symmetric mirror cases
            w = x.parent.left
            if w.color == RED:
                w.color = BLACK
                x.parent.color = RED
                right_rotate(T, x.parent)
                w = x.parent.left
            if w.right.color == BLACK and w.left.color == BLACK:
                w.color = RED
                x = x.parent
            else:
                if w.left.color == BLACK:
                    w.right.color = BLACK
                    w.color = RED
                    left_rotate(T, w)
                    w = x.parent.left
                w.color = x.parent.color
                x.parent.color = BLACK
                w.left.color = BLACK
                right_rotate(T, x.parent)
                x = T.root
    x.color = BLACK    # absorb the extra black
Deletion — Interactive

Build a tree with "Random x5", then enter a key and click "Delete" to see the tree rebalance.

Add some nodes first, then delete one to see fix-up.

Deletion Summary: When Each Case Applies

The four cases can feel overwhelming. Here is a decision tree to help you remember which case applies when:

// x is the double-black node, w is its sibling

Is w RED?
  YES → Case 1. Rotate parent, recolor. Now w is black → check again.
  NO (w is BLACK):
    Are BOTH of w's children BLACK?
      YES → Case 2. Recolor w red. Push double-black up to parent.
      NO (at least one child is RED):
        Is w's FAR child RED?
          YES → Case 4. Rotate parent, recolor. Done.
          NO → Case 3. Rotate w, recolor. Now far child is red → Case 4.

The key memory aid: Cases 1 and 3 are "setup" cases that transform into other cases. Case 2 pushes the problem upward (may repeat). Case 4 is the "terminal" case that resolves everything. If you reach Case 4, you are done.

Why deletion is harder than insertion. In insertion, the violation is always "too many reds" (Property 4). The fix-up either pushes the redness up (Case 1) or rotates it away (Cases 2+3). In deletion, the violation is "too few blacks" (Property 5). We need to either add a black node (by recoloring a red to black) or redistribute blackness via rotation. The asymmetry between adding and removing makes deletion inherently more complex. This is not unique to red-black trees — AVL deletion is also more complex than insertion.
Complete operation complexity. Every red-black tree operation is O(log n):
Search: O(log n) — standard BST search, height is O(log n).
Insert: O(log n) walk + O(log n) fix-up (at most 2 rotations).
Delete: O(log n) walk + O(log n) fix-up (at most 3 rotations).
Min/Max: O(log n) — walk left/right to the leaf.
Successor/Predecessor: O(log n).
All worst-case, not amortized. This is the RB tree's defining advantage.
Concept check: What is the maximum number of rotations performed during a single red-black tree deletion?

Chapter 5: Red-Black Tree Showcase

This is the payoff. The full interactive red-black tree simulation. Insert and delete keys. Watch the tree rebalance with rotations and recoloring. Toggle labels to see black-heights, node depths, and NIL nodes.

How to explore. Try these experiments in order:
1. Insert a sorted sequence (click "Insert Sorted" 15 times) and watch the tree stay balanced — rotations fire automatically.
2. Toggle "Show Black Height" to verify Property 5 at every node.
3. Toggle "Show NIL Nodes" to see the sentinel leaves.
4. Delete the root and watch the fix-up cascade.
5. Try inserting 10 random keys and then deleting them one by one.
Full Red-Black Tree — Insert, Delete, Visualize

Insert and delete keys. The tree rebalances automatically. Toggle overlays with the checkboxes below.

Tree is empty. Insert a key to begin.

Experiments to Try

ExperimentWhat to observe
Insert 1, 2, 3, ..., 15 in orderHeight stays ≤ 6 (plain BST would have height 14). Count the rotations.
Insert 8, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7Nearly perfect balance — almost no rotations needed because the order is good.
Insert 10 random keys, then delete the rootWatch the deletion fix-up — the sibling cases kick in.
Toggle "Show Black Height" and count pathsEvery node's left and right subtrees have equal black-height. Property 5 in action.
Toggle "Show NIL Nodes"See the sentinel leaves — every leaf is a black NIL. Property 3 visualized.
Insert 1-15 sorted, then delete all odd numbersThe tree shrinks but stays balanced. Deletion fix-up maintains the invariant.
The height bound in practice. The theoretical bound is h ≤ 2 log(n+1). In practice, red-black trees are usually much shorter than this bound. For 15 nodes, the bound says h ≤ 8, but the actual tree typically has height 4-5. For 1000 nodes, the bound says h ≤ 20, but typical heights are 12-14. The bound is a worst case, not the average case.

Understanding the Color Distribution

In a well-populated red-black tree, roughly what fraction of nodes are red? It depends on the insertion order, but here are some useful facts:

ScenarioRed fractionWhy
Perfect binary tree (all nodes black)0%A complete black tree is a valid RB tree (trivially satisfies all properties)
Alternating insertions (good balance)~30-40%Some nodes end up red after recoloring, but most stay black
Sorted insertions (worst-case shape)~50%Maximum red nodes — the tree is as "stretched" as the RB rules allow
Maximum possible (theoretical)50%In the tallest valid RB tree, every other node on the longest path is red

The fraction of red nodes tells you how "stretched" the tree is beyond its minimum possible height. More red = taller = closer to the 2 log(n+1) bound. Fewer red = shorter = closer to the optimal log(n+1) height.

Operation Cost Breakdown

Here is a complete summary of what each operation costs, broken down by component:

OperationWalk downFix-up iterationsRotationsTotal
SearchO(log n)00O(log n)
InsertO(log n)O(log n)≤ 2O(log n)
DeleteO(log n)O(log n)≤ 3O(log n)
Min / MaxO(log n)00O(log n)
SuccessorO(log n)00O(log n)
In-order walkO(n)00O(n)

The bounded rotation count is what makes red-black trees special. Although the fix-up loop can iterate O(log n) times (pushing the violation upward), each iteration does O(1) work (recoloring). The expensive rotations happen at most 2 (insert) or 3 (delete) times total.

Chapter 6: AVL Trees Comparison

Red-black trees are not the only self-balancing BST. AVL trees (Adelson-Velsky and Landis, 1962) came first — six years before red-black trees — and use a stricter balance criterion: for every node, the heights of its left and right subtrees differ by at most 1.

AVL Balance Factor

balance(x) = height(x.left) - height(x.right)

// AVL property: balance(x) ∈ {-1, 0, 1} for every node x
// If |balance(x)| ≥ 2 after an insert/delete, rotate to fix

The AVL balance factor is stored at each node (or equivalently, the height). When an insertion or deletion makes some node's balance factor ±2, the AVL tree performs rotations to restore balance. There are four cases, analogous to the RB tree's cases:

AVL CaseConditionFixRB Equivalent
Left-Leftbalance = +2, left child balance ≥ 0Right-rotateCase 3 (outer child)
Left-Rightbalance = +2, left child balance < 0Left-rotate child, then right-rotateCases 2+3 (inner then outer)
Right-Rightbalance = -2, right child balance ≤ 0Left-rotateCase 3 mirror
Right-Leftbalance = -2, right child balance > 0Right-rotate child, then left-rotateCases 2+3 mirror

The AVL cases are simpler to understand (no uncle, no coloring), but AVL deletion can cascade rotations while RB deletion cannot.

The rotations are the same building blocks as red-black (left-rotate, right-rotate), plus "double rotations" — rotate child then parent — which correspond to Cases 2+3 in RB insertion.

RB vs AVL: The Complete Trade-Off Analysis

PropertyRed-Black TreeAVL Tree
Height boundh ≤ 2 · log2(n+1)h ≤ 1.44 · log2(n+2)
Balance strictnessLoose (no path > 2x another)Strict (subtree heights differ by ≤ 1)
Search speedSlightly slower (taller tree)Slightly faster (shorter tree)
Insert rotationsAt most 2At most 2
Delete rotationsAt most 3O(log n) — may cascade all the way up
Storage overhead1 bit per node (color)2 bits per node (balance factor or stored height)
Best forInsert/delete-heavy workloadsRead-heavy workloads
The key trade-off is deletion cost. AVL deletion can trigger O(log n) rotations because fixing the balance at one level can unbalance the level above it, cascading all the way to the root. Red-black deletion does at most 3 rotations — period. This is because the RB coloring scheme provides more "slack" in the balance constraint. The looser balance (height up to 2 log n instead of 1.44 log n) is the price we pay for fewer rotations during mutations. For workloads with frequent mutations, red-black wins. For read-heavy workloads where the tree rarely changes, AVL wins because the shorter height means fewer comparisons per lookup.

Height Comparison in Practice

How much taller is an RB tree than an AVL tree? Let us compare:

nAVL max heightRB max heightDifference
1001014+4 (40%)
1,0001520+5 (33%)
1,000,0002940+11 (38%)
1,000,000,0004460+16 (36%)

The RB tree is about 30-40% taller than the AVL tree. In absolute terms, this means a few extra comparisons per search — usually negligible unless you are doing billions of lookups.

When AVL Wins: Read-Dominated Workloads

Database indexes are the classic example. You build the index once (or update it infrequently), then query it millions of times. Each query descends the tree from root to leaf. With an AVL tree, the path is 1.44 log n nodes; with an RB tree, it is up to 2 log n. For n = 1 billion, that is 44 vs 60 comparisons — a 36% difference that compounds over millions of queries.

In practice, database indexes usually use B-trees (even better for disk access), but for in-memory read-dominated workloads, AVL trees are optimal.

When RB Wins: Write-Heavy Workloads

Consider a network router's routing table, updated thousands of times per second as routes appear and disappear. Each update is an insert or delete. With an AVL tree, each delete can trigger O(log n) rotations — restructuring the tree from the deletion point all the way to the root. With an RB tree, each delete triggers at most 3 rotations. The rest of the fix-up is pure recoloring (O(1) per node).

For latency-sensitive applications where every operation must be fast (not just on average), the RB tree's bounded rotation count provides more predictable performance.

The Rotation Cascade in AVL Deletion

Why can AVL deletion trigger O(log n) rotations while RB deletion is bounded at 3? The key difference is in the balance criterion's "tightness."

In an AVL tree, deleting a node can reduce the height of one subtree by 1, making the parent's balance factor ±2. A rotation fixes the parent, but the rotation itself can reduce the subtree's height by 1, making the grandparent's balance factor ±2. This cascading effect can propagate all the way to the root.

In an RB tree, the coloring rules provide more slack. When a black node is removed, the "double-black" push-up (Case 2) does not change the tree's shape — it only recolors. Eventually, the double-black either reaches a red node (absorbed by coloring it black) or triggers a rotation (Cases 3-4) that terminates the fix-up. The slack in the 2:1 height ratio absorbs the imbalance without cascading rotations.

The simulation below inserts the same keys into both an AVL tree and a red-black tree side by side. Watch the heights and rotation counts diverge.

AVL vs Red-Black Tree — Side by Side

Insert keys into both trees simultaneously. Compare heights and rotation counts. Try sorted input to see the biggest difference.

Concept check: Your application is a database index that is written to once (bulk-loaded) and then read millions of times. Which self-balancing BST is better, and why?

Chapter 7: Balanced BSTs in Practice

Red-black trees are everywhere in systems software. You have been using them every day without knowing it. Let us trace exactly where they appear and why they were chosen.

Where Red-Black Trees Live

SystemUseWhy RB specifically?
Java TreeMap / TreeSetSorted map/set implementation in java.utilO(log n) guaranteed for all operations. NavigableMap interface needs range queries (subMap, headMap, tailMap) which require sorted order.
C++ std::map / std::setOrdered associative containers in the STLC++ standard mandates O(log n) worst-case insert/delete/search. Most implementations (libstdc++, libc++) use RB trees. The bounded rotation count (2-3) is important for real-time guarantees.
Linux CFS SchedulerCPU scheduling — tracks runnable processes by virtual runtimeNeeds O(log n) insert and delete-by-key, plus fast min (leftmost node). RB tree with cached leftmost pointer. Used for ~15 years (2007-2023) before being replaced by EEVDF.
Linux vm_area_structVirtual memory area management — tracks process address space regionsProcesses can have thousands of VMAs. Fast lookup by address, efficient insertion when mmap is called. Replaced by maple tree in kernel 6.1, but RB trees were used for decades.
NginxTimer management for connection timeoutsNeeds fast min (next expiring timer) plus O(log n) insert/delete as connections open and close.
LLVMIntervalMap for register allocationMaps ranges to values. Needs efficient overlap queries and updates.
epoll (Linux)File descriptor tracking for I/O multiplexingManaging thousands of watched file descriptors with efficient add/remove.

Why Not Other Structures?

AlternativeAdvantageWhy not always?
AVL treesShorter height (1.44 log n), faster lookupsO(log n) rotations on delete; slightly more complex to implement correctly
B-treesCache-friendly (wide nodes fit cache lines), optimal for disk I/OHigher constant factors for in-memory use; more complex node splitting/merging
Skip listsSimple to implement, naturally lock-free friendlyProbabilistic guarantees only (expected O(log n), not worst-case); higher memory overhead (multiple forward pointers per node)
Hash tablesO(1) average search and insertNo ordering — cannot do range queries, successor, predecessor, or sorted iteration. O(n) worst case with bad hash function.
Splay treesAmortized O(log n), adapts to access patterns (frequently accessed nodes migrate to root)O(n) worst case per individual operation; not suitable for real-time systems where every operation must be fast
The RB tree's killer feature is predictability. Unlike splay trees (amortized), skip lists (probabilistic), or hash tables (amortized with occasional rehash), red-black trees give worst-case O(log n) for every single operation with a bounded number of rotations. For systems like CPU schedulers where every scheduling decision must complete quickly, or for real-time systems where latency spikes are unacceptable, worst-case guarantees are essential.

The Linux CFS Scheduler: A Case Study

The Completely Fair Scheduler (CFS) used red-black trees for 16 years (2007-2023) to manage runnable processes. Here is how it worked:

1. Virtual Runtime (vruntime)
Each process has a vruntime — a measure of how much CPU time it has consumed, weighted by its priority (nice value). A high-priority process's vruntime increases slowly; a low-priority process's increases quickly. Lower vruntime = "more deserving" of CPU.
2. RB Tree Keyed by vruntime
All runnable processes are stored in a red-black tree, keyed by vruntime. The leftmost node (minimum vruntime) is the process that has received the least (weighted) CPU time.
3. Pick Next = Leftmost Node
To choose the next process, CFS picks the leftmost node in the RB tree. This is O(1) because CFS caches the leftmost pointer — a common optimization for RB trees when you frequently need the minimum. After running, the process's vruntime increases and it is re-inserted.
4. Fairness Guarantee
Over time, all processes converge to similar vruntimes. No process starves because any starved process eventually has the lowest vruntime and gets scheduled. The RB tree makes this efficient: insert O(log n), delete O(log n), find-min O(1).

Why not a min-heap? Heaps give O(1) find-min and O(log n) insert, but deleting an arbitrary element requires O(n) to find it first. When a process blocks (sleeps on I/O, waits on a mutex), CFS must remove it from the tree immediately — this requires O(log n) deletion by key, which an RB tree provides but a heap does not.

Why not a sorted array? With n = 1000 runnable processes, every insert and delete would require O(n) shifting. The scheduler runs hundreds of times per second (each time a process exhausts its time slice, blocks, or wakes up). O(n) per operation is too slow.

Why not a hash table? The scheduler needs the minimum vruntime process, not just any process. Hash tables have no ordering. You would need to scan all entries to find the minimum — O(n).

The pattern. Whenever you need: (1) ordered data, (2) fast insert, (3) fast delete-by-key, and (4) fast min/max — use a balanced BST. This pattern appears constantly: timer queues, priority-aware scheduling, interval management, order books in trading systems, sliding window analytics.

The Sentinel Pattern in Production Code

The sentinel NIL node is not just a textbook convenience — it is a real optimization used in production RB tree implementations. Here is why:

python
# WITHOUT sentinel: every operation must check for None
def get_uncle(node):
    if node.parent is None:            # check 1
        return None
    if node.parent.parent is None:    # check 2
        return None
    if node.parent == node.parent.parent.left:
        uncle = node.parent.parent.right
    else:
        uncle = node.parent.parent.left
    return uncle                       # might still be None

# WITH sentinel: no None checks needed — T.nil has color BLACK
def get_uncle(node):
    if node.parent == node.parent.parent.left:
        return node.parent.parent.right   # always valid (may be T.nil)
    else:
        return node.parent.parent.left    # T.nil.color == BLACK

The sentinel eliminates 4-6 null checks per fix-up iteration. Over millions of insert/delete operations, this adds up. The Linux kernel's rbtree implementation uses a slightly different trick: instead of a sentinel, it stores the color bit in the parent pointer (using the least significant bit, which is always 0 for aligned pointers). This saves memory (no extra sentinel node) while still avoiding null checks.

Memory Layout and Cache Performance

An RB tree node typically occupies 32-40 bytes on a 64-bit system:

// Typical RB tree node memory layout (64-bit system):
key: 8 bytes (int64 or pointer to key)
value: 8 bytes (pointer to value data)
left: 8 bytes (pointer to left child)
right: 8 bytes (pointer to right child)
parent: 8 bytes (pointer to parent — often stores color in LSB)
// Total: 40 bytes per node (no padding needed)
// Cache line: 64 bytes → 1.6 nodes per cache line
// Traversing the tree: ~1 cache miss per level

// Compare with B-tree node (order 64):
// ~64 keys per node → ~512 bytes → 8 cache lines
// But binary search within a node hits the same cache lines
// Net: ~1 cache miss per 6 levels of binary search
// This is why B-trees are faster for large datasets

For small datasets (under ~10,000 keys), cache performance is irrelevant — everything fits in L2 cache. For large datasets (millions of keys), the cache performance gap between RB trees and B-trees becomes significant. This is why databases and file systems prefer B-trees, while language standard libraries (std::map, TreeMap) use RB trees — they optimize for the common case of smaller collections.

python
# Simplified CFS scheduler model
class CFSScheduler:
    def __init__(self):
        self.rbtree = RedBlackTree()   # keyed by vruntime
        self.min_vruntime = 0         # floor for new/woken processes

    def enqueue(self, process):
        # New/woken process: set vruntime to at least min_vruntime
        # (prevents a sleeping process from monopolizing CPU on wake)
        process.vruntime = max(process.vruntime, self.min_vruntime)
        self.rbtree.insert(process)    # O(log n)

    def pick_next(self):
        return self.rbtree.minimum()   # O(1) — cached leftmost pointer

    def update_after_run(self, process, time_slice):
        self.rbtree.delete(process)    # O(log n)
        process.vruntime += time_slice / process.weight  # weighted increase
        self.rbtree.insert(process)    # O(log n)
        self.min_vruntime = self.rbtree.minimum().vruntime

    def dequeue(self, process):
        # Process blocks (e.g., waiting on I/O)
        self.rbtree.delete(process)    # O(log n) — why we can't use a heap
CFS Scheduler Simulation

Add processes, then click "Step" to simulate scheduling. The leftmost node (lowest vruntime, highlighted) is always chosen next. Watch vruntimes converge.

Click "Add Process" a few times, then "Step" to simulate scheduling.
Concept check: Why does the Linux CFS scheduler use a red-black tree instead of a min-heap for tracking runnable processes?

Chapter 8: Interview Arsenal

You will rarely be asked to implement a red-black tree from scratch in an interview. But you will be asked to explain the guarantees, compare balanced BSTs, and solve problems that use them as building blocks. The interviewers want to see that you understand when and why to use a balanced BST, not that you have memorized the deletion fix-up code.

The Five-Dimension Cheat Sheet

DimensionWhat they askYour answer framework
Concept"What guarantees does an RB tree provide?"O(log n) worst-case search/insert/delete. Height ≤ 2 log(n+1). At most 2 rotations per insert, 3 per delete. 5 coloring properties that ensure no path is more than 2x any other.
Design"Design an ordered key-value store with range queries"Use an RB tree (or B-tree for disk). Keys stored in-order. Range query: find start key (O(log n)), then in-order walk to end key (O(k) for k results). Total: O(log n + k). For augmented queries (e.g., "kth smallest"), augment nodes with subtree sizes.
Code"Validate that a tree is a valid RB tree"Single DFS checking: (1) BST property via min/max bounds, (2) no red-red via parent color check, (3) equal black-heights by returning bh from each subtree, (4) root is black. Return bh or -1 for invalid.
Debug"My RB tree insert produces wrong tree structure"Walk through the insertion case-by-case. Most common bugs: (1) wrong mirror case (swapped left/right), (2) forgetting to recolor root black, (3) not using sentinel NIL (null pointer crash when checking uncle color), (4) wrong rotation direction after Case 2.
Frontier"What are modern alternatives to RB trees?"Left-leaning RB trees (Sedgewick — simpler code, same guarantees). B-trees/B+ trees for disk and cache-friendly access. Concurrent skip lists (ConcurrentSkipListMap in Java). WAVL trees (weak AVL — unified framework). Modern hardware favors cache-oblivious structures.

Coding Drill: Validate an RB Tree

This is the most commonly asked RB tree coding question. Given a tree, verify that all five properties hold. The trick: combine the BST property check with a black-height check in a single DFS that returns the black-height of each subtree.

python
def validate_rb_tree(root):
    """Return True if the tree is a valid red-black tree."""
    if root is None:
        return True
    if root.color != BLACK:          # Property 2: root must be black
        return False

    def check(node, lo, hi):
        """Returns black-height if valid, -1 if invalid."""
        if node is None:       # NIL leaf → black-height 0
            return 0

        # Check BST property
        if node.key <= lo or node.key >= hi:
            return -1

        # Property 4: no red child of red parent
        if node.color == RED:
            if (node.left and node.left.color == RED) or \
               (node.right and node.right.color == RED):
                return -1

        # Recurse
        left_bh = check(node.left, lo, node.key)
        right_bh = check(node.right, node.key, hi)

        # Either subtree invalid?
        if left_bh == -1 or right_bh == -1:
            return -1

        # Property 5: equal black-heights
        if left_bh != right_bh:
            return -1

        # Return this node's contribution to black-height
        return left_bh + (1 if node.color == BLACK else 0)

    return check(root, float('-inf'), float('inf')) != -1

Time complexity: O(n) — we visit every node once. Space complexity: O(h) = O(log n) for the recursion stack.

Coding Drill: kth Smallest Element

Another common question: "Given a BST, find the kth smallest element efficiently." The naive approach does an in-order traversal and stops at the kth element — O(n) time. With an augmented red-black tree, you can do it in O(log n).

The augmentation: store at each node the size of its subtree (number of nodes in the subtree rooted at that node). This field can be maintained during rotations in O(1) — after a rotation, recalculate the sizes of the two affected nodes from their children's sizes.

python
def os_select(T, x, k):
    """Find the node with the kth smallest key (1-indexed)."""
    # r = rank of x within its subtree
    r = x.left.size + 1   # left subtree size + 1 (for x itself)
    if k == r:
        return x               # x is the kth element
    elif k < r:
        return os_select(T, x.left, k)   # kth is in left subtree
    else:
        return os_select(T, x.right, k - r)  # adjust k for right subtree

# Usage: node = os_select(T, T.root, 5)  # 5th smallest element
# Time: O(log n) — we descend at most h levels

This is the order-statistic tree from CLRS Chapter 14 — a red-black tree augmented with subtree sizes. It supports:

OperationTimeHow
OS-SELECT(k)O(log n)Walk down the tree, comparing k to left subtree size
OS-RANK(x)O(log n)Walk up from x to root, accumulating left subtree sizes
Insert / DeleteO(log n)Standard RB operations + update sizes along the path

Coding Drill: Count Keys in Range [a, b]

With an order-statistic tree, counting keys in a range [a, b] is O(log n):

python
def count_in_range(T, a, b):
    """Count keys k where a <= k <= b. O(log n)."""
    rank_b = os_rank(T, b)    # rank of b (or where b would be)
    rank_a = os_rank(T, a)    # rank of a
    return rank_b - rank_a + 1
    # Compare with naive: in-order walk from a to b = O(n) worst case

Talking Points: "Why Red-Black Trees?"

When an interviewer asks "why red-black over AVL / B-tree / skip list?", they want to see that you understand the trade-off space, not just that you know what an RB tree is. Here is the framework for answering:

The answer depends on the workload.
Read-heavy, rarely mutated: AVL tree — shorter height (1.44 log n) means fewer comparisons per lookup.
Frequent inserts and deletes: Red-black tree — at most 3 rotations per delete (AVL can do O(log n)).
Disk-based, large datasets: B-tree or B+ tree — minimizes disk I/O by packing hundreds of keys per node.
Concurrent access: Skip list (easier to make lock-free; used by Java's ConcurrentSkipListMap) or concurrent B-tree.
Default in-memory ordered container: Red-black tree — good all-around performance, worst-case guarantees, standard library support.
Approximate / probabilistic OK: Skip list or treap — simpler to implement, expected O(log n).

Design Question: Interval Scheduling

A classic system-design question: "Design a system that stores time intervals and efficiently finds all intervals that overlap a given query point."

Step 1: Augmented RB Tree
Store intervals [low, high] in an RB tree keyed by low endpoint. Each node also stores the maximum high endpoint in its entire subtree. This "max" field is maintained during rotations in O(1) — just take the max of the node's high endpoint and its children's max fields.
Step 2: Overlap Query
To find an interval overlapping point q: if the current interval overlaps q, report it. If the left subtree's max ≥ q, recurse left (an overlapping interval might be there). Otherwise, recurse right. O(log n) per query to find the first overlap.
Step 3: All Overlaps
To find all k overlapping intervals: modify the query to recurse into both subtrees when they might contain overlaps. Total: O(k log n).

This is CLRS Chapter 14's interval tree — a direct application of augmented red-black trees. The augmentation technique works because rotations only affect O(1) nodes, so the "max" field can be updated in O(1) per rotation.

Design Question: LRU Cache with Ordered Eviction

"Design a cache that evicts the least-recently-used item, but also supports efficient range queries by key." This combines a hash table (for O(1) lookups) with an RB tree (for ordered operations).

Hash Table
Maps key → node. Provides O(1) lookup. Each node stores key, value, and a timestamp (or sequence number) of last access.
RB Tree (by key)
Stores nodes ordered by key. Provides O(log n) range queries: "find all items with keys in [a, b]."
RB Tree (by timestamp)
Stores nodes ordered by last-access time. The minimum (leftmost) is the LRU item. Eviction: delete the leftmost node from both trees and the hash table. O(log n).

Total: O(1) lookup, O(log n) insert/evict/range-query. This is a real pattern used in database buffer pools and CDN caches.

Coding Drill: Find the Closest Key

python
def closest_key(T, target):
    """Find the key in the RB tree closest to target. O(log n)."""
    x = T.root
    best = None
    best_diff = float('inf')
    while x != T.nil:
        diff = abs(x.key - target)
        if diff < best_diff:
            best = x.key
            best_diff = diff
        if target < x.key:
            x = x.left
        elif target > x.key:
            x = x.right
        else:
            return x.key   # exact match
    return best
# This works because BST search narrows down to the region around target.
# The closest key must be on the search path (or its immediate neighbors).

Common Implementation Bugs

BugSymptomFix
Forgetting to recolor root black after fix-upRoot becomes red, violating Property 2. Subsequent inserts crash.Always set T.root.color = BLACK as the last line of insert/delete fix-up.
Wrong mirror case in fix-upTree becomes unbalanced on one side. Some paths have too many black nodes.The else branch must be an exact mirror: swap every left↔right and left_rotate↔right_rotate.
Not using sentinel NILNull pointer exceptions in fix-up when checking uncle's color or sibling's children.Use a single sentinel NIL node (color = BLACK) instead of null. Check colors on T.nil without crashing.
Forgetting to update parent pointers after rotationTree corruption, infinite loops during traversal.Every rotation must update 3 parent pointers: the rotated node, its child, and the grandparent's child pointer.
Not handling x.parent = y in delete when x is T.nilFix-up reads x.parent and gets garbage.In rb_delete, when y.parent == z, explicitly set x.parent = y even if x is T.nil.
RB Tree Validator Quiz

A tree is shown. Is it a valid red-black tree? Examine the colors carefully, then click "Valid" or "Invalid." Click "Next" for a new challenge.

Is this tree a valid RB tree? Check all 5 properties.
Interview check: You are designing an in-memory ordered key-value store that supports range queries and sees 80% writes (inserts/deletes) and 20% reads. Which self-balancing BST do you choose?

Chapter 9: Connections

Red-black trees sit at the center of a web of data structure ideas. Here is where they connect to the rest of the CLRS landscape.

TopicConnectionLink
CLRS Ch 12: BSTsRed-black trees are BSTs with balance enforcement. All BST operations (search, min, max, successor, predecessor, in-order traversal) work identically — the RB layer only adds fix-up after insert/delete.BSTs lesson
CLRS Ch 14: AugmentingRed-black trees can be augmented with extra per-node fields (subtree size for order-statistics trees, max endpoint for interval trees) while maintaining O(log n) guarantees. The key: rotations affect only O(1) nodes.Coming soon
CLRS Ch 18: B-TreesB-trees generalize binary trees to multi-way trees for disk storage. Red-black trees are isomorphic to 2-3-4 trees (B-trees of order 4) — every red node "merges" with its black parent to form a 2-, 3-, or 4-node.Coming soon
CLRS Ch 6: HeapsortHeaps give O(1) find-min but O(n) arbitrary deletion. RB trees give O(log n) for everything including arbitrary deletion. Use heaps when you only need extract-min; use RB trees when you also need delete-by-key.Heapsort lesson
CLRS Ch 11: Hash TablesHash tables give O(1) expected search but no ordering. RB trees give O(log n) search with full ordering (range queries, sorted iteration, kth-element). Use hash tables when you only need point lookups.Hash Tables lesson
The 2-3-4 tree isomorphism. Every red-black tree corresponds to a unique 2-3-4 tree (a B-tree where each internal node holds 1-3 keys and has 2-4 children). The mapping:
• A black node with no red children = a 2-node (1 key, 2 children).
• A black node with one red child = a 3-node (2 keys, 3 children). The red child merges into the black parent.
• A black node with two red children = a 4-node (3 keys, 4 children). Both red children merge in.

This is not just a curiosity — it is the reason red-black trees work. The five RB properties are exactly the rules that maintain a valid 2-3-4 tree where all leaves are at the same depth (which guarantees balance). If you ever struggle to remember the RB insertion/deletion cases, think about the corresponding 2-3-4 tree operations: Case 1 (uncle is red) corresponds to splitting a 4-node; Cases 2+3 (uncle is black) correspond to rotating within a 3-node.

The 2-3-4 Mapping: A Worked Example

Let us trace the isomorphism concretely. Consider this red-black tree:

      10(B)
     / \
   5(R)  20(B)
   / \    / \
  3(B) 7(B) 15(R) 25(R)

// Mapping to 2-3-4 tree:
// Node 10(B) has one red child 5(R) on the left → 3-node: [5, 10]
// - Left child of 5: 3(B) → 2-node: [3]
// - Right child of 5: 7(B) → 2-node: [7]
// Node 20(B) has two red children 15(R) and 25(R) → 4-node: [15, 20, 25]

// The 2-3-4 tree:
// [5, 10]
// / | \
// [3] [7] [15, 20, 25]
// All leaves at same depth! This is guaranteed by Property 5.

The RB tree's black nodes form the skeleton of the 2-3-4 tree. Red nodes "merge" into their black parents to create 3-nodes and 4-nodes. Property 5 (equal black-height) ensures all leaves of the 2-3-4 tree are at the same depth, which is the defining property of B-trees.

This isomorphism explains the insertion cases perfectly:

RB Insert Case2-3-4 Tree Operation
Case 1: Uncle is red → recolorSplitting a 4-node (black parent with two red children becomes three 2-nodes, pushing the middle key up)
Cases 2+3: Uncle is black → rotate + recolorAbsorbing a key into a 3-node (rotation restructures the node)

What We Did Not Cover

The Big Picture

Red-black trees solve a fundamental problem in computer science: how to maintain a sorted dynamic collection with guaranteed O(log n) worst-case operations. The coloring rules are a clever encoding of balance constraints that allow local fixes (at most 3 rotations) to restore global balance after any mutation. The price is implementation complexity — the deletion fix-up has many cases and is notoriously error-prone. But the payoff is enormous: deterministic worst-case performance that has made red-black trees the backbone of systems software for over 40 years.

Performance in the Real World: Benchmarks

How do balanced BSTs compare in actual CPU time (not just asymptotic complexity)?

OperationRB TreeAVL TreeSkip ListB-Tree (order 64)Hash Table
Search (1M keys)~400ns~350ns~600ns~200ns~50ns
Insert~500ns~550ns~700ns~300ns~80ns
Delete~500ns~600ns~700ns~350ns~70ns
Range query (100 keys)~4μs~3.5μs~6μs~2μsN/A

Approximate timings on modern x86-64 CPU. B-trees win for cache-friendly access. Hash tables win for unordered point queries. RB trees are the best general-purpose ordered structure.

The B-tree's advantage comes from cache efficiency: each node holds dozens of keys packed into a single cache line (64 bytes). An RB tree node holds one key plus three pointers (~32 bytes with alignment), and each level is likely a separate cache miss. For large trees that do not fit in L2 cache, B-trees can be 2-3x faster than RB trees despite having the same asymptotic complexity.

This is why modern databases use B-trees (or B+ trees) for disk and cache-optimized indexes, and RB trees for smaller in-memory ordered collections where the implementation simplicity and worst-case rotation bounds matter more than cache performance.

The Enduring Legacy

Red-black trees were invented by Rudolf Bayer in 1972 (as "symmetric binary B-trees") and refined by Leonidas Guibas and Robert Sedgewick in 1978 (who introduced the red-black coloring). Over 45 years later, they remain the dominant balanced BST in production software.

The reason: they hit a sweet spot of simplicity, performance, and worst-case guarantees that no other single structure has surpassed for general-purpose in-memory ordered collections. AVL trees have better lookups but worse deletions. B-trees have better cache behavior but more complex code. Skip lists are simpler but probabilistic. Splay trees adapt to access patterns but have O(n) worst case. Hash tables are faster but unordered. Red-black trees are the only structure that provides worst-case O(log n) for all operations with bounded rotations, ordered iteration, and reasonable constant factors.

A Timeline of Balanced BSTs

YearStructureInventorKey Innovation
1962AVL treeAdelson-Velsky, LandisFirst self-balancing BST; strict height balance
19702-3 treeHopcroftMulti-way balanced tree; all leaves at same depth
1972B-treeBayer, McCreightGeneralized multi-way tree; optimal for disk I/O
1972Symmetric binary B-treeBayerBinary representation of 2-3-4 trees (precursor to RB trees)
1978Red-black treeGuibas, SedgewickColor-based balance encoding; O(1) amortized rotations
1985Splay treeSleator, TarjanSelf-adjusting; amortized O(log n) without stored balance info
1989TreapAragon, SeidelRandomized BST+heap hybrid; expected O(log n)
2008Left-leaning RB treeSedgewickSimplified RB variant with fewer cases
2015WAVL treeHaeupler, Sen, TarjanUnified AVL+RB framework; at most 2 rotations for both insert and delete

This timeline reveals a pattern: the field moves toward simpler implementations (left-leaning RB) and unified frameworks (WAVL) while the fundamental ideas — rotations, coloring/ranking, amortized analysis — remain unchanged. Red-black trees are not just an algorithm to memorize; they are a window into how computer scientists think about maintaining order in the face of constant change.

"The design of the red-black tree is a triumph of algorithm engineering: five simple rules that yield O(log n) worst-case performance with O(1) rotations per mutation." — adapted from Introduction to Algorithms (CLRS)
Final check: A red-black tree with n internal nodes is isomorphic to which other data structure?