Generic Binary Search Tree With Keyed Values Using C#

A Binary Search Tree (BST) is a binary tree (max. 2 childs per node) with every node a key and associated value. Also a BST has the property that for every node, the left subtree contains only nodes with a smaller (or equal) key and the right subtree contains only nodes with strictly larger keys. This property will allow us to perform insertion and deletion operations in time proportional to the tree height. Later more on time complexity.
The BST class is declared as follows:
  1. public class BinarySearchTree<Tkey, Tvalue> where Tkey : IComparable<Tkey>  
The nodes in the BST can be keyed by any type that implements IComparable. There is no restriction on the type of the value. Now the simple node definition:
  1. protected class BinaryKeyValueNode<Tkey, Tvalue> where Tkey : IComparable<Tkey>  
  2. {  
  3.     public Tkey Key { getset; }  
  4.     public Tvalue Value { getset; }  
  5.     public BinaryKeyValueNode<Tkey, Tvalue> Parent { getset; }  
  6.     public BinaryKeyValueNode<Tkey, Tvalue> LeftChild { getset; }  
  7.     public BinaryKeyValueNode<Tkey, Tvalue> RightChild { getset; }  
  8.     public BinaryKeyValueNode(Tkey key, Tvalue value)  
  9.     {  
  10.         Value = value;  
  11.         Key = key;  
  12.     }  
  13. }  
So every node consists of a key and a value and keeps a reference to both subtrees (child nodes) and its parent. These are the fields and constructor for our BST:
  1. private Random random;  
  2. protected BinaryKeyValueNode<Tkey, Tvalue> Root { getset; }  
  3. public int Count { getprotected set; }  
  4.   
  5. public BinarySearchTree()  
  6. {  
  7.     Root = null;  
  8.     random = new Random(1);  
  9.     Count = 0;  
  10. }  
We maintain a count (optional) and a reference to the root node (initially null). Insertion goes as follows:
  1. public void Insert(Tkey key, Tvalue value)  
  2. {  
  3.     BinaryKeyValueNode<Tkey, Tvalue> parent = null;  
  4.     BinaryKeyValueNode<Tkey, Tvalue> current = Root;  
  5.     int compare = 0;  
  6.     while (current != null)  
  7.     {  
  8.         parent = current;  
  9.         compare = current.Key.CompareTo(key);  
  10.         current = compare < 0 ? current.RightChild : current.LeftChild;  
  11.     }  
  12.     BinaryKeyValueNode<Tkey, Tvalue> newNode = new BinaryKeyValueNode<Tkey, Tvalue>(key, value);  
  13.     if (parent != null)  
  14.         if (compare < 0)  
  15.             parent.RightChild = newNode;  
  16.         else  
  17.             parent.LeftChild = newNode;  
  18.     else  
  19.         Root = newNode;  
  20.     newNode.Parent = parent;  
  21.     Count++;  
  22. }  
The reference current is the subtree in which we try to insert and parent is the parent of the current subtree. As long as current points to a node (line 6), we go either to the left or to the right, based on the comparison between the key of the current node and the key to the value that we want to insert. If the current key is greater than the key we are inserting then we go to the right subtree, otherwise to the left (line 10). When we have reached an external node, the current will equal null. Now we create the new node and set all the right references (lines 12-20).
 
Finding a value, given its key, works very much the same as in the following:
  1. public Tvalue FindFirst(Tkey key)  
  2. {  
  3.     return FindNode(key).Value;  
  4. }  
  5.   
  6. public Tvalue FindFirstOrDefault(Tkey key)  
  7. {  
  8.     var node=FindNode(key, false);  
  9.     return node == null ? default(Tvalue) : node.Value;  
  10. }  
  11.   
  12. protected BinaryKeyValueNode<Tkey, Tvalue> FindNode(Tkey key, bool ExceptionIfKeyNotFound = true)  
  13. {  
  14.     BinaryKeyValueNode<Tkey, Tvalue> current = Root;  
  15.     while (current != null)  
  16.     {  
  17.         int compare = current.Key.CompareTo(key);  
  18.         if (compare == 0)  
  19.             return current;  
  20.         if (compare < 0)  
  21.             current = current.RightChild;  
  22.         else  
  23.             current = current.LeftChild;  
  24.     }  
  25.     if (ExceptionIfKeyNotFound)  
  26.         throw new KeyNotFoundException();  
  27.     else  
  28.         return null;  
  29. }  
Starting from the root, we compare the given key to the key of the current node. If they are equal, we can return it. If the given key is strictly larger we continue the search in the right subtree, otherwise in the left. Next we have the delete operation:
  1. protected void DeleteNode(BinaryKeyValueNode<Tkey, Tvalue> node)  
  2. {  
  3.     if (node == null)  
  4.         throw new ArgumentNullException();  
  5.     if (node.LeftChild != null && node.RightChild != null//2 childs  
  6.     {  
  7.         BinaryKeyValueNode<Tkey, Tvalue> replaceBy = random.NextDouble() > .5 ? InOrderSuccesor(node) : InOrderPredecessor(node);  
  8.         DeleteNode(replaceBy);  
  9.         node.Value = replaceBy.Value;  
  10.         node.Key = replaceBy.Key;  
  11.     }  
  12.     else //1 or less childs  
  13.     {  
  14.         var child = node.LeftChild == null ? node.RightChild : node.LeftChild;  
  15.         if (node.Parent.RightChild == node)  
  16.             node.Parent.RightChild = child;  
  17.         else  
  18.             node.Parent.LeftChild = child;  
  19.     }  
  20.     Count--;  
  21. }  
  22.   
  23. protected BinaryKeyValueNode<Tkey, Tvalue> InOrderSuccesor(BinaryKeyValueNode<Tkey, Tvalue> node)  
  24. {  
  25.     BinaryKeyValueNode<Tkey, Tvalue> succesor = node.RightChild;  
  26.     while (succesor.LeftChild != null)  
  27.         succesor = succesor.LeftChild;  
  28.     return succesor;  
  29. }  
  30.   
  31. protected BinaryKeyValueNode<Tkey, Tvalue> InOrderPredecessor(BinaryKeyValueNode<Tkey, Tvalue> node)  
  32. {  
  33.     BinaryKeyValueNode<Tkey, Tvalue> succesor = node.LeftChild;  
  34.     while (succesor.RightChild != null)  
  35.         succesor = succesor.RightChild;  
  36.     return succesor;  
Deletion of a node is easy if it has either zero or one child. We can just connect the parent and the child and forget about the deleted node (line 12-18). The BST property will still hold. If the node that needs to be deleted has 2 childs, then we need to find an appropriate node to replace it by that is either the node with the lowest key in the right subtree (in-order-successor), or the node with the highest key in the left subtree (in-order-predecessor) (lines 5-11). We decide which one with a coinflip.
We now have all the essential operations that must be done on a BST. If one would like to traverse the entire tree, to enumerate all its elements, then there are multiple ways to do so, namely depth-first or breadth-first. Here is the implementation to all 3 ways to make a depth-first tree traversal:
  1. public IEnumerable<Tvalue> TraverseTree(DepthFirstTraversalMethod method)  
  2. {  
  3.     return TraverseNode(Root, method);  
  4. }  
  5.   
  6. protected IEnumerable<Tvalue> TraverseNode(BinaryKeyValueNode<Tkey, Tvalue> node, DepthFirstTraversalMethod method)  
  7. {  
  8.     IEnumerable<Tvalue> TraverseLeft = node.LeftChild == null ? new Tvalue[0] : TraverseNode(node.LeftChild, method),  
  9.         TraverseRight = node.RightChild == null ? new Tvalue[0] : TraverseNode(node.RightChild, method),  
  10.         Self = new Tvalue[1] { node.Value };  
  11.     switch(method)  
  12.     {  
  13.         case DepthFirstTraversalMethod.PreOrder:  
  14.             return Self.Concat(TraverseLeft).Concat(TraverseRight);  
  15.         case DepthFirstTraversalMethod.InOrder:  
  16.             return TraverseLeft.Concat(Self).Concat(TraverseRight);  
  17.         case DepthFirstTraversalMethod.PostOrder:  
  18.             return TraverseLeft.Concat(TraverseRight).Concat(Self);  
  19.         default:  
  20.             throw new ArgumentException();  
  21.     }              
  22. }  
  23.   
  24. public enum DepthFirstTraversalMethod  
  25. {  
  26.     PreOrder,  
  27.     InOrder,  
  28.     PostOrder  
  29. }  
If you want to learn more about tree traversal, read the wikipedia. Traversing a BST in-order will rusult in a sorted enumeration of all node-values.
 
On Time Complexity
 
The three basic operations, insert, find and delete, all have time complexity O(h) (linearly dependant), where h is the height of the tree. The height of the tree is bound by the 2-log of the count (lower bound) and the count (upper bound). The expected height of the tree is the square root of the count (that grows faster than the 2-log), after many random insertions and deletions. The performance of a BST can be  improved uon by self-balancing; keeping the height small (2-log). I will implement a self-balancing BST in my next article soon.

Up Next
    Ebook Download
    View all
    Learn
    View all