Red Black Tree
A red-black tree is a self-balancing binary search tree that uses color-coded nodes to maintain balance and ensure efficient search, insertion, and deletion operations.
Visualization
There are properties that a red-black tree must satisfy:
- Node Color: Every node is either red or black.
- Root: The root is black.
- Red Node Property: No two consecutive red nodes. In other words, if a node is red, then all of its children (if any) are black.
- Black height: For each node, all simple paths from the node to descendant null pointers contain the same number of black nodes (black height).
These properties ensure that the longest possible path (the one with alternating black and red nodes) is no more than twice as long as the shortest possible path (the one with only black nodes).
These properties ensure that with every node in the red-black tree, the height difference between the left and right sub-tree is no more than twice.
This guarantees that the worst-case time complexity for search, insertion, and deletion operations is O(log n), where n is the number of nodes in the tree.
Especially the no more than twice
property makes red-black tree very efficient in a high frequency of insertion and deletion operations,
because it only needs a little reconstruction, which is formed by many rotation steps.
Insertion
First, we insert the new node as we do in a regular Binary Search Tree, then color the newly inserted node red.
We call the new node K
, parent of the new node P
, grandparent of the new node G
, and uncle node U
(sibling of the parent node).
- If the new node is root, change its color to black and done.
- If the parent of the new node is black, the insertion is done.
- If the parent of the new node is red, we have to fix the above Property(3) violation.
- (3.1)
P
andU
are red: changeP
andU
into black,G
into red. Repeat withG
as the new node (repeat to fix the violation if needed upper in the tree). - (3.2)
P
is red andU
is black (or NULL), base onP
andK
is left or right child-
(3.2.1)
P
is right child ofG
,K
is right child ofP
- Left rotation at
G
. - Change
P
into black,G
into red.
- Left rotation at
-
(3.2.2)
P
is right child ofG
,K
is left child ofP
- Right rotation at
P
. - Back to case 3.2.1.
- Right rotation at
-
(3.2.3)
P
is left child ofG
,K
is left child ofP
(reverse of 3.2.1)- Right rotation at
G
. - Change
P
into black,G
into red.
- Right rotation at
-
(3.2.4)
P
is left child ofG
,K
is right child ofP
- Left rotation at
P
. - Back to case 3.2.3.
- Left rotation at
-
- (3.1)
Deletion
First, we follow steps of deletion in a regular binary search tree, bring to the case we will delete x
at the leaf or has only 1 child.
The idea of the algorithm is finding red nearby x
then trying to turn x
into red by applying rotation and color change.
Then we can simply remove red node x
without violating red-black tree properties.
We call the sibling of x
S
, parent of x
P
.
-
If
x
is red, simply removex
. -
If
x
is black and has only 1 child. This child should be red or the Property(4) above is violated.- Swapping color of
x
and its child. - We remove
x
by replacing it with its child.
- Swapping color of
-
If
x
is black and not case 2. We apply rotation and color changing to turnx
into red. To simplify we consider the casex
is left-child (in casex
is right-child we do the same steps but reverse the rotation)-
(3.1)
S
is black,S.right
is red. In this caseP
,S.left
are either red or black still ok.- Rotate left at
P
. - Make
S
color same toP
. - Make
P
,S.right
black. - Make
x
red.
- Rotate left at
-
(3.2)
S
is black,S.right
is black (or NULL),S.left
is red.- Make
S
red,S.left
black. - Rotate right at
S
. - Back to case 3.1.
- Make
-
(3.3)
S
is black, bothS
children are black.- (3.3.1)
P
is red.- Make
P
black. - Make
S
,x
red.
- Make
- (3.3.2)
P
is black.- The subtree we are checking all black, we repeat from the beginning of step 3 with mission to turn
P
into red (recursive call upper in the tree, withP
plays the newx
role). - After turn
P
into red, back to case 3.3.1.
- The subtree we are checking all black, we repeat from the beginning of step 3 with mission to turn
- (3.3.1)
-
(3.4)
S
is red. In this case, bothS
children are black according to Property 3.- Rotate left
P
. - Make
S
black,P
red. - Back to case 3.1, 3.2 or 3.3.1 (
x
black,P
red and newS
black).
- Rotate left
-
Sample code
export interface RedBlackNode {
value: number
color: 0 | 1
parent: RedBlackNode | null
left: RedBlackNode | null
right: RedBlackNode | null
}
function createNode(value: number): RedBlackNode {
return {
value,
color: 1,
parent: null,
left: null,
right: null
}
}
export class RedBlackTree {
root: RedBlackNode | null = null
insert(value: number) {
console.log('insert', value)
const k = insertNode(this.root, value)
if (!k) return
if (!this.root) {
this.root = k
}
this.fixInsert(k)
}
remove(value: number) {
const delNode = searchNode(this.root, value)
if (!delNode) return
// find actual removed node
let altNode: RedBlackNode | null = null
if (delNode.left && delNode.right) {
altNode = findMinNode(delNode.right)
}
const xNode = altNode || delNode
// fix the node to be deleted
this.fixDelete(xNode)
// override delnode if different, then delete
delNode.value = xNode.value
this.deleteNode(xNode)
}
private rotateLeft(x: RedBlackNode): void {
if (!x) return
const p = x.parent
const y = x.right
if (!y) return
const b = y.left
// update b
if (b) b.parent = x
// update x
x.parent = y
x.right = b
// update y
y.parent = p
y.left = x
// update p
if (p) {
// not rotate root
if (p.left === x) {
p.left = y
} else {
p.right = y
}
} else {
// rotate root
this.root = y
}
}
private rotateRight(y: RedBlackNode): void {
if (!y) return
const p = y.parent
const x = y.left
if (!x) return
const b = x.right
// update b
if (b) b.parent = y
// update y
y.parent = x
y.left = b
// update x
x.parent = p
x.right = y
// update p
if (p) {
// not rotate root
if (p.left === y) {
p.left = x
} else {
p.right = x
}
} else {
// rotate root
this.root = x
}
}
private deleteNode(node: RedBlackNode) {
if (!node) return
if (node.left && node.right) return
const child = node.left || node.right
const p = node.parent
if (p) {
if (p.left === node) {
p.left = child
} else {
p.right = child
}
if (child) {
child.parent = p
}
} else {
this.root = child
if (child) {
child.parent = null
}
}
}
// fix 2 consecutive red nodes (mainly)
private fixInsert(k: RedBlackNode) {
const p = k.parent
if (!p) {
// root node, make it black
k.color = 0
return
}
const isNodeLeft = p.left === k
if (!isRed(p)) {
// Case 2: nothing to do if parent is black
return
}
const g = p.parent
if (!g) return
const isParentLeft = g.left === p
const u = isParentLeft ? g.right : g.left
if (u && isRed(u)) {
p.color = 0
u.color = 0
g.color = 1
this.fixInsert(g)
return
}
// u is black
if (!isParentLeft) {
// p is right child
if (!isNodeLeft) {
// node is right child
this.rotateLeft(g)
p.color = 0
g.color = 1
} else {
// node is left child
this.rotateRight(p)
// repeat the above fix
this.fixInsert(p)
}
} else {
// p is left child
if (isNodeLeft) {
// node is left
this.rotateRight(g)
p.color = 0
g.color = 1
} else {
this.rotateLeft(p)
this.fixInsert(p)
}
}
}
// try to turn the node into red
private fixDelete(x: RedBlackNode) {
console.log('fix_Delete', x)
if (isRed(x)) {
return
}
// only 1 red child node
if (!x.left || !x.right) {
const child = x.left || x.right
if (child && isRed(child)) {
x.color = 1
child.color = 0
return
}
}
// x's children are black (possibly NULL)
const p = x.parent
// if x is root change color to red
// (to remove or swap again after recursive call)
if (!p) {
// TODO document this case
x.color = 1
return
}
const isNodeLeft = p.left === x
// sibling
if (isNodeLeft) {
const s = p.right
// this case not exist (break attribute 5)
if (!s) return
// s is black
if (!isRed(s)) {
if (s.right && isRed(s.right)) {
// 3.1, s.right is red
this.rotateLeft(p)
s.color = p.color
p.color = 0
s.right.color = 0
x.color = 1 // done
} else if (s.left && isRed(s.left)) {
// 3.2, s.left is red
s.color = 1
s.left.color = 0
this.rotateRight(s)
// reduce to 3.1
this.fixDelete(x)
} else {
// 3.3, both s's children are black
if (isRed(p)) {
// 3.3.1
p.color = 0
s.color = 1
x.color = 1
} else {
// 3.3.2
// TODO check if P root do {...}
this.fixDelete(p)
// changed p into red, reduce to 3.3.1
this.fixDelete(x)
}
}
} else {
// 3.4
this.rotateLeft(p)
s.color = 0
p.color = 1
// reduce to 3.1, 3.2 or 3.3.1
this.fixDelete(x)
}
} else {
// mirror of above 3.* cases
const s = p.left
// this case not exist (break attribute 5)
if (!s) return
// s is black
if (!isRed(s)) {
if (s.left && isRed(s.left)) {
// mirror 3.1, s.left is red
this.rotateRight(p)
s.color = p.color
p.color = 0
s.left.color = 0
x.color = 1 // done
} else if (s.right && isRed(s.right)) {
// mirror 3.2, s.right is red
this.rotateLeft(s)
s.color = 1
s.right.color = 0
// reduce to mirror 3.1
this.fixDelete(x)
} else {
// mirror 3.3, both s's children are black
if (isRed(p)) {
// mirror 3.3.1
p.color = 0
s.color = 1
x.color = 1
} else {
// mirror 3.3.2
this.fixDelete(p)
// changed p into red, reduce to mirror 3.3.1
this.fixDelete(x)
}
}
} else {
// mirror 3.4
this.rotateRight(p)
s.color = 0
p.color = 1
// reduce to mirror 3.1, 3.2 or 3.3.1
this.fixDelete(x)
}
}
}
}
function insertNode(node: RedBlackNode | null, value: number): RedBlackNode | null {
if (!node) {
return createNode(value)
}
// no insert duplicate value
if (node.value === value) return null
const isGoLeft = value < node.value
const next = isGoLeft ? node.left : node.right
const k = insertNode(next, value)
if (!k) return null
if (!next) {
// just created k
k.parent = node
if (isGoLeft) {
node.left = k
} else {
node.right = k
}
}
return k
}
function isRed(node: RedBlackNode | null): boolean {
return node ? node.color === 1 : false
}
function searchNode(node: RedBlackNode | null, value: number): RedBlackNode | null {
if (!node) return null
if (node.value === value) return node
if (value < node.value) {
return searchNode(node.left, value)
}
return searchNode(node.right, value)
}
function findMinNode(node: RedBlackNode): RedBlackNode {
let n = node
while (n.left) n = n.left
return n
}