One colour per node keeps a binary tree nearly balanced. Watch which insertions recolour and which rotate.
Red and black nodes keep balance
Eight keys inserted into a red–black tree
static void fix_after_insert(struct rb *z){ while (z->parent != NULL && z->parent->colour == RED) { struct rb *p = z->parent; struct rb *g = p->parent; if (p == g->left) { struct rb *u = g->right; if (u != NULL && u->colour == RED) { p->colour = BLACK; u->colour = BLACK; g->colour = RED; recolourings++; z = g; } else { if (z == p->right) { rotate_left(p); z = p; p = z->parent; } p->colour = BLACK; g->colour = RED; rotate_right(g); } } else { struct rb *u = g->left; if (u != NULL && u->colour == RED) { p->colour = BLACK; u->colour = BLACK; g->colour = RED; recolourings++; z = g; } else { if (z == p->left) { rotate_right(p); z = p; p = z->parent; } p->colour = BLACK; g->colour = RED; rotate_left(g); } } } root->colour = BLACK;} for (int i = 0; i < 8; i++) rb_insert(keys[i]); preorder(root);main has started.
- height
- 0
- rotations
- 0
- recolourings
- 0
Describe this picture
A picture across the frame, with the code under it. The tree is drawn from the top, each node a box with its key and two dots for its left and right pointers. Red nodes are filled in the accent colour; black nodes are plain boxes. A ring marks the node the program is working on. The code pane shows fix_after_insert and main’s loop, with the line about to run marked. The clip inserts 10, 20, 30, 15, 25, 5, 1 and 27, about 27 seconds. 10 arrives red and turns black as the root. 20 arrives red under it. 30 arrives red under 20, red under red: 10 turns red and 20 black, and a left rotation lifts 20 to the root. 15 arrives under 10, red under red again, but its uncle 30 is red, so the colours change instead: 10 and 30 black, 20 red, then 20 black again as the root. 25 and 5 arrive under black parents and nothing more happens. 1 arrives under 5 and its uncle 15 is red: 5 and 15 turn black and 10 red. 27 arrives under 25, which is red with no sibling: 25 rotates left, 27 turns black and 30 red, and 30 rotates right, leaving 27 over 25 and 30. Under the picture are one sentence on the current step and the readouts “height”, “rotations” and “recolourings”, which end at 3, 3 and 2. The control, “View”, has the positions “red–black” and “2-3-4”; 2-3-4 replays the same steps with each red node’s key drawn inside its black parent’s box, ending with 10 20 at the root over 1 5, 15 and 25 27 30. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as rb.step, and the view as rb.view.
Glued to their parents
AVL trees store a height in every node. A red–black tree stores one bit instead, a colour, and keeps four rules:
- Every node is red or black.
- The root is black.
- A red node never has a red child.
- Every path from a node down to an empty child passes the same number of black nodes.
The rules mean more than they seem to. Think of a red node as a key glued to its parent, part of the same node. Then a black node with no red children is a node with one key; with one red child, a node with two keys; with two red children, three keys. Rule 3 says the glue goes only one level. Rule 4 says every path meets the same number of these combined nodes: all leaves are level. That is a 2-3-4 tree, a 2-3 tree that also allows nodes with three keys, drawn with binary nodes.
Set “View” to 2-3-4 and watch the same insertions with each red key pulled up into its parent. The final tree is 10 20 at the root over 1 5, 15 and 25 27 30, every leaf one level down.
Key idea
The colours are not decoration. A red node belongs with its parent; the black nodes are the 2-3-4 tree’s nodes, and rule 4 says its leaves are all level.
The code and its cost
The node
The full program, rb.c, gives each node a colour and a pointer to its parent, so that the fix-up can climb:
enum colour { RED, BLACK };
struct rb {
int key;
enum colour colour;
struct rb *left, *right, *parent;
};
The root, root, and the two counters are static variables outside any function. Each node is 32 bytes: the key and the colour (an enum, 4 bytes here) then three pointers.
Inserting
rb_insert inserts as in Binary search trees: walk down from the root, hang the new node where the walk falls off, and set its parent. The new node is red, because a red node adds no black node to any path, so rule 4 still holds. Only rule 3 can break: if the new node’s parent is red too. fix_after_insert repairs that.
The fix-up
while (z->parent != NULL && z->parent->colour == RED) {
struct rb *p = z->parent;
struct rb *g = p->parent;
if (p == g->left) {
struct rb *u = g->right;
if (u != NULL && u->colour == RED) {
p->colour = BLACK;
u->colour = BLACK;
g->colour = RED;
recolourings++;
z = g;
} else {
if (z == p->right) {
rotate_left(p);
z = p;
p = z->parent;
}
p->colour = BLACK;
g->colour = RED;
rotate_right(g);
}
} else {
/* the mirror image, left and right swapped */
}
}
root->colour = BLACK;
z is the red node, p its red parent, g the grandparent (black, since the tree was valid before), and u the uncle, g’s other child. Two cases:
- The uncle is red. In 2-3-4 terms,
gwith its two red children was a full node, andzwould make a fourth key. Recolouring splits it:panduturn black andgturns red, which pushesgup into the node above. That may make red under red one level higher, so the loop goes on withz = g, two levels up. No pointer changes. - The uncle is black or empty.
g’s node had room. One rotation atg, withpturned black andgred, makespthe top of the three. Ifzis on the inside (a right child under a left child, or the mirror), a first rotation atpturns it into the outside case. Either way the loop ends.
At the end the root is made black: a red root only adds one black node to every path.
rotate_left and rotate_right are the rotations of AVL trees, with the parent pointers kept up to date as well.
What it costs
Rule 4 gives every path the same number of black nodes, and rule 3 lets at most one red node follow each black one, so the longest path from the root is at most twice the shortest. From that, a red–black tree with n nodes has height below 2 log₂(n + 1). Search, insertion and removal each cost a number of steps proportional to log n. An insertion makes at most two rotations; recolouring may climb, two levels at a time, towards the root.
Against AVL trees, the balance is looser: up to twice the shortest path, not about 1.44 times log₂ n. In exchange, an insertion or a removal changes fewer pointers, which is why many libraries choose red–black trees for maps that change often. Removal follows the same idea, with more cases, and is not shown here.
Running it yourself
gcc -std=c11 -Wall -Wextra -pedantic rb.c -o rb
./rb
It prints the tree in preorder with each colour, 20B 10R 5B 1R 15B 27B 25R 30R, then 3 rotations, 2 recolourings.
Worked example
Insert 3 into the final tree. The walk goes left of 20, left of 10, left of 5 and right of 1: 3 hangs as 1’s right child, red under red. Its uncle, 5’s right child, is empty, and 3 is on the inside (a right child of a left child). So: rotate 1 left, which lifts 3 above 1; then 3 turns black, 5 turns red, and 5 rotates right, which lifts 3 above 5. 3 is black over 1 and 5, both red. Two more rotations, and the preorder becomes 20B 10R 3B 1R 5R 15B 27B 25R 30R. Every path still passes 2 black nodes.
The height bound. For n = 8, 2 log₂ 9 ≈ 6.3, so no red–black tree of 8 nodes is taller than 6. The clip’s tree has height 3.
The 2-3-4 reading. A black node with two red children is a node with three keys, a 4-node: the final tree’s 27 with 25 and 30.
Two checks. Can a red–black tree have a red root? (No: rule 2.) What is the black height of the final tree, counting the root? (2: 20 and 5 on the way to 1, 20 and 27 on the way to 25.)
Where it’s used
Ordered maps and sets. Many standard libraries implement their ordered maps and sets as red–black trees.
Schedulers. CFS, Linux’s scheduler until version 6.6, kept runnable tasks in a red–black tree ordered by the processor time each had used, and always ran the leftmost.
B-trees. The 2-3-4 reading points the other way too: B-trees keep many keys in a node on purpose, sized to a disk block.
Reference card
| Code or idea | Meaning |
|---|---|
| the rules | root black; no red under red; equal black count on every path down |
| red node | a key glued to its black parent: the 2-3-4 tree’s node |
| new node | red, so the black counts stay equal |
| red uncle | recolour p, u black and g red, then look at g |
| black uncle, outside | recolour, then rotate g |
| black uncle, inside | rotate p first, then as outside |
root->colour = BLACK | the last line of every insertion |
| height | below 2 log₂(n + 1) |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.