Skip to content

Implement Red-Black Tree in Python - Guide for Technical Coding Interview

Updated: at 05:13 AM

A red-black tree is a self-balancing binary search tree where each node has an extra bit representing color, either red or black. By constraining how nodes are colored during insertions and deletions, red-black trees ensure the tree remains approximately balanced during all operations, allowing for O(log n) search, insert, and delete.

Red-black trees are important data structures widely used in languages like Java and C++. Here we will examine how to implement insertion, deletion, and balancing logic to construct a fully functional red-black tree in Python.

Table of Contents

Open Table of Contents

Overview of Red-Black Trees

Red-black trees are binary search trees that add the extra constraint of node coloring in order to ensure the tree remains balanced during operations. Here are some key properties of red-black trees:

By constraining coloring during insertions and deletions, red-black trees ensure the maximum height of the tree (number of black nodes on any root-to-leaf path) does not exceed 2log(n+1) where n is the number of nodes. This guarantees O(log n) worst-case performance for search, insert, and delete operations.

The balance of red-black trees comes from how coloring changes during insertions and deletions. Specific balancing rules are followed that require certain coloring operations and tree rotations to retain the black-height property of the tree.

Red-Black Tree Node Implementation

We will implement a red-black tree node class that extends the typical binary search tree node structure by adding a color attribute:

class RBNode:
  def __init__(self, value, color='red'):
    self.value = value
    self.color = color
    self.left = None
    self.right = None
    self.parent = None

Each node contains the standard value, left, and right attributes to represent the data value and links to children nodes.

The color attribute tracks if the node is ‘red’ or ‘black’. New nodes added during insertion are colored red by default.

The parent attribute is helpful for tracking parent nodes when doing rotations and color changes.

Implementing Red-Black Tree Insertion

We will first examine the algorithm for inserting new nodes into the red-black tree. This must follow specific rules to ensure the tree remains balanced.

The steps for insertion are:

  1. Perform standard BST insertion, adding the node as a red leaf node.
  2. Fix red-black properties if needed through color changes and rotations.

Standard BST Insertion

Insertion begins by adding the new node with a red color as a standard BST insertion:

def insert(self, value):
  # Regular BST insert
  new_node = RBNode(value)
  if self.root is None:
    # Handle empty tree
    self.root = new_node
  else:
    curr_node = self.root
    while True:
      if value < curr_node.value:
        if curr_node.left is None:
          curr_node.left = new_node
          new_node.parent = curr_node
          break
        else:
          curr_node = curr_node.left
      else:
        if curr_node.right is None:
          curr_node.right = new_node
          new_node.parent = curr_node
          break
        else:
          curr_node = curr_node.right

This follows the standard recursive BST insertion logic, descending the tree and adding the new node with the given value once we reach a leaf node.

The new node is initially added with a red color.

Fixing Red-Black Properties

After regular BST insertion, we may have violated red-black tree properties. There are two main red-black tree invariants that must be maintained after insertion:

  1. No red node can have a red child (so red nodes must have black children).
  2. All paths from a node to descendant leaves must contain the same number of black nodes.

There are specific cases that can occur after adding a red child that violate these invariants:

Case 1: If the parent is black, tree is still valid.

If the parent of the new red node is black, both properties are satisfied. Nothing needs to be done since rules are not violated.

Case 2: If parent is red, we have a red violation.

If the parent is red, this means there is now a red node with a red child, violating property 1. To fix this, there are subcases:

The full fixing logic including handling these cases is shown below:

def insert_fix(self, new_node):
  # Fix red-black properties if needed
  parent = new_node.parent

  # Case 1: If parent is black, tree is still valid
  if parent is None or parent.color == 'black':
    return

  # Case 2: If parent is red, have to handle violations
  else:
    if new_node.uncle() is not None and new_node.uncle().color == 'red':
      # Case 2a: Uncle is red, recolor parent, grandparent, uncle
      new_node.parent.color = 'black'
      new_node.uncle().color = 'black'
      new_node.grandparent().color = 'red'

      # Recurse up the tree
      self.insert_fix(new_node.grandparent())

    else:
      # Case 2b: Uncle is black (or null), restructure tree
      grandparent = new_node.grandparent()

      # New node is left child of parent, parent is left of grandparent
      if new_node == new_node.parent.left:
        if new_node.parent == grandparent.left:
          # Left-left case
          self.rotate_right(grandparent)
          new_node.parent.color = 'black'
          grandparent.color = 'red'

        else:
          # Left-right case
          self.rotate_left(new_node.parent)
          self.rotate_right(grandparent)
          new_node.color = 'black'
          grandparent.color = 'red'

      # Mirror cases: parent/new node on right
      else:
        if new_node.parent == grandparent.right:
          # Right-right case
          self.rotate_left(grandparent)
          new_node.parent.color = 'black'
          grandparent.color = 'red'

        else:
          # Right-left case
          self.rotate_right(new_node.parent)
          self.rotate_left(grandparent)
          new_node.color = 'black'
          grandparent.color = 'red'

This covers all cases that can occur after adding a red child node, ensuring red-black properties are restored through rotations and recoloring.

The full insert method handles both standard insertion and fixing:

def insert(self, value):
  # Regular BST insert
  new_node = RBNode(value)
  # Code for standard insertion...

  # Fix red-black properties
  self.insert_fix(new_node)

Rotations for Balancing

The insertion fixing logic requires left and right rotations to restructure the tree in certain cases. These operations change the position of nodes to move the red violation upward while maintaining the BST ordering, similar to AVL tree rotations.

Here is an implementation of left and right rotations on a given node:

def rotate_left(self, node):
  right_child = node.right
  node.right = right_child.left

  if right_child.left is not None:
    right_child.left.parent = node

  right_child.parent = node.parent

  if node.parent is None:
    # Node is root
    self.root = right_child
  elif node == node.parent.left:
    node.parent.left = right_child
  else:
    node.parent.right = right_child

  right_child.left = node
  node.parent = right_child


def rotate_right(self, node):
  # Mirror of rotate left
  left_child = node.left

  node.left = left_child.right

  if left_child.right is not None:
    left_child.right.parent = node

  left_child.parent = node.parent

  if node.parent is None:
    self.root = left_child
  elif node == node.parent.right:
    node.parent.right = left_child
  else:
    node.parent.left = left_child

  left_child.right = node
  node.parent = left_child

These rotations restructure the nodes such that the rotated node becomes a child while its child becomes the new parent. This moves a red violation upward in the tree.

Deletion in a Red-Black Tree

Implementing deletion in a red-black tree also requires maintaining the coloring invariants after removing nodes.

The steps for deletion are:

  1. Perform standard BST deletion logic of removing the node and restructuring children.
  2. Fix any red-black violations through rotations and recoloring.

Standard BST Deletion

We first implement the standard BST node removal process:

def delete(self, value):
  node_to_remove = self.search(value)

  if node_to_remove is None:
    # Value not found, no deletion needed
    return

  if node_to_remove.left is None:
    self._replace_node(node_to_remove, node_to_remove.right)
  elif node_to_remove.right is None:
    self._replace_node(node_to_remove, node_to_remove.left)
  else:
    # Node has two children
    # Find successor (leftmost of right subtree)
    successor = self._find_min(node_to_remove.right)
    successor_value = successor.value
    self._replace_node(successor, successor.right)
    node_to_remove.value = successor_value

  # Fix red-black properties if needed
  self.delete_fix(node_to_remove)

This follows the same logic as a standard BST deletion. If the node is a leaf, it can simply be removed. If it has one child, that child replaces it. If it has two children, we find its successor in the right subtree and swap their values before deleting the successor leaf node instead.

After removal, we check if red-black properties were violated and call fix-up logic.

Red-Black Tree Fixing After Deletion

Similar to insertion, a deletion can break the red-black tree invariants. There are two main cases:

Case 1: Deleted node was black

If the deleted node was black, removing it caused black-heights of paths to diverge, since other paths did not have their black node count reduced.

To balance this, we check the color of the deleted node’s child. If it is red, recolor it black to maintain black heights. If it is black, more complex fixes are needed:

Case 2: Deleted node was red

If the deleted node was red, since red nodes do not add to the black height of paths we know heights remain valid.

Here is the full deletion fixup logic:

def delete_fix(self, x):

  # Start fixes from parent since x is gone
  x_parent = x.parent

  if x.color == 'red':
    # Case 2: If x was red, tree is still valid
    return

  if x_parent is None:
    # Case 1: x was black, but was root
    # Set new root to black
    self.root.color = 'black'
    return

  # Case 1: x was black
  if x.sibling().color == 'red':
    # Case 1a: Sibling is red
    x_parent.color = 'red'
    x.sibling().color = 'black'
    if x == x_parent.left:
      self.rotate_left(x_parent)
    else:
      self.rotate_right(x_parent)

  # Fix double black at x_parent
  self.delete_fix(x_parent)

def delete_fix(self, x):

  if x.color == 'red':
    # Color flip
    x.color = 'black'
    return

  if x.parent is None:
    # Reached root, all black-heights fixed
    return

  if x.sibling().color == 'black':
    # Case 1b and 1c
    if (x.sibling().left is not None and x.sibling().left.color == 'red') or (x.sibling().right is not None and x.sibling().right.color == 'red'):
      # Case 1c: At least 1 red child
      if x.sibling().left is not None and x.sibling().left.color == 'red':
        if x == x.parent.left:
          self.rotate_right(x.sibling())
          x.sibling().color = x.parent.color
          x.parent.color = 'black'
        else:
          x.sibling().left.color = x.parent.color
          x.parent.color = 'black'
          self.rotate_right(x.sibling())
          self.rotate_left(x.parent)
      else:
        if x == x.parent.left:
          x.sibling().right.color = x.parent.color
          x.parent.color = 'black'
          self.rotate_left(x.sibling())
          self.rotate_right(x.parent)
        else:
          self.rotate_left(x.sibling())
          x.sibling().color = x.parent.color
          x.parent.color = 'black'
    else:
      # Case 1b: All black children
      x.sibling().color = 'red'
      if x.parent.color == 'black':
        self.delete_fix(x.parent)
      else:
        x.parent.color = 'black'

  # Pass double black node up tree
  self.delete_fix(x.parent)

This comprehensive fixing logic handles all cases after deletion to restore red-black properties. The tree remains balanced after any insertion or deletion.

Full Deletion Method

Combining the standard deletion and fixing logic, the full delete method is:

def delete(self, value):

  # Standard deletion
  node_to_remove = self.search(value)
  # Delete node cases

  # Fix red-black properties
  self.delete_fix(node_to_remove)

Complete Red-Black Tree in Python

Putting everything together, here is an implementation of a complete functional red-black tree in Python:

import sys

class RBNode:
  # Red-Black Tree Node class

  def __init__(self, value, color='red'):
    ...

  # Required helper methods and properties
  def grandparent(self): ...

  def sibling(self): ...

  def uncle(self): ...


class RedBlackTree:

  def __init__(self):
    self.root = None

  def search(self, value):
    # Search logic

  def insert(self, value):
    # Regular insertion
    new_node = RBNode(value)
    # Insert fixing
    self.insert_fix(new_node)

  def insert_fix(self, new_node):
    # Full insert fix logic

  def delete(self, value):
    # Standard deletion
    node_to_remove = self.search(value)
    # Delete node cases

    # Fix red-black properties
    self.delete_fix(node_to_remove)

  def delete_fix(self, x):
    # Full delete fix logic

  # Rotation methods
  def rotate_left(self, node):
    ...

  def rotate_right(self, node):
    ...

This provides a full implementation of a red-black tree data structure in Python. The key additions to a standard BST are the color tracking, the insertion fix and deletion fix logic, and the left/right rotations used to rebalance during insertions and deletions.

Applications of Red-Black Trees

Red-black trees are used extensively in various systems and libraries where balanced search trees are required. Some example applications include:

For these applications, red-black trees provide optimal O(log n) performance for search, insert, and delete while maintaining balanced tree height through their unique coloring algorithm. This makes them one of the most effective balanced binary search tree structures.