Data Structure 📂 Tree in Data Structure · 3 of 7 61 min read

Red-Black Tree — Balanced BST with Colour Properties

Master Red-Black Trees with animated diagrams, step-by-step rotation walkthroughs, full Python code, complexity tables, and real-world use cases from Linux to Java.

Section 01

The Story Behind Red-Black Trees

The Cinema Seat Problem
Imagine a cinema booking system. Every seat has a number — 1 to 500. As bookings arrive, you insert seat numbers into a list. If you insert them in order (1, 2, 3, 4…) into a plain Binary Search Tree (BST), you get a perfectly straight line — every new node is the rightmost child. Finding seat 499 means traversing 498 steps. That's O(n) — as slow as a plain array.

Now imagine a supervisor who paints each seat either Red or Black and enforces a strict rule: "No path from entrance to any empty seat may have more than twice as many steps as any other path." This forces the cinema to stay balanced — and that supervisor's rulebook is the Red-Black Tree.

A Red-Black Tree is a self-balancing Binary Search Tree where every node carries a colour bit — red or black — and four invariants (rules) ensure the tree height stays at most 2 log₂(n+1). This guarantees O(log n) for search, insert, and delete — always, even in pathological insertion orders.

🏛️
Where You Already Use Red-Black Trees

The C++ STL std::map and std::set, Java's TreeMap and TreeSet, Python's SortedDict (via sortedcontainers), Linux kernel's completely fair scheduler (CFS), and the epoll event system all rely on Red-Black Trees internally.


Section 02

The Five Red-Black Properties

A valid Red-Black Tree must satisfy all five of these properties simultaneously. Violating even one means the tree is broken and may degrade to O(n).

🌲 The Five Inviolable Red-Black Properties
P1
Every node is either Red or Black. The colour bit is a single extra byte per node — minimal memory overhead.
P2
The root is always Black. After every insertion or deletion, if the root ends up red, it is simply recoloured black.
P3
Every leaf (NIL sentinel) is Black. NIL nodes are conceptual black leaves — they simplify the algorithm by eliminating special-case null checks.
P4
If a node is Red, both its children must be Black. No two consecutive red nodes on any root-to-leaf path. This is the "no double red" rule — the most commonly violated during insertion.
P5
All paths from any node to its descendant NIL leaves contain the same number of Black nodes (the Black-Height). This is the property that guarantees logarithmic balance.
⚠️
Why These Five Together Guarantee O(log n)

P5 alone ensures every root-to-leaf path has the same black-node count bh. P4 means between any two black nodes there can be at most one red node. So the longest path is at most 2·bh and the shortest is bh. This bounds tree height at 2·log₂(n+1) — a 2× constant, never worse.


Section 03

Anatomy of a Valid Red-Black Tree

The diagram below shows a valid RBT with black-height = 2 on every path. Hover nodes to inspect their properties.

🌲 Valid Red-Black Tree (static view) black-height = 2 on every path
Node 13 | Color: BLACK | Black-Height: 2 13 Node 8 | Color: RED | P4: both children are black ✓ 8 Node 17 | Color: RED | P4: both children are black ✓ 17 Node 1 | Color: BLACK | Black-Height from here: 1 1 Node 11 | Color: BLACK | Black-Height from here: 1 11 Node 15 | Color: BLACK | Black-Height from here: 1 15 Node 25 | Color: BLACK | Black-Height from here: 1 25 N N N N N N N N RED node BLACK node NIL sentinel
↑ Every path from root to NIL: 13→8→1→NIL = 2 black nodes; 13→17→25→NIL = 2 black nodes. Black-height is uniform. ✓
💡
Black-Height — The Key Metric

The black-height of a node is the number of black nodes on any path from that node down to a NIL leaf (not counting the node itself). P5 says all paths from the same node must have the same black-height. A tree with black-height bh has at least 2bh − 1 internal nodes.


Section 04

Rotations — The Building Block

Before understanding insertion/deletion, you must understand rotations. They are local pointer rearrangements — O(1) — that change tree structure without violating BST ordering. There are two: Left-Rotate and Right-Rotate.

🔄 Left Rotation Animation — step through each pointer change Step 0 / 5
BEFORE x α y β γ left-rotate(x) AFTER y x α β γ

def left_rotate(T, x):
  y = x.right                # y is x's right child
  x.right = y.left           # β becomes x's right
  if y.left != T.nil: y.left.parent = x
  y.parent = x.parent        # link y to x's parent
  if x.parent == T.nil: 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
  x.parent = y
Press ▶ Next Step to walk through each pointer change.
Left-rotate(x): y = x.right child is promoted above x.

Section 05

Insertion — Step-by-Step with Fix-Up Animation

Insertion follows normal BST insertion, then colours the new node Red, then calls RB-Insert-Fixup to restore Properties P2 and P4 if violated. There are three fixup cases based on the uncle's colour.

🔴
Case 1 — Uncle is Red
Recolour parent + uncle → Black, grandparent → Red. Move the "problem" two levels up. No rotation needed.
Recolour only · O(log n) recolours max
↗️
Case 2 — Uncle Black, Triangle
New node is the inner grandchild (forms a "triangle"). Rotate the parent to convert to Case 3. No colour change yet.
1 rotation → converts to Case 3
⬆️
Case 3 — Uncle Black, Line
New node is the outer grandchild (forms a "line"). Rotate grandparent, swap colours of parent and grandparent.
1 rotation + 2 recolours → Done
🎬 Inserting 4 into the tree — watch fix-up cases unfold Phase 0 / 6
7 3 18 2 5 14 20 N N N N N N N N
Press ▶ Next Phase to begin the insertion walkthrough.
Tree contains [7, 3, 18, 2, 5, 14, 20]. Insert 4.

Section 06

Python Implementation — Full Red-Black Tree

🐍
What This Code Implements

A complete RBT with insert(), search(), inorder(), and all three fix-up cases. Deletion is shown in Section 08. Uses a sentinel NIL node to simplify boundary conditions — the standard CLRS approach.

# ── Red-Black Tree Node ─────────────────────────────────────
class RBNode:
    def __init__(self, key):
        self.key    = key
        self.color  = "RED"       # new nodes always start RED
        self.left   = None
        self.right  = None
        self.parent = None

# ── Red-Black Tree ───────────────────────────────────────────
class RedBlackTree:
    def __init__(self):
        self.NIL  = RBNode(0)        # sentinel: BLACK, shared leaf
        self.NIL.color = "BLACK"
        self.root = self.NIL

    # ── Rotations (O(1)) ─────────────────────────────────────
    def _left_rotate(self, x):
        y         = x.right
        x.right   = y.left
        if y.left != self.NIL:
            y.left.parent = x
        y.parent  = x.parent
        if   x.parent == self.NIL: self.root       = y
        elif x        == x.parent.left: x.parent.left  = y
        else:                              x.parent.right = y
        y.left    = x
        x.parent  = y

    def _right_rotate(self, y):
        x         = y.left
        y.left    = x.right
        if x.right != self.NIL:
            x.right.parent = y
        x.parent  = y.parent
        if   y.parent == self.NIL: self.root       = x
        elif y        == y.parent.right: y.parent.right = x
        else:                               y.parent.left  = x
        x.right   = y
        y.parent  = x

    # ── Insert public API ────────────────────────────────────
    def insert(self, key):
        z        = RBNode(key)
        z.left   = self.NIL
        z.right  = self.NIL
        z.parent = self.NIL
        y = self.NIL
        x = self.root
        while x != self.NIL:           # standard BST walk
            y = x
            x = x.left if z.key < x.key else x.right
        z.parent = y
        if   y       == self.NIL:  self.root    = z
        elif z.key   <  y.key:     y.left       = z
        else:                         y.right      = z
        self._insert_fixup(z)

    # ── Insert Fix-Up (Cases 1-3, mirrored for left/right) ──
    def _insert_fixup(self, z):
        while z.parent.color == "RED":          # P4 violated
            if z.parent == z.parent.parent.left:
                uncle = z.parent.parent.right
                if uncle.color == "RED":             # Case 1
                    z.parent.color         = "BLACK"
                    uncle.color            = "BLACK"
                    z.parent.parent.color  = "RED"
                    z = z.parent.parent
                else:
                    if z == z.parent.right:       # Case 2
                        z = z.parent
                        self._left_rotate(z)
                    z.parent.color        = "BLACK"  # Case 3
                    z.parent.parent.color = "RED"
                    self._right_rotate(z.parent.parent)
            else:                                    # mirror: parent is right child
                uncle = z.parent.parent.left
                if uncle.color == "RED":
                    z.parent.color        = "BLACK"
                    uncle.color           = "BLACK"
                    z.parent.parent.color = "RED"
                    z = z.parent.parent
                else:
                    if z == z.parent.left:
                        z = z.parent
                        self._right_rotate(z)
                    z.parent.color        = "BLACK"
                    z.parent.parent.color = "RED"
                    self._left_rotate(z.parent.parent)
        self.root.color = "BLACK"                  # P2: root always black

    # ── Search O(log n) ──────────────────────────────────────
    def search(self, key, node=None):
        if node is None: node = self.root
        if node == self.NIL or node.key == key:
            return node
        return self.search(key, node.left) if key < node.key \
          else self.search(key, node.right)

    # ── In-order traversal → sorted output ─────────────────
    def inorder(self, node=None):
        if node is None: node = self.root
        result = []
        stack, cur = [], node
        while cur != self.NIL or stack:
            while cur != self.NIL:
                stack.append(cur); cur = cur.left
            cur = stack.pop()
            result.append((cur.key, cur.color[0]))  # (key, 'R'/'B')
            cur = cur.right
        return result

# ── Demo ─────────────────────────────────────────────────────
if __name__ == "__main__":
    rbt = RedBlackTree()
    for k in [7, 3, 18, 10, 22, 8, 11, 26]:
        rbt.insert(k)

    print("In-order (sorted):")
    for key, col in rbt.inorder():
        print(f"  {key:3d}  [{col}]")

    found = rbt.search(10)
    print(f"\nSearch 10: found={found.key != 0}, color={found.color}")
    miss = rbt.search(99)
    print(f"Search 99: found={miss != rbt.NIL}")
OUTPUT
In-order (sorted): 3 [B] 7 [B] 8 [R] 10 [B] 11 [R] 18 [B] 22 [B] 26 [R] Search 10: found=True, color=BLACK Search 99: found=False
What the Output Confirms

In-order output is sorted — BST property preserved. Colours alternate red/black in a valid pattern. The root (not shown but always 10 after these insertions) is Black. No two consecutive red nodes appear in any path. Black-height is uniform at 2.


Section 07

Deletion — The Harder Operation

Deletion is more complex: removing a Black node reduces the black-height on that path. The fix-up loop handles 4 cases, with symmetry between left and right. The key concept is a "double-black" phantom colour to track the deficit.

🗑️ RB-Delete Fix-Up: 4 Cases
Case 1
Sibling is Red — Rotate parent, swap sibling+parent colours. Converts to Case 2, 3, or 4.
Case 2
Sibling is Black, sibling's children both Black — Recolour sibling Red, move deficit up to parent.
Case 3
Sibling Black, sibling's far child Black, near child Red — Rotate sibling, swap sibling+near child colours → Case 4.
Case 4
Sibling Black, sibling's far child Red — Rotate parent, recolour sibling = parent's colour, parent+far child → Black. Done.
# ── Delete (adds to RedBlackTree class) ─────────────────────
def delete(self, key):
    z = self.search(key)
    if z == self.NIL: return           # key not found
    y, y_orig_color = z, z.color
    if   z.left  == self.NIL: x = z.right; self._transplant(z, z.right)
    elif z.right == self.NIL: x = z.left;  self._transplant(z, z.left)
    else:
        y = self._minimum(z.right)    # in-order successor
        y_orig_color = y.color
        x = y.right
        if y.parent == z: x.parent = y
        else:
            self._transplant(y, y.right)
            y.right = z.right; y.right.parent = y
        self._transplant(z, y)
        y.left = z.left; y.left.parent = y
        y.color = z.color
    if y_orig_color == "BLACK":
        self._delete_fixup(x)           # only needed if black node removed

def _transplant(self, u, v):
    if   u.parent == self.NIL:       self.root        = v
    elif u        == u.parent.left:   u.parent.left   = v
    else:                              u.parent.right  = v
    v.parent = u.parent

def _minimum(self, node):
    while node.left != self.NIL: node = node.left
    return node
CONCEPT OUTPUT
Deleting RED node → no fixup needed (no black-height change) Deleting BLACK node → _delete_fixup(x) (black-height deficit on x's path) Max rotations per delete: 3 (Cases 1→3→4 in worst case) Amortised cost: O(log n) always

Section 08

Time & Space Complexity

Operation Average Worst Case Notes Guaranteed?
Search O(log n) O(log n) Height ≤ 2·log₂(n+1) ✓ Always
Insert O(log n) O(log n) ≤ 2 rotations per insert ✓ Always
Delete O(log n) O(log n) ≤ 3 rotations per delete ✓ Always
Min / Max O(log n) O(log n) Leftmost / rightmost node ✓ Always
In-order traversal O(n) O(n) Visits every node once ✓ Always
BST search (unbalanced) O(log n) O(n) Degrades on sorted input ✗ Not guaranteed
AVL search O(log n) O(log n) Stricter balance (height diff ≤1) ✓ Faster lookup
AVL insert O(log n) O(log n) Up to O(log n) rotations ⚠ More rotations
Space (per node) O(n) + 1 bit O(n) 1 extra colour bit vs plain BST ✓ Minimal overhead
⚖️
RBT vs AVL — When to Choose What

AVL trees are more strictly balanced (height ≤ 1.44·log n vs RBT's 2·log n), so they offer faster lookups. But RBTs do fewer rotations on insert/delete (max 2 vs O(log n) for AVL). Prefer RBT for write-heavy workloads (databases, schedulers). Prefer AVL for read-heavy, static data.


Section 09

Real-World Use Cases

🐧
Linux CFS Scheduler
kernel/sched/fair.c
Linux's Completely Fair Scheduler stores tasks in an RBT keyed by virtual runtime. The leftmost node (minimum key) is the next task to run — retrieved in O(1) with caching. Insertions/deletions on context switch cost O(log n).
🗄️
Database Index Pages
In-memory sorted index
MySQL's InnoDB uses B-Trees on disk, but for in-memory adaptive hash indexes and InnoDB row locks, RBTs manage ordered key sets efficiently without disk I/O overhead.
Java TreeMap / TreeSet
java.util.TreeMap
Java's TreeMap is a textbook RBT implementation. It provides floorKey(), ceilingKey(), subMap() — all in O(log n) — used in interval scheduling, event queues, and range queries.
epoll Event System
Linux I/O multiplexing
Linux's epoll uses an RBT to manage file descriptor sets. Adding/removing socket FDs during high-throughput networking (10K+ connections) stays O(log n) regardless of connection count.
🔧
C++ STL std::map
GCC libstdc++ / LLVM libc++
Both major C++ standard library implementations back std::map and std::set with Red-Black Trees, providing deterministic O(log n) for all ordered-map operations.
💰
Trading Order Books
Price-time priority queues
High-frequency trading engines use RBTs (or B-Trees) for limit order books: price levels are keys, lists of orders are values. Best bid/ask is O(1) via min/max pointers; insert/cancel is O(log n).

Section 10

Live Insertion Sequence — Watch the Tree Balance Itself

Insert keys 1 through 7 in order — the worst case for a plain BST (degrades to a linked list). Watch the RBT keep itself balanced through rotations and recolouring.

📈 Sequential insert 1→7 (worst case for plain BST) Not started
Click ▶ Insert Next to begin
A plain BST inserting 1,2,3,4,5,6,7 becomes a straight line — O(n). RBT stays balanced.
Keys to insert: 1, 2, 3, 4, 5, 6, 7

Section 11

RBT vs Alternatives — Quick Comparison

Property Plain BST AVL Tree Red-Black Tree B-Tree
Worst-case search O(n) O(log n) O(log n) O(log n)
Insert rotations max 0 O(log n) 2 O(log n) splits
Delete rotations max 0 O(log n) 3 O(log n) merges
Balance strictness None Strict (Δh ≤ 1) Relaxed (2× log n) Disk-optimal pages
Implementation complexity Simple Moderate Moderate Complex
Best use case Sorted input known-small Read-heavy lookups Balanced read/write Disk/DB storage
Used in Teaching AVL databases, gaming Linux, Java, C++ STL MySQL, SQLite, NTFS

Section 12

The Full Insertion Pipeline

01
Standard BST Insert
Walk the tree from root: go left if key < node.key, right otherwise. Stop at NIL. Place new node there. O(log n) comparisons.
02
Colour New Node RED
New node always starts RED. Why? Inserting RED never changes black-height (P5). Only risk is P4 (two consecutive reds). Far easier to fix than a P5 violation.
03
Check P4 — Is parent RED?
If parent is BLACK → no violation → done. If parent is RED → double-red violation → enter fix-up loop.
04
Fix-Up Loop — Check Uncle
Uncle = sibling of parent. Uncle RED → Case 1 (recolour, move up). Uncle BLACK + triangle → Case 2 (rotate parent). Uncle BLACK + line → Case 3 (rotate grandparent + recolour).
05
Force Root → BLACK
After fix-up, unconditionally set root.color = BLACK. If fix-up propagated a red to the root (Case 1 chain), this single line fixes P2 at zero cost.

Section 13

Golden Rules — Red-Black Trees

🌲 6 Non-Negotiable Red-Black Tree Rules
1
Always insert new nodes as RED. Inserting as black immediately violates P5 (changes black-height on one path). Red only risks P4, which is far simpler to fix — at most 2 rotations ever.
2
Always use a sentinel NIL node, not null pointers. A single shared NIL node (colour = BLACK, children = itself) eliminates all boundary-condition checks in rotations and fix-ups. CLRS-standard practice.
3
Understand the 3 insert cases — they are symmetric. Cases 1/2/3 for left-subtree violations have exact mirrors for right-subtree. The only difference is swapping "left" and "right" everywhere. Code both or use a direction variable.
4
After any rotation, verify all 5 properties mentally. Rotations preserve BST ordering and black-height (P5) but you must still verify P4 (no double-red) is resolved — not worsened — by the rotation.
5
Deletion fix-up only triggers when a BLACK node is removed. Removing a RED node never changes black-height (P5). Only when y_original_color == BLACK do you call delete_fixup. This is the most commonly misunderstood point in RBT deletion.
6
Never confuse RBT with heap. A heap orders parent > children. An RBT orders left < node < right (BST property). RBT gives O(log n) search; a heap gives O(n) search but O(1) max/min extraction. Use RBT for sorted-map semantics; use heap for priority queues.
🎓
You Now Know Red-Black Trees

You understand the 5 properties, both rotations, all 3 insertion fix-up cases, the 4 deletion fix-up cases, and the time complexity guarantees. The pattern to remember: insert RED → at most 2 rotations → O(log n) always. Every balanced BST in every production system you use is built on these exact ideas.