| name | algo-red-black-trees |
| description | Implement a red-black self-balancing BST (insert, rotations, recoloring) for O(log n) search on sorted VCF variant positions. Use when building a balanced BST, verifying invariants, or comparing red-black vs AVL trees. |
| tool_type | python |
| primary_tool | Python |
Red-Black Trees
Self-balancing BST where each node is RED or BLACK. Guarantees O(log n) search/insert/delete even when input arrives pre-sorted (where a plain BST degrades to O(n)).
When to Use
- Need a balanced BST with guaranteed O(log n) insert/search on data that may arrive in sorted order (e.g., SNP positions read in genomic order from a VCF).
- Implementing/explaining the standard library behind
TreeMap (Java), std::map/std::set (C++), or the Linux CFS scheduler.
- Write-heavy workload where AVL's stricter balancing would cause excess rotations.
- Building an ordered index over genomic coordinates to support overlap/range queries (gene annotations, ChIP-seq peaks, variant calls).
- Asked to verify/prove the 5 red-black invariants hold after a sequence of insertions.
Version Compatibility
Pure Python, stdlib only — no version dependencies. Examples use Python ≥3.9 (type hints with list[int], tuple[int,int]).
Prerequisites
- No external packages required (stdlib only).
- Prior concept: binary search trees (see
algo-binary-search-trees) — red-black trees add self-balancing on top of plain BST insert/search.
5 Invariants
| # | Property |
|---|
| 1 | Every node is RED or BLACK |
| 2 | Root is BLACK |
| 3 | Every leaf (NIL) is BLACK |
| 4 | RED node has only BLACK children (no red-red) |
| 5 | All root-to-leaf paths have equal BLACK node count (black-height) |
These guarantee: longest path <= 2x shortest path, so height is O(log n).
Red-Black vs AVL
| Aspect | AVL | Red-Black |
|---|
| Height bound | 1.44 log n | 2 log n |
| Search | Faster (shorter) | Slightly slower |
| Insert/Delete | More rotations | Fewer rotations (recolor often suffices) |
| Best for | Read-heavy | Write-heavy |
| Used in | Database indexes | Java TreeMap, C++ std::map, Linux CFS |
Core Implementation
Goal: a self-balancing BST that stays O(log n) even under sorted-order insertion.
Approach: color every node RED or BLACK, insert as a normal BST insert (new node RED), then walk up from the new node fixing red-red violations with rotations and recoloring until the 5 invariants hold.
RED, BLACK = True, False
class RBNode:
"""Node for a Red-Black Tree; new nodes are RED by default."""
def __init__(self, data, color=RED):
self.data = data
self.color = color
self.parent = self.left = self.right = None
def __repr__(self):
return f"({'R' if self.color == RED else 'B'}){self.data}"
class RedBlackTree:
"""Self-balancing BST guaranteeing O(log n) search/insert."""
def __init__(self):
self.NIL = RBNode(data=None, color=BLACK)
self.root = self.NIL
def rotate_left(self, x):
y = x.right
x.right = y.left
if y.left is not self.NIL:
y.left.parent = x
y.parent = x.parent
if x.parent is None:
self.root = y
x x.parent.left:
x.parent.left = y
:
x.parent.right = y
y.left = x
x.parent = y
():
x = y.left
y.left = x.right
x.right .NIL:
x.right.parent = y
x.parent = y.parent
y.parent :
.root = x
y y.parent.right:
y.parent.right = x
:
y.parent.left = x
x.right = y
y.parent = x
():
node = RBNode(data, color=RED)
node.left = node.right = .NIL
parent, current = , .root
current .NIL:
parent = current
data < current.data:
current = current.left
data > current.data:
current = current.right
:
node.parent = parent
parent :
.root = node
data < parent.data:
parent.left = node
:
parent.right = node
._fix_insert(node)
():
node.parent node.parent.color == RED:
gp = node.parent.parent
node.parent gp.left:
uncle = gp.right
uncle.color == RED:
node.parent.color = uncle.color = BLACK
gp.color = RED
node = gp
:
node node.parent.right:
node = node.parent
.rotate_left(node)
node.parent.color = BLACK
gp.color = RED
.rotate_right(gp)
:
uncle = gp.left
uncle.color == RED:
node.parent.color = uncle.color = BLACK
gp.color = RED
node = gp
:
node node.parent.left:
node = node.parent
.rotate_right(node)
node.parent.color = BLACK
gp.color = RED
.rotate_left(gp)
.root.color = BLACK
():
current = .root
current .NIL:
data == current.data:
current
current = current.left data < current.data current.right
():
result = []
():
n .NIL:
_walk(n.left)
result.append(n.data)
_walk(n.right)
_walk(.root)
result
():
node = .root node node
node .NIL:
left_bh = .black_height(node.left)
left_bh + ( node.color == BLACK )
():
.root.color != BLACK:
,
():
node .NIL:
,
node.color == RED:
child (node.left, node.right):
child .NIL child.color == RED:
,
lh, lok = check(node.left)
rh, rok = check(node.right)
(lok rok) lh != rh:
,
lh + ( node.color == BLACK ),
_, ok = check(.root)
(ok, ok )
Insert Fix-Up Cases (parent is left child of grandparent)
| Case | Condition | Action |
|---|
| 1 | Uncle is RED | Recolor parent+uncle BLACK, grandparent RED, move up |
| 2 | Uncle BLACK, node is right child | Left-rotate parent (reduces to case 3) |
| 3 | Uncle BLACK, node is left child | Right-rotate grandparent, recolor |
Mirror cases apply when parent is right child.
Genomic Application: Ordered Variant Store + Interval Overlaps
Goal: index SNP/variant positions (or genomic intervals) so range queries stay O(log n) even though a VCF's positions arrive already sorted by chromosome coordinate.
Approach: use RedBlackTree.insert/inorder for sorted position storage; for interval overlap, key each node by interval start and scan candidates near the query using two intervals [a,b], [c,d] overlap iff a <= d and c <= b.
def insert_variants_and_verify(positions: list[int]) -> tuple[bool, str]:
"""Insert chromosomal SNP positions into a RedBlackTree and verify balance."""
tree = RedBlackTree()
for pos in positions:
tree.insert(pos)
return tree.verify_properties()
class IntervalStore:
"""Ordered store of genomic intervals (start, end), keyed by start position."""
def __init__(self):
self.rbt = RedBlackTree()
self._intervals: dict[int, list[tuple[int, int]]] = {}
def add_interval(self, start: int, end: int) -> None:
"""Insert the interval, keyed by its start coordinate."""
self.rbt.insert(start)
self._intervals.setdefault(start, []).append((start, end))
def find_overlaps(self, q_start: int, q_end: int) -> list[tuple[int, int]]:
"""Return all stored intervals overlapping [q_start, q_end]."""
overlaps = []
for start .rbt.inorder():
iv_start, iv_end ._intervals[start]:
iv_start <= q_end q_start <= iv_end:
overlaps.append((iv_start, iv_end))
overlaps
__name__ == :
variant_positions = [, , , , , , ]
ok, msg = insert_variants_and_verify(variant_positions)
ok, msg
store = IntervalStore()
iv [(, ), (, ), (, ), (, )]:
store.add_interval(*iv)
store.find_overlaps(, ) == [(, ), (, ), (, )]
()
Pitfalls
- NIL sentinel must be BLACK and shared — don't create new NIL nodes per operation.
- After insert fix-up, always force root to BLACK (a Case 1 recolor can turn it RED).
IntervalStore.find_overlaps above scans all starts (O(n) per query) — it's a teaching-simplified interval store, not a true augmented interval tree; for real workloads use an interval tree that also tracks max-end per subtree to prune the search to O(log n + k).
- Deletion fix-up is significantly more complex (4 cases + mirrors) — omitted here but follows the same rotation/recolor pattern as insert.
- Duplicate keys are silently ignored by
insert above; for multi-valued keys (e.g., multiple variants at the same position) store a list per key as IntervalStore does.
See Also
algo-binary-search-trees — the unbalanced BST this structure builds on
algo-avl-trees — the stricter-balance alternative (shorter trees, more rotations)
algo-hash-tables-bloom — O(1) average lookup when ordering isn't needed
bio-genome-intervals-interval-arithmetic — production-grade interval overlap operations