Working With Red-Black Trees In C#

Introduction 

 
Although binary search trees (BSTs) are used widely, as data gets added, binary search trees tend to degenerate into an unordered linked list over time. The "red-black tree" is a related type of binary tree that's often more suitable for certain types of search operations. This article discusses how red-black trees facilitate faster searches and become less disordered over time than similar data structures, and shows how to build and search a red-black tree in C#.

A tree is a data structure that represents hierarchical data by storing it in nodes that are related to one another in specific ways. The topmost node of a tree is called the root. It is the node from which all the other nodes of the tree descend. A tree can have one and only one root. Using the same logic, a tree should have at least one node—the root node. A BST is a special tree structure that ensures both that the tree nodes are ordered, and that each node contains a value. In a binary tree, each node may have only two children, called left and right internal nodes. For any given node, the left child node stores a value that is less than the current node's value, while the right child node's value is greater than the current node's value. Such trees have a "height," which you can calculate by counting the number of links from the root node to the deepest node (the one furthest away from the root) in the tree.
 
Figure 1. Simple Binary Search Tree: This nine-node tree illustrates the simple left-right decisions made when adding new nodes or searching the tree. (Image adapted from Wikipedia).
 
The tree shown in Figure 1 has nine nodes, and a height of three—the distance from the root node (8) to any of the nodes 4, 7, or 13. Note how all left nodes have values less than their parent nodes while the right nodes store values greater than their parents. The nodes that have no children are called "leaf nodes." In Figure 1, the nodes that correspond to the values 1, 4, 7, and 13 are all leaf nodes.

To search for a specific item in a binary tree, you start at the root node and repeatedly take either the left or right branch depending on whether the value that you are searching for is greater than or less than the value of the current node. Search operations in a binary tree take O(h) units of time, where, h represents the height of the tree. A BST can easily degenerate into an unbalanced list when added nodes fall disproportionately into one branch of the tree, making that branch far longer than others, thus making searches take longer for that branch than for others. According to MSDN, "The disadvantage of BSTs is that in the worst-case their asymptotic running time is reduced to linear time. This happens if the items inserted into the BST are inserted in order or in near-order. In such a case, a BST performs no better than an array." This is where red-black trees fit into the picture.

A red-black tree is an optimized version of a BST that adds a color attribute to each node. The value of this color attribute value is always either red or black. The root node is always black. In addition to color, each node contains a reference to its parent node, and its child nodes—left and right, as well as an optional key value. Red-black trees perform better than ordinary BST because they use the color attribute and the node references to maintain a better balance.

Red-black trees always follow these rules,
  • The root node of a red-black tree is always black.
  • The path from the root of the tree to any leaf node contains the same number of black nodes throughout the tree, also known as the "black-height" of the tree.
  • Both children of a red node are always black.
  • All "external" nodes—leaf nodes—are always colored black.
The maximum height of a red-black tree that contains n nodes is given by 2log(n+1). The time taken to search for any item in a red-black tree follows the formula O(log n), which implies that it is a good choice for search operations. Figure 2 shows a typical red-black tree.
 
Working With Red-Black Trees In C#
Figure 2. Red-Black Tree: This tree conforms to all red-black tree rules. (Image adapted from Wikipedia.)
 

Implementing a BST in C#


It's worth looking at the code for a binary search tree first; you can use it to see how the red-black tree implementation differs, and for comparison testing. The code given below implements a simple binary search tree. It's interesting to look at the recursive code for searching the tree:
  1. public Node Search(Node node, Object key) {  
  2.     if (node == nullreturn null;  
  3.     else {  
  4.         int result = String.Compare(key.ToString(), node.key.ToString());  
  5.         if (result < 0) return Search(node.left, key);  
  6.         else if (result > 0) return Search(node.right, key);  
  7.         else return node;  
  8.     }  
  9. }  
The search method compares the string values of the key parameter and the key of the passed-in node. The result of that comparison sets up the recursive call to search when the key value is either less than (search the left child) or greater than (search the right child) the key value of the node parameter. As the search progresses down the tree, eventually either the key value will match a node value, resulting in a successful search, or the search will fail and the method will return null.

Implementing a Red-Black Tree in C#


The rest of this article discusses a red-black tree implementation, shows how you can use it to search data, and shows an example comparing the efficiency of a search operation over a large data set between the red-black tree and the binary tree implementations.

Start by creating an enum containing two integer constants that represent the colors of the red-black tree nodes,
  1. public enum Color {  
  2.     Red = 0, Black = 1  
  3. }  
You also need an enum that holds a direction, represented by constants called Left and Right,
  1. public enum Direction {  
  2.     Left,  
  3.     Right  
  4. }  
The Node class shown below represents a single red-black tree node. It has two overloaded constructors: One accepts the data that the new node should hold, and the other accepts both the node data and references to its child left and right nodes,
  1. public class Node {  
  2.     public IComparable data;  
  3.     public Node left;  
  4.     public Node right;  
  5.     public Color color = Color.Black;  
  6.     public Node(IComparable data): this(data, nullnull) {}  
  7.     public Node(IComparable data, Node left, Node right) {  
  8.         this.data = data;  
  9.         this.left = left;  
  10.         this.right = right;  
  11.     }  
  12. }  
Next, here's a base class called Tree from which the RedBlackTree class (discussed later) will inherit. This class contains methods for searching and comparing node data and a method called Display() to display the tree data as text. It also contains references to the root node, the current node, and a "nil" node that serves as the single reference for all leaf or "external" nodes,
  1. public class Tree {  
  2.     protected Node root;  
  3.     protected Node freshNode;  
  4.     protected Node currentNode;  
  5.     protected Tree() {  
  6.         freshNode = new Node(null);  
  7.         freshNode.left = freshNode.right = freshNode;  
  8.         root = new Node(null);  
  9.     }  
  10.     protected int Compare(IComparable item, Node node) {  
  11.         if (n != root) return item.CompareTo(node.data);  
  12.         else return 1;  
  13.     }  
  14.     public IComparable Search(IComparable data) {  
  15.         freshNode.data = data;  
  16.         currentNode = root.right;  
  17.         while (true) {  
  18.             if (Compare(data, currentNode) < 0) currentNode = currentNode.left;  
  19.             else if (Compare(data, currentNode) > 0) currentNode = currentNode.right;  
  20.             else if (currentNode != freshNode) return currentNode.data;  
  21.             else return null;  
  22.         }  
  23.     }  
  24.     protected void Display() {  
  25.         this.Display(root.right);  
  26.     }  
  27.     protected void Display(Node temp) {  
  28.         if (temp != freshNode) {  
  29.             Display(temp.left);  
  30.             Console.WriteLine(temp.data);  
  31.             Display(temp.right);  
  32.         }  
  33.     }  
  34. }  
With the base class in place, you can create a RedBlackTree class: You need the added attribute called color, a reference to the parent and the grandparent nodes, and a temporary node reference. Here's the code for the RedBlackTree class.
  1. public sealed class RedBlackTree: Tree {  
  2.     private Color Black = Color.Black;  
  3.     private Color Red = Color.Red;  
  4.     private Node parentNode;  
  5.     private Node grandParentNode;  
  6.     private Node tempNode;  
  7. }  
The Insert() method adds new nodes to the RedBlackTree. The insert operation places the new node either to the left or the right of the parent node, depending on whether its value is lesser or greater than the parent node's value,
  1. public void Insert(IComparable item) {  
  2.     currentNode = parentNode = grandParentNode = root;  
  3.     freshNode.data = item;  
  4.     int returnedValue = 0;  
  5.     while (Compare(item, currentNode) != 0) {  
  6.         tempNode = grandParentNode;  
  7.         grandParentNode = parentNode;  
  8.         parentNode = currentNode;  
  9.         returnedValue = Compare(item, currentNode);  
  10.         if (returnedValue < 0) currentNode = currentNode.left;  
  11.         else currentNode = currentNode.right;  
  12.         if (currentNode.left.color == Color.Red && currentNode.right.color == Color.Red) ReArrange(item);  
  13.     }  
  14.     if (currentNode == freshNode) {  
  15.         currentNode = new Node(item, freshNode, freshNode);  
  16.         if (Compare(item, parentNode) < 0) parentNode.left = currentNode;  
  17.         else parentNode.right = currentNode;  
  18.         ReArrange(item);  
  19.     }  
  20. }  
You may have noticed calls to a ReArrange method in the preceding code. That's necessary, because when you add or delete nodes from a red-black tree, you may need to move nodes around or change their color to meet the red-black tree rules discussed earlier. The ReArrange operation actually swaps nodes to ensure that the color properties are preserved, but at the same time makes sure that the in-order traversal of the tree is not lost by calling the Rotate methods shown below to restore red-black tree ordering rules.
  1. private void ReArrange(IComparable item) {  
  2.     currentNode.color = Red;  
  3.     currentNode.left.color = Color.Black;  
  4.     currentNode.right.color = Color.Black;  
  5.     if (parentNode.color == Color.Red) {  
  6.         grandParentNode.color = Color.Red;  
  7.         bool compareWithGrandParentNode = (Compare(item, grandParentNode) < 0);  
  8.         bool compareWithParentNode = (Compare(item, parentNode) < 0);  
  9.         if (compareWithGrandParentNode != compareWithParentNode) parentNode = Rotate(item, grandParentNode);  
  10.         currentNode = Rotate(item, tempNode);  
  11.         currentNode.color = Black;  
  12.     }  
  13.     root.right.color = Color.Black;  
  14. }  
  15. private Node Rotate(IComparable item, Node parentNode) {  
  16.     int value;  
  17.     if (Compare(item, parentNode) < 0) {  
  18.         value = Compare(item, parentNode.left);  
  19.         if (value < 0) parentNode.left = this.Rotate(parentNode.left, Direction.Left);  
  20.         else parentNode.left = this.Rotate(parentNode.left, Direction.Right);  
  21.         return parentNode.left;  
  22.     } else {  
  23.         value = Compare(item, parentNode.right);  
  24.         if (value < 0) parentNode.right = this.Rotate(parentNode.right, Direction.Left);  
  25.         else parentNode.right = this.Rotate(parentNode.right, Direction.Right);  
  26.         return parentNode.right;  
  27.     }  
  28. }  
  29. private Node Rotate(Node node, Direction direction) {  
  30.     Node tempNode;  
  31.     if (direction == Direction.Left) {  
  32.         tempNode = node.left;  
  33.         node.left = tempNode.right;  
  34.         tempNode.right = node;  
  35.         return tempNode;  
  36.     } else {  
  37.         tempNode = node.right;  
  38.         node.right = tempNode.left;  
  39.         tempNode.left = node;  
  40.         return tempNode;  
  41.     }  
  42. }  

Working with Red-Black Trees


Here's an example showing how you can use the RedBlackTree class. The main() method shown below creates a new RedBlackTree instance and populates it with 1,000,000 nodes containing random integer values between 1 and 1,000,000. Finally, it inserts a node with the value 1,000,001 (forcing that node to appear at the end of the tree), and then searches for it, printing the elapsed time.
  1. public static void Main(String[] args) {  
  2.     RedBlackTree redBlackTree = new RedBlackTree();  
  3.     Random random = new Random();  
  4.     for (int i = 0; i < 1000000; i++) {  
  5.         redBlackTree.Insert(random.Next(1, 1000000));  
  6.         random.Next();  
  7.     }  
  8.     redBlackTree.Insert(1000001);  
  9.     DateTime startTime = DateTime.Now;  
  10.     int p = (int) redBlackTree.Search(1000001);  
  11.     DateTime endTime = DateTime.Now;  
  12.     TimeSpan TimeElapsed = (TimeSpan)(endTime - startTime);  
  13.     Console.WriteLine("The number " + p + " has been found in " + TimeElapsed.Milliseconds.ToString() + " milliseconds.");  
  14.     Console.Read();  
  15.     Console.Read();  
  16. }  
When I run the preceding application on my system, the search requires just three milliseconds. Your system's timing may vary. To explore how much faster searching a red-black tree is compared to searching a binary search tree, I built both tree types in a similar manner, populated them with identical node values, and ran a comparison test.
 
Here's the test code:
  1. public static void Main(String[] args) {  
  2.     RedBlackTree redBlackTree = new RedBlackTree();  
  3.     BinaryTree.BinarySearchTree binarySearchTree = new  
  4.     BinaryTree.BinarySearchTree();  
  5.     for (int i = 0; i < 900000; i++) {  
  6.         redBlackTree.Insert(i);  
  7.     }  
  8.     for (int p = 0; p < 900000; p++) {  
  9.         binarySearchTree.Insert(p, p.ToString());  
  10.     }  
  11.     DateTime startTime = DateTime.Now;  
  12.     redBlackTree.Search(99449);  
  13.     DateTime endTime = DateTime.Now;  
  14.     TimeSpan TimeElapsed = (TimeSpan)(endTime - startTime);  
  15.     Console.WriteLine("Red-Black Tree Search Time: " + TimeElapsed.Milliseconds.ToString() + " milliseconds.");  
  16.     startTime = DateTime.Now;  
  17.     binarySearchTree.Search(binarySearchTree.Root, "99449");  
  18.     endTime = DateTime.Now;  
  19.     TimeElapsed = (TimeSpan)(endTime - startTime);  
  20.     Console.WriteLine("Binary Tree Search Time: " + TimeElapsed.Milliseconds.ToString() + " milliseconds.");  
  21.     Console.Read();  
  22. }  
On my system, the search using the red-black tree took scarcely any time compared to the same search using a binary search tree. The large difference is because, as discussed earlier, BST performance degrades quickly when you populate the tree with ordered or near-ordered values. In contrast, the red-black tree maintains branch balance even for ordered data, which is the root cause of the difference in search performance.

At this point, you've seen how red-black trees provide more efficient binary search operations for some types of data by maintaining more balance among the branches of the search tree than is usually possible with a typical binary search tree implementation. While adding nodes to red-black trees does take a little longer, that time is usually more than offset by improved search performance as the data volume in the tree grows. If you want more information on building red-black trees and other data structures in C#, I suggest you read this MSDN article.