Order
Keys remain organized like any BST; in-order traversal is increasing.
Algorithms · Balanced data structures
A red-black tree is a binary search tree that limits imbalance through colors, recoloring and rotations. It is not perfectly balanced, but it guarantees logarithmic height.
A BST built from sorted keys can degenerate into a chain of height n. A red-black tree preserves the BST property and constrains colors along paths: no path can become more than twice as long as another.
Keys remain organized like any BST; in-order traversal is increasing.
Every node adds one logical bit: red or black.
After an update, recoloring and a constant number of rotations are enough.
Every node is either red or black.
The root is black.
Every NIL leaf—that is, every null pointer—is black.
A red node has black parent and children: two red nodes can never be consecutive.
Every path from a node to a descendant NIL contains the same number of black nodes: its black height.
Every new key enters red. Step through the trace to see which violation appears and how recoloring and rotations move or remove it.
BST-INSERT(T, z)
z.color ← REDwhile color(parent(z)) = RED if color(uncle(z)) = RED
color(parent(z)) ← BLACK
color(uncle(z)) ← BLACK
color(grandparent(z)) ← RED
z ← grandparent(z) else if z is an inner child
z ← parent(z)
rotate z toward the line configuration color(parent(z)) ← BLACK
color(grandparent(z)) ← RED rotate grandparent(z) to promote parent(z)T.root.color ← BLACKLet z be the current node, p its parent, g its grandparent and u its uncle, the other child of g. Enter the loop only when p is red; g exists and is black. First assume p = g.left. A NIL pointer counts as a black uncle.
In the diagrams B = black, R = red; black NIL leaves are omitted. Intermediate states may temporarily violate invariants.
Condition: p and u are red. Make p and u black and g red, then assign z ← g. No rotation is needed.
20B 20R ← z
/ \ / \
10R 30R → 10B 30B
/ /
5R ← z 5R
Why it works: each path through g loses g’s black contribution but gains that of p or u, preserving its black count. The original red pair disappears; a new one may arise between g and its parent. The loop continues higher up. If g is the root, the final black recoloring completes the repair.
Condition: u is black, z is p’s right child, and p is g’s left child: a left–right configuration. Assign z ← p and rotate left at z. This rotation does not change colors.
30B 30B
/ /
10R → 20R
\ /
20R ← z 10R ← z
Result: z is now a left child of a left parent. The red–red pair still exists but is aligned: execute case 3 immediately, in the same iteration. After rotation, reread the parent and grandparent of the new z.
Condition: u is black and z is p’s left child, with p itself g’s left child. Make p black and g red, then rotate right at g. The parent becomes the subtree root.
30B 20B
/ / \
20R → 10R 30R
/
10R ← z
Why it terminates: z’s new parent is black, in-order ordering is unchanged, and the subtrees preserve their black contribution to the rest of the tree. No violation propagates to the subtree’s parent. The loop terminates.
| Case | Condition | Operation |
|---|---|---|
| 1 | Red left uncle | Same recolorings; z ← g |
| 2 | Black uncle, z is p’s left child | z ← p; RIGHT-ROTATE(T, z) |
| 3 | Black uncle, z is p’s right child | p.color ← BLACK; g.color ← RED; LEFT-ROTATE(T, g) |
Remember: case 1 can repeat O(log n) times; case 2 leads to case 3, which terminates. One insertion performs at most two rotations.
Use z.p for the parent. Sentinel NIL is black and the root’s parent is NIL. BST-INSERT attaches a new node and sets its parent; it returns NIL if the key already exists. Rotations also update parents and the root. Duplicate keys are ignored, as in the lab.
RB-INSERT(T, key)
z ← BST-INSERT(T, key)
if z = NIL
return // duplicate key
z.left ← NIL
z.right ← NIL
z.color ← RED
RB-INSERT-FIXUP(T, z)
RB-INSERT-FIXUP(T, z)
while z.p.color = RED
if z.p = z.p.p.left
u ← z.p.p.right
if u.color = RED // case 1
z.p.color ← BLACK
u.color ← BLACK
z.p.p.color ← RED
z ← z.p.p
else
if z = z.p.right // case 2
z ← z.p
LEFT-ROTATE(T, z)
z.p.color ← BLACK // case 3
z.p.p.color ← RED
RIGHT-ROTATE(T, z.p.p)
else
u ← z.p.p.left
if u.color = RED // mirror 1
z.p.color ← BLACK
u.color ← BLACK
z.p.p.color ← RED
z ← z.p.p
else
if z = z.p.left // mirror 2
z ← z.p
RIGHT-ROTATE(T, z)
z.p.color ← BLACK // mirror 3
z.p.p.color ← RED
LEFT-ROTATE(T, z.p.p)
T.root.color ← BLACK
Define bh(x) as the number of black nodes from x to a descendant NIL, excluding x and including NIL; bh(NIL) = 0. By induction, a subtree rooted at x contains at least 2bh(x) − 1 internal nodes: each child has black height at least bh(x) − 1.
Let h be the number of edges on the longest root-to-NIL path, equivalently the number of internal nodes on that path. With no consecutive reds, at least half the nodes after the root are black: bh(root) ≥ h/2. Hence n ≥ 2h/2 − 1 and:
The bound uses an inequality: bh(root) is not always equal to h/2. The lab’s black counter includes the root: for a nonempty valid tree, it displays bh(root) + 1.
| Operation | Worst-case time | Note |
|---|---|---|
| Search | Θ(log n) | Follows one path. |
| Insertion | Θ(log n) | Search + fix-up; at most 2 rotations. |
| Deletion | Θ(log n) | Search + fix-up; at most 3 rotations. |
| Space per node | Θ(1) | Color and pointers. |
Distinguish z, whose key we want to delete, from y, the node physically detached. This distinction is essential when z has two children.
Example: root 20B with children 10R and 30R. Deleting key 20 copies 30 into the root and detaches red node 30: no black is lost, even though the requested key was in a black node.
Double black is a bookkeeping device, not a third stored color. If x is the root, discard the extra black: all paths lost the same contribution. If x is red, absorb the extra black by making it black.
RB-DELETE(T, z)
if z.left = NIL or z.right = NIL
y ← z
else
y ← TREE-MINIMUM(z.right)
removedColor ← y.color
if y.left ≠ NIL
x ← y.left
else
x ← y.right
x.p ← y.p // also when x = NIL
if y.p = NIL
T.root ← x
else if y = y.p.left
y.p.left ← x
else
y.p.right ← x
if y ≠ z
z.key ← y.key
z.data ← y.data // keep z.color unchanged
if removedColor = BLACK
RB-DELETE-FIXUP(T, x)
return y // detached node
The procedure receives an existing node z; search first if starting from a key. The shared sentinel’s left and right pointers refer to itself. During deletion, temporarily set NIL.p to the parent of the vacated position so fix-up can read it. An implementation using null must pass the parent separately.
Assume x is a left child. Let p be its parent and w = p.right its sibling. The near nephew is w.left and the far nephew is w.right. The notes label these A (x), B (p), D (w), C (near), E (far). NIL children are black.
p
/ \
x (+B) w
/ \
near far
Condition: w is red, so p and w’s children are black. Make w black and p red, rotate left at p, then recompute w ← x.p.right.
Effect: x keeps its extra black and the same parent, but its new sibling is black. Case 1 prepares case 2, 3 or 4; it is not terminal and does not necessarily imply case 3.
Action: make w red and assign x ← p. Removing one black from the sibling’s side equalizes the two sides; the deficit moves to the parent.
Result: if the new x is red, the final recoloring makes it black and finishes. If it is black and not the root, continue higher up. This is the only case that can repeat O(log n) times; it uses no rotations.
Condition: w is black, w.left red and w.right black. Make w.left black and w red; rotate right at w and recompute x’s sibling.
Effect: the new sibling is black and the new far nephew is red. x retains its deficit, but the configuration is ready for case 4, which runs immediately.
Condition: w is black and w.right red; the near nephew may have either color. Set w.color ← p.color, p.color ← BLACK and w.right.color ← BLACK. Rotate left at p and finish by setting x ← T.root.
Why it works: x’s side gains the missing black contribution; the opposite side is compensated by recoloring the far nephew. The new local root keeps p’s old color, so the repair preserves the black count seen by ancestors.
If x is a right child: swap left and right throughout. The sibling is p.left, the near nephew w.right and the far nephew w.left; left rotations become right rotations and vice versa. Overall: at most three rotations (cases 1, 3 and 4), plus any upward steps in case 2.
20B 20B 20B 30B
/ \ / \ / \ / \
10B 40B → NIL+B 40B → NIL+B 30B → 20B 40B
/ / \
30R 30R 40R
DELETE 10 CASE 3 CASE 4
After detaching 10B, x is 20’s left NIL: w = 40B, near = 30R, far = NIL. Case 3 rotates at 40 and makes 30 the new sibling; case 4 rotates at 20, producing root 30B with children 20B and 40B. Every path again has the same black count.
RB-DELETE-FIXUP(T, x)
while x ≠ T.root and x.color = BLACK
if x = x.p.left
w ← x.p.right
if w.color = RED // case 1
w.color ← BLACK
x.p.color ← RED
LEFT-ROTATE(T, x.p)
w ← x.p.right
if w.left.color = BLACK and w.right.color = BLACK
w.color ← RED // case 2
x ← x.p
else
if w.right.color = BLACK // case 3
w.left.color ← BLACK
w.color ← RED
RIGHT-ROTATE(T, w)
w ← x.p.right
w.color ← x.p.color // case 4
x.p.color ← BLACK
w.right.color ← BLACK
LEFT-ROTATE(T, x.p)
x ← T.root
else
w ← x.p.left
if w.color = RED // mirror 1
w.color ← BLACK
x.p.color ← RED
RIGHT-ROTATE(T, x.p)
w ← x.p.left
if w.right.color = BLACK and w.left.color = BLACK
w.color ← RED // mirror 2
x ← x.p
else
if w.left.color = BLACK // mirror 3
w.right.color ← BLACK
w.color ← RED
LEFT-ROTATE(T, w)
w ← x.p.left
w.color ← x.p.color // mirror 4
x.p.color ← BLACK
w.left.color ← BLACK
RIGHT-ROTATE(T, x.p)
x ← T.root
x.color ← BLACK
Recompute w after the rotations in cases 1 and 3. The following tests are not all joined by “else if”: multiple cases may run in one iteration. In a valid state carrying a deficit, sibling w is not NIL; its children may be NIL.
The lab above visualizes insertion; the diagrams and pseudocode in this section describe deletion.
Start with an empty tree and insert keys in array order, running fix-up after every insertion. The array does not need to be sorted first. The tree remains red-black after every prefix of the sequence, so the next insertion already benefits from logarithmic height.
RB-BUILD(A)
T ← EMPTY-RB-TREE() // root = NIL, NIL.color = BLACK
for key in A
RB-INSERT(T, key) // includes fix-up after each insertion
return T
An empty array produces root NIL. For duplicates, use set semantics: an existing key does not create another node. A multiset can store an occurrence counter in each node.
| Key | Initial position | Repair | Local result |
|---|---|---|---|
| 41 | Root | Root to black | 41B |
| 38 | Left of 41B | Black parent: no case | 38 stays red |
| 31 | Left of 38R | Case 3; right rotation at 41 | Root 38B, children 31R and 41R |
| 12 | Left of 31R | Case 1: uncle 41R; then black root | 31B, 41B, 12R |
| 19 | Right of 12R | Case 2 at 12, then case 3 at 31 | 19B with children 12R and 31R |
| 8 | Left of 12R | Case 1: uncle 31R; 19 moves up below 38B | 19R, 12B, 31B, 8R |
38B
/ \
19R 41B
/ \
12B 31B
/
8R
Check: in-order is [8, 12, 19, 31, 38, 41]; no red node has a red child. Every root-to-NIL path contains three black nodes including the root and NIL, so bh(root) = 2 under the convention excluding the root. Changing insertion order may produce a different, still valid shape.
For n distinct keys, insertion i costs O(log(i + 1)). Summing gives O(n log n) time and Θ(n) space for the tree. The upper bound is optimal in the worst case for arbitrary keys in the comparison model.
Lower-bound proof: if we could build a BST containing n arbitrary distinct keys in o(n log n), an in-order traversal in Θ(n) would return them sorted. That would beat the Ω(n log n) lower bound for comparison sorting, a contradiction. Construction therefore takes Θ(n log n) in the worst case.
Splitting an array by position does not separate values: one half may contain [9, 1] and the other [4, 7]. The two trees cannot be joined as though every key in the first were smaller than every key in the second. Simple join operations require that precondition.
One general union extracts the two in-order sequences, merges them and rebuilds a valid RB tree, costing linear time in the total number of keys. In a divide-and-conquer construction this gives T(n) = 2T(n/2) + Θ(n) = Θ(n log n), since each level costs Θ(n). Base cases must include both empty and single-element intervals; without the latter, a call on the same half never terminates.
If the array is already sorted: direct Θ(n) constructions exist using medians and a consistent level coloring. Making every node black works only if all paths to NIL have the same length; a generic balanced BST is insufficient. Starting from unsorted input, comparison sorting still costs Θ(n log n) in the worst case.
10 becomes a black root, 30 its red right child. Inserting 20 forms a right–left triangle with a black NIL uncle: mirror case 2, rotate right at 30; then mirror case 3, recolor and rotate left at 10. Result: root 20B, children 10R and 30R.
x is the left NIL; w = 30B has two black NIL children. Case 2: 30 becomes red and x moves up to 20. x is the root, so discard the extra black. Result: 20B with right child 30R.
No. y = 20B, x = 10R. The child replaces the root and is colored black; the fix-up loop does not run.
O(n) construction plus Θ(n) in-order traversal would yield O(n) sorting. In the comparison model, worst-case cost is at least Ω(n log n). The argument requires arbitrary keys; integer algorithms under additional domain assumptions can use operations other than comparisons.
Topics follow Algoritmi, lectures 16–18 (pp. 53–67): properties, rotations, insertion, deletion and construction. Further reading: Dartmouth · Red-black trees; Princeton · Sorting and searching.