Published on

Red-Black Trees - Self-Balancing Binary Search Trees

Authors
  • avatar
    Name
    Balaram Shiwakoti
    Twitter

Red-Black trees were initially the most complex data structure I encountered during DSA preparation. I kept thinking, "Why do we need colors on tree nodes?" But once I understood that the color properties ensure the tree stays balanced and guarantee O(log n) operations, the elegance of the design became clear. Let me share what I've learned about these sophisticated self-balancing trees.

Introduction to Red-Black Trees

When I first learned about Red-Black trees, I was overwhelmed by the color rules and rotation cases. But then I discovered they're used in many real-world systems - from C++ STL maps to Linux kernel schedulers - because they provide excellent worst-case guarantees with relatively simple operations.

Red-Black Tree is a self-balancing binary search tree where each node has a color (red or black) and follows specific color properties to maintain balance. They guarantee O(log n) time for search, insertion, and deletion operations.

Red-Black Tree Properties

Five Fundamental Properties

  1. Every node is either red or black
  2. The root is always black
  3. All leaves (NIL nodes) are black
  4. Red nodes cannot have red children (no two red nodes adjacent)
  5. Every path from root to leaf contains the same number of black nodes

Key Insights

Property 4 + 5 together ensure:

  • No path is more than twice as long as any other path
  • Tree height is at most 2 × log₂(n + 1)
  • Guarantees O(log n) operations

Example Red-Black Tree:

       B(7)
      /    \
   R(3)    R(18)
   /  \    /   \
B(1) B(6) B(10) B(22)
          /  \    \
       R(8) R(11) R(26)

Verification:

  • Root is black ✓
  • No red-red parent-child pairs ✓
  • All paths have same black height (3) ✓

I remember spending hours verifying these properties on different trees until they became intuitive!

Node Structure and Implementation

Node Definition

class RBNode:
    def __init__(self, key, color='RED'):
        self.key = key
        self.color = color  # 'RED' or 'BLACK'
        self.left = None
        self.right = None
        self.parent = None

class RedBlackTree:
    def __init__(self):
        # Create sentinel NIL node
        self.NIL = RBNode(None, 'BLACK')
        self.root = self.NIL
    
    def is_red(self, node):
        """Check if node is red (NIL nodes are black)"""
        return node != self.NIL and node.color == 'RED'
    
    def is_black(self, node):
        """Check if node is black"""
        return node == self.NIL or node.color == 'BLACK'

Why NIL Nodes?

NIL (sentinel) nodes simplify implementation:

  • All leaf pointers point to same NIL node
  • NIL is always black (satisfies property 3)
  • Eliminates special cases in rotations
  • Makes code cleaner and less error-prone

Rotations in Red-Black Trees

Left Rotation

def left_rotate(self, x):
    """Rotate left around node 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

Right Rotation

def right_rotate(self, y):
    """Rotate right around node 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.left:
        y.parent.left = x
    else:
        y.parent.right = x
    
    x.right = y
    y.parent = x

Visual representation:

Left Rotation:          Right Rotation:
     x                       y
    / \                     / \
   a   y        <-->       x   c
      / \                 / \
     b   c               a   b

Insertion Operation

Insertion Algorithm

Steps:

  1. Insert node using standard BST insertion (color it red)
  2. Fix any Red-Black property violations
  3. Restore properties using rotations and recoloring
def insert(self, key):
    """Insert key into Red-Black tree"""
    # Create new red node
    new_node = RBNode(key, 'RED')
    new_node.left = self.NIL
    new_node.right = self.NIL
    
    # Standard BST insertion
    parent = self.NIL
    current = self.root
    
    while current != self.NIL:
        parent = current
        if new_node.key < current.key:
            current = current.left
        else:
            current = current.right
    
    new_node.parent = parent
    
    if parent == self.NIL:
        self.root = new_node
    elif new_node.key < parent.key:
        parent.left = new_node
    else:
        parent.right = new_node
    
    # Fix Red-Black properties
    self.insert_fixup(new_node)

def insert_fixup(self, node):
    """Fix Red-Black properties after insertion"""
    while self.is_red(node.parent):
        if node.parent == node.parent.parent.left:
            uncle = node.parent.parent.right
            
            if self.is_red(uncle):
                # Case 1: Uncle is red
                node.parent.color = 'BLACK'
                uncle.color = 'BLACK'
                node.parent.parent.color = 'RED'
                node = node.parent.parent
            else:
                if node == node.parent.right:
                    # Case 2: Uncle is black, node is right child
                    node = node.parent
                    self.left_rotate(node)
                
                # Case 3: Uncle is black, node is left child
                node.parent.color = 'BLACK'
                node.parent.parent.color = 'RED'
                self.right_rotate(node.parent.parent)
        else:
            # Symmetric cases (parent is right child)
            uncle = node.parent.parent.left
            
            if self.is_red(uncle):
                # Case 1: Uncle is red
                node.parent.color = 'BLACK'
                uncle.color = 'BLACK'
                node.parent.parent.color = 'RED'
                node = node.parent.parent
            else:
                if node == node.parent.left:
                    # Case 2: Uncle is black, node is left child
                    node = node.parent
                    self.right_rotate(node)
                
                # Case 3: Uncle is black, node is right child
                node.parent.color = 'BLACK'
                node.parent.parent.color = 'RED'
                self.left_rotate(node.parent.parent)
    
    # Root must always be black
    self.root.color = 'BLACK'

Insertion Cases Explained

Case 1: Uncle is Red

Before:        After:
    B             R
   / \           / \
  R   RB   B
 /             /
R             R
  • Recolor parent, uncle, and grandparent
  • Move problem up the tree

Case 2: Uncle is Black, Node is "Inside"

Before:        After:
    B             B
   / \           / \
  R   BR   B
   \           /
    R         R
  • Rotate to convert to Case 3
  • Then apply Case 3 fix

Case 3: Uncle is Black, Node is "Outside"

Before:        After:
    B             R
   / \           / \
  R   BB   B
 /
R
  • Rotate and recolor
  • Problem solved

Deletion Operation

Deletion is more complex than insertion due to multiple cases:

def delete(self, key):
    """Delete key from Red-Black tree"""
    node = self.search(key)
    if node == self.NIL:
        return False
    
    self.delete_node(node)
    return True

def delete_node(self, node):
    """Delete given node from tree"""
    original_color = node.color
    
    if node.left == self.NIL:
        # Case 1: No left child
        replacement = node.right
        self.transplant(node, node.right)
    elif node.right == self.NIL:
        # Case 2: No right child
        replacement = node.left
        self.transplant(node, node.left)
    else:
        # Case 3: Two children
        successor = self.minimum(node.right)
        original_color = successor.color
        replacement = successor.right
        
        if successor.parent == node:
            replacement.parent = successor
        else:
            self.transplant(successor, successor.right)
            successor.right = node.right
            successor.right.parent = successor
        
        self.transplant(node, successor)
        successor.left = node.left
        successor.left.parent = successor
        successor.color = node.color
    
    # Fix Red-Black properties if black node was deleted
    if original_color == 'BLACK':
        self.delete_fixup(replacement)

def delete_fixup(self, node):
    """Fix Red-Black properties after deletion"""
    while node != self.root and self.is_black(node):
        if node == node.parent.left:
            sibling = node.parent.right
            
            if self.is_red(sibling):
                # Case 1: Sibling is red
                sibling.color = 'BLACK'
                node.parent.color = 'RED'
                self.left_rotate(node.parent)
                sibling = node.parent.right
            
            if (self.is_black(sibling.left) and 
                self.is_black(sibling.right)):
                # Case 2: Sibling's children are black
                sibling.color = 'RED'
                node = node.parent
            else:
                if self.is_black(sibling.right):
                    # Case 3: Sibling's left child is red
                    sibling.left.color = 'BLACK'
                    sibling.color = 'RED'
                    self.right_rotate(sibling)
                    sibling = node.parent.right
                
                # Case 4: Sibling's right child is red
                sibling.color = node.parent.color
                node.parent.color = 'BLACK'
                sibling.right.color = 'BLACK'
                self.left_rotate(node.parent)
                node = self.root
        else:
            # Symmetric cases (node is right child)
            # Similar logic with left/right swapped
            pass
    
    node.color = 'BLACK'

def transplant(self, u, v):
    """Replace subtree rooted at u with subtree rooted at 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

Search Operation

def search(self, key):
    """Search for key in tree"""
    current = self.root
    
    while current != self.NIL and key != current.key:
        if key < current.key:
            current = current.left
        else:
            current = current.right
    
    return current

def minimum(self, node):
    """Find minimum key in subtree"""
    while node.left != self.NIL:
        node = node.left
    return node

def maximum(self, node):
    """Find maximum key in subtree"""
    while node.right != self.NIL:
        node = node.right
    return node

Complete Implementation Example

class RedBlackTree:
    def __init__(self):
        self.NIL = RBNode(None, 'BLACK')
        self.root = self.NIL
    
    def insert(self, key):
        """Public interface for insertion"""
        new_node = RBNode(key, 'RED')
        new_node.left = self.NIL
        new_node.right = self.NIL
        
        # BST insertion
        parent = self.NIL
        current = self.root
        
        while current != self.NIL:
            parent = current
            if new_node.key < current.key:
                current = current.left
            else:
                current = current.right
        
        new_node.parent = parent
        
        if parent == self.NIL:
            self.root = new_node
        elif new_node.key < parent.key:
            parent.left = new_node
        else:
            parent.right = new_node
        
        self.insert_fixup(new_node)
    
    def inorder_traversal(self):
        """Return sorted list of keys"""
        result = []
        self._inorder_helper(self.root, result)
        return result
    
    def _inorder_helper(self, node, result):
        if node != self.NIL:
            self._inorder_helper(node.left, result)
            result.append(node.key)
            self._inorder_helper(node.right, result)
    
    def validate(self):
        """Validate Red-Black tree properties"""
        return (self._check_property_1() and
                self._check_property_2() and
                self._check_property_4() and
                self._check_property_5()[0])
    
    def _check_property_1(self):
        """Every node is red or black"""
        return self._check_colors(self.root)
    
    def _check_colors(self, node):
        if node == self.NIL:
            return True
        
        if node.color not in ['RED', 'BLACK']:
            return False
        
        return (self._check_colors(node.left) and
                self._check_colors(node.right))
    
    def _check_property_2(self):
        """Root is black"""
        return self.root == self.NIL or self.root.color == 'BLACK'
    
    def _check_property_4(self):
        """No red node has red child"""
        return self._check_red_red(self.root)
    
    def _check_red_red(self, node):
        if node == self.NIL:
            return True
        
        if (node.color == 'RED' and
            (self.is_red(node.left) or self.is_red(node.right))):
            return False
        
        return (self._check_red_red(node.left) and
                self._check_red_red(node.right))
    
    def _check_property_5(self):
        """All paths have same black height"""
        return self._check_black_height(self.root)
    
    def _check_black_height(self, node):
        if node == self.NIL:
            return True, 1
        
        left_valid, left_height = self._check_black_height(node.left)
        right_valid, right_height = self._check_black_height(node.right)
        
        if not left_valid or not right_valid or left_height != right_height:
            return False, 0
        
        height = left_height
        if node.color == 'BLACK':
            height += 1
        
        return True, height

# Example usage
rbt = RedBlackTree()
keys = [7, 3, 18, 10, 22, 8, 11, 26, 2, 6, 13]

for key in keys:
    rbt.insert(key)
    print(f"Inserted {key}, valid: {rbt.validate()}")

print(f"Inorder traversal: {rbt.inorder_traversal()}")

Time and Space Complexity

Time Complexity

OperationAverage CaseWorst Case
SearchO(log n)O(log n)
InsertO(log n)O(log n)
DeleteO(log n)O(log n)

Guaranteed O(log n) due to height bound: h ≤ 2 log₂(n + 1)

Space Complexity

  • Storage: O(n) for n nodes
  • Each node: Constant extra space for color and parent pointer
  • Recursion: O(log n) for operations

Comparison with Other Trees

Red-Black vs AVL Trees

AspectRed-BlackAVL
BalanceLooser (height ≤ 2 log n)Stricter (height ≤ 1.44 log n)
RotationsFewer (≤ 3 per operation)More (O(log n) per operation)
SearchSlightly slowerFaster
Insert/DeleteFasterSlower
Memory1 bit per nodeInteger per node

Red-Black vs B-Trees

AspectRed-BlackB-Tree
Node degree2 (binary)m (multi-way)
Use caseIn-memoryExternal storage
Cache performanceGoodExcellent
ImplementationComplexVery complex

Applications of Red-Black Trees

Standard Libraries

C++ STL:

  • std::map and std::set
  • std::multimap and std::multiset
  • Guaranteed O(log n) operations

Java:

  • TreeMap and TreeSet
  • Sorted collections with predictable performance

Operating Systems

Linux Kernel:

  • Completely Fair Scheduler (CFS)
  • Virtual memory management
  • I/O scheduling

Process Scheduling:

  • Maintain runnable processes
  • O(log n) insertion and removal
  • Fair time allocation

Database Systems

Index Structures:

  • Alternative to B-trees for smaller datasets
  • In-memory index implementations
  • Range query support

Advanced Topics

Persistent Red-Black Trees

class PersistentRBNode:
    """Immutable Red-Black tree node"""
    def __init__(self, key, color, left=None, right=None):
        self.key = key
        self.color = color
        self.left = left or NIL
        self.right = right or NIL
    
    def with_color(self, new_color):
        """Return new node with different color"""
        return PersistentRBNode(self.key, new_color, self.left, self.right)
    
    def with_children(self, new_left, new_right):
        """Return new node with different children"""
        return PersistentRBNode(self.key, self.color, new_left, new_right)

Concurrent Red-Black Trees

Challenges:

  • Multiple threads accessing tree simultaneously
  • Maintaining consistency during rotations
  • Lock-free implementations

Solutions:

  • Fine-grained locking
  • Copy-on-write techniques
  • Lock-free algorithms

Common Implementation Pitfalls

1. Forgetting NIL Node Updates

# Wrong - doesn't update NIL parent
def wrong_rotate(self, x):
    y = x.right
    x.right = y.left
    # Missing: if y.left != self.NIL: y.left.parent = x

# Correct - always update parent pointers
def correct_rotate(self, x):
    y = x.right
    x.right = y.left
    if y.left != self.NIL:
        y.left.parent = x

2. Incorrect Color Handling

# Wrong - doesn't handle NIL color properly
def wrong_is_red(self, node):
    return node.color == 'RED'  # Crashes on NIL

# Correct - NIL nodes are always black
def correct_is_red(self, node):
    return node != self.NIL and node.color == 'RED'

3. Missing Root Color Fix

# Wrong - root might end up red
def wrong_insert_fixup(self, node):
    while self.is_red(node.parent):
        # ... fixup logic ...
    # Missing: self.root.color = 'BLACK'

# Correct - always ensure root is black
def correct_insert_fixup(self, node):
    while self.is_red(node.parent):
        # ... fixup logic ...
    self.root.color = 'BLACK'

Conclusion

Red-Black trees are sophisticated self-balancing data structures that provide excellent worst-case guarantees. Key takeaways:

Core Concepts:

  • Five color properties ensure balance
  • Height bounded by 2 log₂(n + 1)
  • Rotations and recoloring maintain properties

Performance:

  • Guaranteed O(log n) for all operations
  • Fewer rotations than AVL trees
  • Good practical performance

Applications:

  • Standard library implementations
  • Operating system components
  • Database indexing systems

For loksewa preparation, focus on:

  • Understanding the five Red-Black properties
  • Implementing basic insertion with fixup
  • Analyzing time and space complexity
  • Comparing with other balanced trees
  • Recognizing when Red-Black trees are appropriate

Remember: Red-Black trees demonstrate how simple rules (color properties) can ensure complex guarantees (balanced height). The key insight is that the color constraints prevent the tree from becoming too unbalanced while allowing efficient maintenance operations.

Understanding Red-Black trees provides insight into advanced data structure design and the trade-offs between different balancing strategies!