Red-Black Tree
The Red-Black Tree (RBT) is a lazily-structured "self-healing" tree.
A Red-Black Tree is a self-balancing binary search tree (BST) that ensures efficient operations for insertion, deletion, and lookup in time. It achieves balance through color-coding and specific rules that restrict the tree’s shape.
RBT Properties
A valid Red-Black Tree must have the following properties:
- Every node is either red or black.
- The root node is always black.
- Every leaf (
nil
) is always black. - Red nodes cannot have red children (no two consecutive red nodes).
- For any node
x
, every descending path fromx
to a leaf (nil
) contains the same number of black nodes.- This ensures balance by controlling the depth.
Furthermore:
- Key-bearing nodes (non-
nil
) are considered internal nodes. - Each red node has two black children (where one or both can be
nil
). - To maintain a valid black height (BH), red nodes must be inserted along different-length paths.
Why Use a Red-Black Tree?
- Keeps the tree approximately balanced, ensuring operations (insertion, deletion, and search).
- Unlike an AVL Tree, which is more strictly balanced, RBTs require fewer rotations during insertion and deletion.
Fun Fact: RBTs are used to implement data structures like std::map
and std::set
.
Red-Black Trees with n
internal nodes (non-leaf, non-nil
) and height h
satisfy the following inequality:
where the height of the tree is its longest depth, meaning it determines the worst-case runtime.
Remarks:
- Any binary tree satisfies .
- A Red-Black Tree satisfies .
- The height of the tree .
- Therefore, BST operations on these trees have a cost of (balanced tree).
Main Operations
The main operations in a Red-Black Tree are as follows:
- Insertion
- Deletion
- Rotation
Each operation can optionally use helper functions such as insertFixup()
, deleteFixup()
, or transplant()
.
Insertion
For insertion, we use two functions: insert()
and insertFixup()
(a helper function).
Inserting a node into an RBT is very similar to inserting into a plain Binary Search Tree (BST). However, in an RBT, a regular insertion will likely violate RBT properties.
To fix this, a helper function ensures the tree’s integrity.
General Idea:
- Insert the node like a normal BST.
- Color the new node red.
- Fix violations using rotations and recoloring to restore the RBT properties.
Insertion Cases:
Given node z
:
z
is theroot
.z.uncle
is coloredRED
.z.uncle
is coloredBLACK
and forms a triangle (more on this later).z.uncle
is coloredBLACK
and forms a line (more on this later).
Since inserting a node into an RBT is very similar to inserting in a regular BST, let’s look at the pseudocode:
insert(T, z):
y = nil
x = root
while x != nil
y = x
if z.key < x.key
x = x.left
else
x = x.right
z.parent = y
if y == nil
root = z
else if z.key < y.key
y.left = z
else
y.right = z
z.left = nil
z.right = nil
z.color = RED
insertFixup(T, z)
The main changes are the recoloring of the new node and the final instruction calling insertFixup()
.
Now, let’s look at the helper function:
insertFixup(T, z):
while z.parent != nil && z.parent.color == RED
if z.parent == z.parent.parent.left
y = z.parent.parent.right
if y.color == RED // Case 2
z.parent.color = BLACK
y.color = BLACK
z.parent.parent.color = RED
z = z.parent.parent
else
if z == z.parent.right // Case 3
z = z.parent
leftRotate(T, z)
// Case 4
z.parent.color = BLACK
z.parent.parent.color = RED
rightRotate(T, z.parent.parent)
else
y = z.parent.parent.left
if y.color == RED // Case 2
z.parent.color = BLACK
y.color = BLACK
z.parent.parent.color = RED
z = z.parent.parent
else
if z == z.parent.left // Case 3
z = z.parent
rightRotate(T, z)
// Case 4
z.parent.color = BLACK
z.parent.parent.color = RED
leftRotate(T, z.parent.parent)
root.color = BLACK
Deletion
Deleting a node from a Red-Black Tree is not as straightforward as insertion.
The main idea is:
- Transplant (replaces subtrees efficiently).
- Delete (removes the node).
- DeleteFixup (fixes any violations).
Delete Cases:
- Node
z
has no children → simply remove it. - Node has one child → use
transplant()
to replace it with its child. - Node has two children → find the successor (smallest in right subtree) and use
transplant()
to replacez
with the successor.
At the end, we call deleteFixup()
to restore RBT properties.
delete(T, k):
z = find(k)
y = z
y_og_color = y.color
if z.left == nil // Case 1
x = z.right
transplant(T, z, z.right)
else if z.right == nil // Case 2
x = z.left
transplant(T, z, z.left)
else // Case 3
y = min(z.right)
y_og_color = y.color
x = y.right
if y.parent == z
x.parent = y
else
transplant(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_og_color == BLACK
deleteFixup(T, x)
Transplant
The transplant()
function replaces one subtree with another while maintaining tree structure. It is commonly used during deletion.
Why use transplant()
?
- It simplifies handling deletion cases.
- It efficiently replaces nodes without manually updating multiple pointers.
How transplant()
works:
- Replace one node
u
with another nodev
. - Update
u
’s parent to point tov
.
transplant(T, u, v):
if u.parent == nil // u is root
root = v
else if u == u.parent.left // u is a left child
u.parent.left = v
else // u is a right child
u.parent.right = v
v.parent = u.parent
Delete Fixup
deleteFixup()
rebalances the tree after deletion. It handles four cases:
w
isRED
.w
isBLACK
, and both its children areBLACK
.w
isBLACK
, its left child isRED
, and its right child isBLACK
.w
isBLACK
, and its right child isRED
.
deleteFixup(T, x):
while x != root and x.color == BLACK
if x == x.parent.left
w = x.parent.right
if w.color == RED // Case 1
w.color = BLACK
x.parent.color = RED
leftRotate(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
w.left.color = BLACK
w.color = RED
And just like that, you should now be familiar with the basics of the Red-Black Tree structure and properties!
If you want to see it in action my source code is avaliable here.