Overview
A red–black tree (RBT) is a binary search tree (BST) augmented with a color bit per node and a set of local invariants that globally bound the tree’s height by O(log n). Because search, insert, and delete all maintain these invariants with at most a constant number of rotations and recolorings, RBTs are a practical default for ordered maps/sets, and they power many standard libraries.
Key promise: the longest root–leaf path is at most twice as long as the shortest, producing Θ(log n) worst-case time for lookup and updates, while keeping implementation complexity moderate compared to other balanced BSTs.
Note
Notation: nodes have
key,left,right,parent, andcolor ∈ {RED, BLACK}. External nulls (leaves) are treated as black NIL sentinels.
Structure Definition
A red–black tree is a BST over keys with these properties:
-
Every node is either RED or BLACK.
-
The root is BLACK.
-
Every NIL leaf is BLACK.
-
No RED node has a RED child (no two reds in a row).
-
Equal black-height: every path from a node to any descendant NIL has the same number of BLACK nodes.
The black-height bh(x) of a node x is the number of BLACK nodes on any path from x down to, but not including, a NIL leaf; property (5) makes bh well-defined.
graph TD N26["26"]:::black --> N17["17"]:::red N26 --> N41["41"]:::black N17 --> N14["14"]:::black N17 --> N21["21"]:::black N41 --> N30["30"]:::red N41 --> N47["47"]:::red classDef black fill:#1a1a2e,stroke:#aaa,color:#fff,font-weight:bold classDef red fill:#c62828,stroke:#aaa,color:#fff,font-weight:bold linkStyle 0,2 stroke:#f9a825,stroke-width:3px linkStyle 1,4 stroke:#4fc3f7,stroke-width:3px
Yellow path 26(B)→17(R)→14(B)→NIL: bh = 2. Blue path 26(B)→41(B)→30(R)→NIL: bh = 2. Both paths share the same black-height despite different lengths — property (5) in action.
Core Operations
Rotations are the only shape-changing primitives; colors change only via recoloring.
Rotation primitives (0-indexed pointers)
function LEFT_ROTATE(T, x):
y = x.right
x.right = y.left
if y.left ≠ NIL: y.left.parent = x
y.parent = x.parent
if x.parent == NIL: T.root = y
else if x == x.parent.left: x.parent.left = y
else: x.parent.right = y
y.left = x
x.parent = y
function RIGHT_ROTATE(T, y):
x = y.left
y.left = x.right
if x.right ≠ NIL: x.right.parent = y
x.parent = y.parent
if y.parent == NIL: T.root = x
else if y == y.parent.right: y.parent.right = x
else: y.parent.left = x
x.right = y
y.parent = xInsertion (BST insert + fixup)
-
Insert a new node
zas in a BST (respect ordering), withz.color = REDandz.left = z.right = NIL. -
Fixup while a red–red violation occurs (i.e.,
zhas a red parent):-
Let
p = parent(z),g = grandparent(z), andu = uncle(z)(the other child ofg). -
Case 1 -
uis RED (recoloring): setp.color = BLACK,u.color = BLACK,g.color = RED; then continue withz = g. -
Case 2/3 -
uis BLACK (rotation + recolor): rotate to makezandpaligned (a triangle becomes a line), then rotate aroundgand recolorpblack andgred.
-
-
Finish by coloring the root BLACK.
function RB_INSERT(T, z):
// standard BST insert
y = NIL; x = T.root
while x ≠ NIL:
y = x
if z.key < x.key: x = x.left else: x = x.right
z.parent = y
if y == NIL: T.root = z
else if z.key < y.key: y.left = z else: y.right = z
z.left = z.right = NIL
z.color = RED
// fixup
while z.parent.color == RED:
if z.parent == z.parent.parent.left:
y = z.parent.parent.right // uncle
if y.color == RED: // Case 1
z.parent.color = BLACK
y.color = BLACK
z.parent.parent.color = RED
z = z.parent.parent
else: // y is BLACK
if z == z.parent.right: // Case 2 (triangle)
z = z.parent
LEFT_ROTATE(T, z)
z.parent.color = BLACK // Case 3 (line)
z.parent.parent.color = RED
RIGHT_ROTATE(T, z.parent.parent)
else: // mirror cases
y = z.parent.parent.left
if y.color == RED:
z.parent.color = BLACK
y.color = BLACK
z.parent.parent.color = RED
z = z.parent.parent
else:
if z == z.parent.left:
z = z.parent
RIGHT_ROTATE(T, z)
z.parent.color = BLACK
z.parent.parent.color = RED
LEFT_ROTATE(T, z.parent.parent)
T.root.color = BLACKCase 1 — Uncle is RED (recolor only):
graph LR subgraph before1 [" Before "] direction TB G1["g (B)"]:::black --> P1["p (R)"]:::red G1 --> U1["u (R)"]:::red P1 --> Z1["z (R)"]:::red P1 --> N1[" "]:::nil end before1 -. "recolor: p,u→B g→R" .-> after1 subgraph after1 [" After "] direction TB G2["g (R) ← new z"]:::red --> P2["p (B)"]:::black G2 --> U2["u (B)"]:::black P2 --> Z2["z (R)"]:::red P2 --> N2[" "]:::nil end classDef black fill:#1a1a2e,stroke:#aaa,color:#fff,font-weight:bold classDef red fill:#c62828,stroke:#aaa,color:#fff,font-weight:bold classDef nil fill:none,stroke:none,color:transparent
No rotations — push the violation upward by recoloring. Continue fixup at g.
Case 2 — Triangle → rotate to line:
graph LR subgraph before2 [" Before (triangle) "] direction TB G3["g (B)"]:::black --> P3["p (R)"]:::red G3 --> U3["u (B)"]:::black P3 --> N3[" "]:::nil P3 --> Z3["z (R)"]:::red end before2 -. "LEFT_ROTATE(p)" .-> after2 subgraph after2 [" After (line) "] direction TB G4["g (B)"]:::black --> Z4["z (R)"]:::red G4 --> U4["u (B)"]:::black Z4 --> P4["p (R)"]:::red Z4 --> N4[" "]:::nil end classDef black fill:#1a1a2e,stroke:#aaa,color:#fff,font-weight:bold classDef red fill:#c62828,stroke:#aaa,color:#fff,font-weight:bold classDef nil fill:none,stroke:none,color:transparent
Triangle (z is right child of p, p is left child of g) becomes a line via one rotation. Falls through to Case 3.
Case 3 — Line → rotate at grandparent + recolor:
graph LR subgraph before3 [" Before (line) "] direction TB G5["g (B)"]:::black --> P5["p (R)"]:::red G5 --> U5["u (B)"]:::black P5 --> Z5["z (R)"]:::red P5 --> N5[" "]:::nil end before3 -. "RIGHT_ROTATE(g), recolor" .-> after3 subgraph after3 [" After "] direction TB P6["p (B)"]:::black --> Z6["z (R)"]:::red P6 --> G6["g (R)"]:::red G6 --> N6[" "]:::nil G6 --> U6["u (B)"]:::black end classDef black fill:#1a1a2e,stroke:#aaa,color:#fff,font-weight:bold classDef red fill:#c62828,stroke:#aaa,color:#fff,font-weight:bold classDef nil fill:none,stroke:none,color:transparent
Rotate at grandparent, swap p/g colors. Red-red violation resolved — p is now black root of the subtree.
Deletion (BST delete + fixup)
Deletion removes a node and can violate black-height or create a “double black” at the position replacing the removed black node. Standard approach:
-
Find node
zwith the key; if both children non-NIL, swap with its successory(which has at most one non-NIL child). -
Splice out
y; letxbey’s single child (possibly NIL). Tracky_original_color. -
If
y_original_color == BLACK, callRB_DELETE_FIXUP(T, x)to discharge the “double black” atx.
Fixup uses sibling colors and nephew configurations:
function RB_DELETE(T, z):
y = z
y_original_color = y.color
if z.left == NIL:
x = z.right
TRANSPLANT(T, z, z.right)
else if z.right == NIL:
x = z.left
TRANSPLANT(T, z, z.left)
else:
y = TREE_MINIMUM(z.right) // successor
y_original_color = y.color
x = y.right
if y.parent == z:
x.parent = y
else:
TRANSPLANT(T, y, y.right)
y.right = z.right
y.right.parent = y
TRANSPLANT(T, z, y)
y.left = z.left
y.left.parent = y
y.color = z.color
if y_original_color == BLACK:
RB_DELETE_FIXUP(T, x)
function 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: sibling red
w.color = BLACK
x.parent.color = RED
LEFT_ROTATE(T, x.parent)
w = x.parent.right
if w.left.color == BLACK and w.right.color == BLACK: // Case 2
w.color = RED
x = x.parent
else:
if w.right.color == BLACK: // Case 3: far nephew black, near red
w.left.color = BLACK
w.color = RED
RIGHT_ROTATE(T, w)
w = x.parent.right
w.color = x.parent.color // Case 4: far nephew red
x.parent.color = BLACK
w.right.color = BLACK
LEFT_ROTATE(T, x.parent)
x = T.root
else: // 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 = BLACKflowchart TD Start["x carries extra black\n(double-black)"] --> C1{"Sibling w\ncolor?"} C1 -->|"RED"| Case1["<b>Case 1</b>\nw → BLACK, p → RED\nLEFT_ROTATE(p)\nw = p.right"] Case1 --> C2 C1 -->|"BLACK"| C2{"w's children\ncolors?"} C2 -->|"both BLACK"| Case2["<b>Case 2</b>\nw → RED\nx = p\n(push black up)"] Case2 --> Start C2 -->|"near RED\nfar BLACK"| Case3["<b>Case 3</b>\nnear → BLACK, w → RED\nRIGHT_ROTATE(w)\nw = p.right"] Case3 --> Case4 C2 -->|"far RED"| Case4["<b>Case 4</b>\nw → p.color, p → BLACK\nfar → BLACK\nLEFT_ROTATE(p)\n✓ resolved"] style Case1 fill:#fff3e0,stroke:#e65100,color:#333 style Case2 fill:#e3f2fd,stroke:#1565c0,color:#333 style Case3 fill:#fce4ec,stroke:#c62828,color:#333 style Case4 fill:#e8f5e9,stroke:#2e7d32,color:#333 style Start fill:#f3e5f5,stroke:#7b1fa2,color:#333
Case 1 converts a red sibling to black, reducing to Cases 2–4. Case 3 rotates the near red nephew to the far position, reducing to Case 4. Case 4 is terminal — one rotation and recolor discharges the double black. Case 2 pushes the extra black up (may loop).
Example (Stepwise)
Insert 41, 38, 31, 12 (empty tree initially):
-
Insert 41 (root colored black by fixup).
-
Insert 38 (red child of black → OK).
-
Insert 31: parent 38 is red and uncle (right of 41) is NIL (black); triangle about
(41 ← 38 → 31). Rotate right at 38 to line up, then left at 41, recolor to make new root black, children red. -
Insert 12: recolor with red uncle if encountered, else rotate/recolor depending on orientation. In a few steps, invariants hold and height stays small.
Delete 38:
-
Successor swap if needed; splice out successor
y. -
If the removed node was black, run delete fixup. Follow the sibling cases to discharge double black with at most two rotations and a few recolors.
Tip
During debugging, log (node, color, bh) along the modified path; black-height should remain the same at affected subtrees after each fixup step.
Complexity and Performance
Let n be the number of nodes.
-
Search:
O(log n)worst-case (height bounded). -
Insert:
O(log n)search plus ≤ 2 rotations andO(1)recolors. -
Delete:
O(log n)search plus ≤ 3 rotations andO(1)recolors in fixup. -
Space:
Θ(n)(one color bit; many implementations pack color in a parent pointer’s low bit or alongside metadata).
Height bound. From properties (4) and (5):
-
Longest path alternates red/black at most, so its length ≤ 2 × black-height.
-
Each subtree with black-height
bhhas at least2^{bh} − 1internal nodes. -
Therefore
h ≤ 2 log2(n + 1), giving the logarithmic guarantees.
Implementation Details or Trade-offs
Sentinels vs nulls
- Use a single shared black
NILsentinel for all external leaves (simplifies color checks and avoids null-branching). Root’s parent may also be aNILsentinel.
Color storage
- Pack color in pointer bits (if aligned) or store as a single byte; keep the hot fields contiguous to help cache locality.
API design
-
Expose map/set operations (
find,lower_bound,insert,erase,iterate in-order). -
Provide iterators via in-order successor/predecessor using parent pointers.
Iterator stability
- Rotations preserve in-order sequence, so iterators to elements remain valid except to erased nodes (container-specific rules apply).
Comparators
- Require a strict weak ordering comparator (transitive, antisymmetric). Comparator bugs can violate BST invariants and break balancing logic.
Deletion corner cases
-
When removing a red node, no fixup is needed (colors/bh unchanged).
-
When splicing a black with a red child, color the child BLACK and skip fixup (discharges the double black immediately).
Testing harness
-
After random sequences, verify:
-
Root is black;
-
No red–red edges;
-
Equal black-height on all root→NIL paths;
-
In-order traversal is sorted and has exactly
nnodes.
-
Warning
Mixing mirrored cases (left/right) is a classic source of bugs. Implement left-cases and generate right-cases mechanically (or through a macro/templating trick) to avoid divergence.
Warning
Forgetting to update parents during rotations (especially
NILparents) silently corrupts the structure and causes iterator crashes later.
Warning
Recolor order matters: in delete fixup Case 4, copy parent’s color to sibling before painting parent and far nephew black, then rotate.
Practical Use Cases
-
Ordered maps/sets in language libraries: C++
std::map/std::set, many JVM collections, OS schedulers. -
Event queues and interval indexes needing ordered traversal plus fast updates.
-
Database/storage engines (memory-resident trees) where consistent worst-case bounds beat average-case structures.
Tip
If you need order statistics (select k-th, rank queries), augment nodes with subtree sizes and maintain them across rotations - RBT balancing and size updates compose cleanly.
Limitations / Pitfalls
Warning
Deletion complexity is higher than insertion; ensure all four sibling cases (and mirrors) are covered. Missing a near/far nephew check leads to infinite loops or property violations.
Warning
Pathological comparator (non-transitive, inconsistent across calls) can create cycles or break the BST invariant; validate comparator semantics in debug builds.
Warning
Incorrect NIL handling during fixups. Treat external leaves as black nodes; accessing fields through actual
nullrather than aNILsentinel tends to add branches and bugs.
Summary
Red–black trees are height-balanced BSTs that guarantee O(log n) operations through a compact set of coloring rules and local rotations. Insertion is handled by at most two rotations and a small recolor dance; deletion by a well-understood double-black fixup. Their balance guarantee, modest memory overhead, and robust performance under arbitrary update sequences explain their dominance as the default balanced ordered container in general-purpose libraries.