Red-Black Tree
A self-balancing BST that colors nodes red or black and enforces color rules so that no path is more than twice as long as any other.
Definition
A red-black tree is a Binary Search Tree where each node carries one bit of color and five rules hold: every node is red or black; the root is black; every leaf (null) is black; a red node has two black children (no two reds in a row); and every root-to-leaf path has the same number of black nodes (the black-height). Together these imply the longest path is at most twice the shortest, so h ≤ 2 log₂(n + 1).
Compared with an AVL Tree, a red-black tree is slightly taller but needs at most two rotations per insert and three per delete, with the rest of the fix-up done by recoloring. That makes it the default choice for library ordered maps: C++ std::map/std::set, Java TreeMap/TreeSet, the Linux kernel scheduler and many memory allocators.
A red-black tree is exactly a B-Tree of order 4 (a 2-3-4 tree) drawn as a binary tree: each black node with its red children forms one B-tree node. That view explains why the rules look arbitrary but produce balance.
Intuition
A mental model before the formal terms.
Think of black nodes as "real" levels and red nodes as extra keys squeezed into the same level. The rule "same black count on every path" means every leaf sits at the same *black* depth, like a perfectly balanced tree; the rule "no red-red" limits how many extras can hide in one level. So the tree is a perfect tree with at most a factor-of-two stretch.
Insert always paints the new node red so that black-heights stay equal; the only rule that can break is red-red, and that is fixed by pushing the problem upward with recolors until a rotation or the root resolves it.
How it works
- Insert as in a BST and color the new node red. If its parent is black, done.
- If the parent is red, look at the uncle. Uncle red: recolor parent and uncle black, grandparent red, and repeat at the grandparent. Uncle black: rotate — a single rotation at the grandparent for the "line" case, a double rotation for the "triangle" case — then recolor so the subtree root is black.
- Finally paint the root black; this can only increase black-height uniformly.
- Delete removes a node with at most one child (after successor replacement). Removing a red node breaks nothing. Removing a black node creates a "double black" deficit, resolved by cases on the sibling's color and its children's colors, using at most three rotations.
- Search, min, max, successor and inorder traversal are identical to a plain BST.
Why it works
Let bh be the black-height. The subtree rooted at any node contains at least 2^bh - 1 nodes (induction on height). Since no two reds are adjacent, h ≤ 2·bh, so n ≥ 2^(h/2) - 1 and h ≤ 2 log₂(n + 1).
Each insert fix-up step either terminates with at most two rotations or moves the violation two levels up, so the fix-up is O(log n) with O(1) rotations.
Rotations preserve inorder order, and the recolor cases are chosen precisely so black-height stays equal on all paths after each step.
Operations
| Operation | Description | Cost |
|---|---|---|
| search(key) | Standard BST descent. | O(log n) |
| insert(key) | BST insert as red, then recolor/rotate fix-up walking up; ≤ 2 rotations. | O(log n) |
| delete(key) | BST delete then double-black fix-up; ≤ 3 rotations. | O(log n) |
| rotate | Same pointer surgery as AVL, plus color swaps. | O(1) |
| min / max / successor | Same as BST. | O(log n) |
| inorder traversal | Sorted output. | O(n) |
Recognition
How to tell a problem wants this.
- You need a guaranteed-
O(log n)ordered container and you are in a language with one built in: reach forTreeMap/std::mapand know it is a red-black tree. - Interview question: "how does
TreeMapwork?" or "compare AVL and red-black". - Write-heavy ordered workloads where fewer rotations matter.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related AVL Tree visualization.
1insert(node, key): BST insert recursively2update height(node); bf = height(left) - height(right)3if bf > 1 and key < node.left.key: LL → rotateRight(node)4if bf < -1 and key > node.right.key: RR → rotateLeft(node)5if bf > 1 and key > node.left.key: LR → rotateLeft(node.left), then rotateRight(node)6if bf < -1 and key < node.right.key: RL → rotateRight(node.right), then rotateLeft(node)7return node (possibly the new subtree root)Pseudocode
1insert(key):2 z = bstInsert(key); z.color = RED3 while z.parent is RED:4 uncle = sibling of z.parent5 if uncle is RED: recolor parent, uncle BLACK; grandparent RED; z = grandparent6 else:7 if z is inner child: rotate z.parent toward outside; z = old parent8 rotate grandparent away from z; swap colors of parent and grandparent9 root.color = BLACKImplementation
1from typing import Optional2 3RED = "red"4BLACK = "black"5 6 71 · Node with color and parent pointer8class RBNode:9 def __init__(self, key: int) -> None:10 self.key = key11 self.color = RED12 self.left: Optional["RBNode"] = None13 self.right: Optional["RBNode"] = None14 self.parent: Optional["RBNode"] = None15 16 17class RBTree:18 def __init__(self) -> None:19 self.root: Optional[RBNode] = None20 21 def contains(self, key: int) -> bool:22 cur = self.root23 while cur is not None:24 if key == cur.key:25 return True26 cur = cur.left if key < cur.key else cur.right27 return False28 292 · Rotations that maintain parent pointers30 def _rotate_left(self, x: RBNode) -> None:31 y = x.right32 assert y is not None33 x.right = y.left34 if y.left is not None:35 y.left.parent = x36 y.parent = x.parent37 if x.parent is None:38 self.root = y39 elif x is x.parent.left:40 x.parent.left = y41 else:42 x.parent.right = y43 y.left = x44 x.parent = y45 46 def _rotate_right(self, y: RBNode) -> None:47 x = y.left48 assert x is not None49 y.left = x.right50 if x.right is not None:51 x.right.parent = y52 x.parent = y.parent53 if y.parent is None:54 self.root = x55 elif y is y.parent.left:56 y.parent.left = x57 else:58 y.parent.right = x59 x.right = y60 y.parent = x61 623 · BST insert as a red leaf63 def insert(self, key: int) -> None:64 z = RBNode(key)65 parent: Optional[RBNode] = None66 cur = self.root67 while cur is not None:68 parent = cur69 if key == cur.key:70 return71 cur = cur.left if key < cur.key else cur.right72 z.parent = parent73 if parent is None:74 self.root = z75 elif key < parent.key:76 parent.left = z77 else:78 parent.right = z79 self._fix_insert(z)80 81 @staticmethod82 def _is_red(n: Optional[RBNode]) -> bool:83 return n is not None and n.color == RED84 854 · Fix-up: recolor or rotate while parent is red86 def _fix_insert(self, z: RBNode) -> None:87 while self._is_red(z.parent):88 p = z.parent89 assert p is not None90 g = p.parent91 assert g is not None # a red node is never the root92 if p is g.left:93 uncle = g.right94 if self._is_red(uncle): # case 1: recolor, move up95 assert uncle is not None96 p.color = uncle.color = BLACK97 g.color = RED98 z = g99 else:100 if z is p.right: # case 2: LR -> LL101 z = p102 self._rotate_left(z)103 p = z.parent104 assert p is not None105 p.color = BLACK # case 3: rotate106 g.color = RED107 self._rotate_right(g)108 else: # mirror image109 uncle = g.left110 if self._is_red(uncle):111 assert uncle is not None112 p.color = uncle.color = BLACK113 g.color = RED114 z = g115 else:116 if z is p.left:117 z = p118 self._rotate_right(z)119 p = z.parent120 assert p is not None121 p.color = BLACK122 g.color = RED123 self._rotate_left(g)1245 · Root is always black125 assert self.root is not None126 self.root.color = BLACK127 128 def inorder(self) -> list[int]:129 out: list[int] = []130 stack: list[RBNode] = []131 cur = self.root132 while cur or stack:133 while cur:134 stack.append(cur)135 cur = cur.left136 cur = stack.pop()137 out.append(cur.key)138 cur = cur.right139 return out- Colors are module-level string constants; nodes start
REDwithparent = None. - Rotations
assertthe pivot child exists and useisfor identity checks likex is x.parent.left. insertdescends iteratively, attaches the red node and calls_fix_insert._fix_insertfollows the three cases; the manyassert ... is not Nonelines exist to satisfy static type checkers, since the invariants guarantee them.- The root is forced black at the end.
Iterative, so no RecursionError risk; inorder is also iterative.
- Use
is/is notfor node identity;==would fall back to identity anyway butisstates the intent. - A shared
NILsentinel node (as in CLRS) removes mostNonechecks at the cost of extra care ininorder. - Python has no red-black tree in the stdlib;
sortedcontainersis the common third-party choice and is faster than a pure-Python tree.
- Comparing colors with
ison strings — it happens to work for interned literals but use==. - Forgetting to re-read
p = z.parentafter the case-2 rotation. - Leaving the root red after the loop.
- C++ has red-black trees built in (
std::map/std::set); JS/TS/Python do not. - TS uses a type guard so
uncle.colortype-checks; Python needsassertlines for the same purpose; JS and C++ rely on the invariant only. - Identity comparison: C++ compares pointers with
==, JS/TS use===, Python usesis.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(log n) | O(log n) | |
| Search | O(log n) | O(log n) | |
| Insert | O(log n) | O(log n) | ≤ 2 rotations, O(log n) recolors. |
| Delete | O(log n) | O(log n) | ≤ 3 rotations. |
| Update | O(log n) | O(log n) | |
| Rotation | O(1) | O(1) | |
| Inorder traversal | O(n) | O(n) | |
| Space | O(n) | Height ≤ 2 log₂(n + 1). One color bit per node (often stored in a pointer's low bit). | |
Advantages & disadvantages
- Guaranteed
O(log n)with a constant number of rotations per update — good for write-heavy use. - Only one bit of metadata per node.
- Battle-tested: the implementation behind most standard library ordered maps.
- Up to twice as tall as an AVL tree, so lookups can be slightly slower.
- Deletion fix-up has many cases and is notoriously error-prone to write by hand.
- Pointer chasing and poor locality like any binary tree; B-Trees win for large or disk-resident data.
Use cases
std::map,std::set, JavaTreeMap,TreeSet, .NETSortedDictionary.- Linux CFS scheduler run-queue and virtual memory area trees.
- Sweep-line algorithms that need an ordered status structure with predecessor/successor.
- You need a general-purpose ordered map with guaranteed bounds and mixed reads/writes.
- You are using a standard library ordered container — you already are using one.
- Fewer rotations matter, e.g. when rotations invalidate cached data or iterators.
Alternatives
Common mistakes
- Forgetting to set the root black after fix-up.
- Treating
nullleaves as red; they must count as black in the uncle test. - Missing the triangle-to-line pre-rotation, which leaves a red-red violation after the main rotation.
- Losing
parentpointer updates during rotations — every rotation touches three parent links.
Interview patterns
- State the five properties and derive the
2 log₂(n+1)height bound. - Walk through inserting
10, 20, 30(recolor vs rotate cases). - Explain why
TreeMapuses red-black rather than AVL. - Map a red-black tree to a 2-3-4 tree to explain the color rules.
- Recursion versus iterationIntermediate
- Greedy or dynamic programming?Advanced
- Merge IntervalsIntermediate