This post looks at how to implement a binary tree using generics in C #. A binary tree is a data structure in which each node has at most two children. they are a good way to store unsorted data, as the data becomes sorted as you insert it into the tree.
Another benefit of storing information in a tree structure is the opportunity for faster search times. in an array (or list) you need to traverse the whole array to discover if it contains a particle value. A well-balanced binary tree can drastically reduce the number of look-ups required.
Given the left set of numbers, the tree on the right is created. if a value is smaller than the root, it goes left, if a value is larger it goes right. if both the left and right nodes have been assigned already, we need to travel further down the tree and create new entries there.
In the tree above the longest route from the root of the tree (5) is to discover that it contains (6) which in this example takes just 4 steps. all other values can be found in 1-3 steps.
Balancing the tree
Several algorithms exist for balancing trees-but in this post we keep things simple: our tree does not balance itself as new values are inserted.
By re-balancing it is possible to ensure that the maximum depth of the tree stays constant. if we were to insert data in-order into the tree, the tree itself wocould become lopsided and that wocould reduce its use in searching. the greater the trees depth (maximum number of steps from the root) the longer a potential search wocould take, and in the most unfortunate instance it wocould be the same as for a linked list or array.
So to maximize this trees potential we wocould need our data insertion to be as random as possible.
The Tree Node Structure
Each node in our tree can be described by a very simple class:
class BinaryTreeNode { public BinaryTreeNode Left; public BinaryTreeNode Right; public BinaryTreeNode Parent; public T Data; public BinaryTreeNode() { Left = null; Right = null; Parent = null; } }
We use a class, and not a struct as C # passes classes by reference, and structs by value. passing by reference is the same as using pointers in C/C ++. it allows us to dynamically add new entries-or remove them-and to traverse the tree.
Each node has the potential to link to another Left and Right node and it stores one Data unit.
Because we will be traveling through the tree non-recursively we also need to keep a link to the Parent node. this allows us to travel back up the tree if we have reached a dead end in our search.
Using Generics in a Binary Tree
To allow us to store any kind of data-not just integers, the actual Binary Tree class is implemented using Generics. You are free to define your own datastructure and store it in the tree.
Class BinaryTree <T>
But how to sort our data? Because T can be anything, the compiler does not know how to compare two instances of T to each other. For our tree algorithm we need to know if:
T1 <T2
T1 = T2
T1> T2
Generic sorting routines have the same problem, and the System class of C # provides a delegate function that we will use in our tree:
public delegate int Comparison<T>(T x,T y)
A sample implementation for comparing integers is provided in the class:
/// <summary> /// For integer comparisons we provide a demonstration function. /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns><0 for left smaller than right, >0 if they are equal, +1 if right is larger than left</returns> public static int CompareFunction_Int(int left, int right) { return left - right; }
This function is passed to the class constructor on creation:
BinaryTree<int> Test = new BinaryTree<int>(BinaryTree<int>.CompareFunction_Int);
If you use your own data type you will have to provide your own comparison function.
Insert Algorithm
Most of the work of building the tree is done in the insert algorithm.
The flowchart is a little long to insert in a blog post, but if you are interested you canfind it here.
Custom Iterator
To retrieve all the values from the binary tree the BinaryTree class provides a custom iterator. The iterator it returns performs an ordered walk through the binary tree returning each data entry stored in order of increasing value.
BinaryTree<int> Test = new BinaryTree<int>(BinaryTree<int>.CompareFunction_Int); /*...*/ // Iterate over all members in the tree -- values are returned in sorted order foreach(int value in Test) { Console.WriteLine("Value: {0}", value); }
The code for the BinaryTree class is listed below, a small set of test routines is given in the TreeTest class.
using System;using System.Collections;using System.Collections.Generic;using System.Linq;using System.Text;namespace Developer.Collections{ /// <summary> /// The BinaryTree class implements a simple non-balanced sorted Binary Tree in C# /// </summary> /// <typeparam name="T">The tree can contain any type, but you are required to provide your own comparison function.</typeparam> class BinaryTree<T> { /// <summary> /// The tree is build up out of BinaryTreeNode instances /// </summary> class BinaryTreeNode { public BinaryTreeNode Left; public BinaryTreeNode Right; public BinaryTreeNode Parent; public T Data; public BinaryTreeNode() { Left = null; Right = null; Parent = null; } } BinaryTreeNode Root; Comparison<T> CompareFunction; /// <summary> /// The BinaryTree constructor requires that we pass a comparison function. We need one as generics can only /// be compared as equals, but not for order. The solution is to allow the caller to pass a suitable comparison /// function. We use the C# Comparison delegate for this (found in System.Collections) /// </summary> /// <param name="theCompareFunction">Pass a delegate function of the type Comparison to the function</param> public BinaryTree(Comparison<T> theCompareFunction) { Root = null; CompareFunction = theCompareFunction; } /// <summary> /// For integer comparisons we provide a demonstration function. /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns><0 for left smaller than right, >0 if they are equal, +1 if right is larger than left</returns> public static int CompareFunction_Int(int left, int right) { return left - right; } /// <summary> /// For string comparisons we provide a demonstration function /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns>-1 for left smaller than right, 0 if they are equal, +1 if right is larger than left</returns> public static int CompareFunction_String(string left, string right) { return left.CompareTo(right); } /// <summary> /// The add function uses non-recursive tree traversal to find the next available insertion point /// </summary> /// <param name="Value">The value to insert into tree.</param> public void Add(T Value) { BinaryTreeNode child = new BinaryTreeNode(); child.Data = Value; // Is the tree empty? Make the root the new child if (Root == null) { Root = child; } else { // Start from the root of the tree BinaryTreeNode Iterator = Root; while (true) { // Compare the value to insert with the value in the current tree node int Compare = CompareFunction(Value, Iterator.Data); // The value is smaller or equal to the current node, we need to store it on the left side // We test for equivalence as we allow duplicates (!) if (Compare <= 0) if (Iterator.Left != null) { // Travel further left Iterator = Iterator.Left; continue; } else { // An empty left leg, add the new node on the left leg Iterator.Left = child; child.Parent = Iterator; break; } if (Compare > 0) if (Iterator.Right != null) { // Continue to travel right Iterator = Iterator.Right; continue; } else { // Add the child to the right leg Iterator.Right = child; child.Parent = Iterator; break; } } } } /// <summary> /// This routine walks through the tree to see if the value given can be found. /// </summary> /// <param name="Value">The value to look for in the tree</param> /// <returns>True if found, False if not found</returns> public bool Find(T Value) { BinaryTreeNode Iterator = Root; while (Iterator != null) { int Compare = CompareFunction(Value, Iterator.Data); // Did we find the value ? if (Compare == 0) return true; if (Compare < 0) { // Travel left Iterator = Iterator.Left; continue; } // Travel right Iterator = Iterator.Right; } return false; } /// <summary> /// Given a starting node, this routine will locate the left most node in the sub-tree /// If no further nodes are found, it returns the starting node /// </summary> /// <param name="start">The sub-tree starting point</param> /// <returns></returns> BinaryTreeNode FindMostLeft(BinaryTreeNode start) { BinaryTreeNode node = start; while (true) { if (node.Left != null) { node = node.Left; continue; } break; } return node; } /// <summary> /// Returns a list iterator of the elements in the tree implementing the IENumerator interface. /// </summary> /// <returns>IENumerator</returns> public IEnumerator<T> GetEnumerator() { return new BinaryTreeEnumerator(this); } /// <summary> /// The BinaryTreeEnumerator implements the IEnumerator allowing foreach enumeration of the tree /// </summary> class BinaryTreeEnumerator : IEnumerator<T> { BinaryTreeNode current; BinaryTree<T> theTree; public BinaryTreeEnumerator(BinaryTree<T> tree) { theTree = tree; current = null; } /// <summary> /// The MoveNext function traverses the tree in sorted order. /// </summary> /// <returns>True if we found a valid entry, False if we have reached the end</returns> public bool MoveNext() { // For the first entry, find the lowest valued node in the tree if (current == null) current = theTree.FindMostLeft(theTree.Root); else { // Can we go right-left? if (current.Right != null) current = theTree.FindMostLeft(current.Right); else { // Note the value we have found T CurrentValue = current.Data; // Go up the tree until we find a value larger than the largest we have // already found (or if we reach the root of the tree) while (current != null) { current = current.Parent; if (current != null) { int Compare = theTree.CompareFunction(current.Data,CurrentValue); if (Compare < 0) continue; } break; } } } return (current != null); } public T Current { get { if (current == null) throw new InvalidOperationException(); return current.Data; } } object IEnumerator.Current { get { if (current == null) throw new InvalidOperationException(); return current.Data; } } public void Dispose() { } public void Reset() { current = null; } } } class TreeTest { static void Main(string[] args) { BinaryTree<int> Test = new BinaryTree<int>(BinaryTree<int>.CompareFunction_Int); // Build the tree Test.Add(5); Test.Add(2); Test.Add(1); Test.Add(3); Test.Add(3); // Duplicates are OK Test.Add(4); Test.Add(6); Test.Add(10); Test.Add(7); Test.Add(8); Test.Add(9); // Test if we can find values in the tree for (int Lp = 1; Lp <= 10; Lp++) Console.WriteLine("Find ({0}) = {1}", Lp,Test.Find(Lp)); // Test if we can find a non-existing value Console.WriteLine("Find (999) = {0}", Test.Find(999)); // Iterate over all members in the tree -- values are returned in sorted order foreach(int value in Test) { Console.WriteLine("Value: {0}", value); } } }}