A binary tree is a recursive self-referencing data structure that can either be empty or comprise three elements: some data that is typically referred to as the node and two sub-trees, w
Trang 1Creating a Generic Class
The NET Framework Class Library contains a number of generic classes readily
available for you You can also define your own generic classes, which is what you will
do in this section Before we do this, I will provide a bit of background theory
The Theory of Binary Trees
In the following exercises, you will define and use a class that represents a binary tree This is a very practical exercise because this class happens to be one that is missing from the System.Collections.Generic namespace A binary tree is a very useful data structure used for a variety of operations, including sorting and searching through data very
quickly There are volumes written on the minutiae of binary trees, but it is not the
purpose of this book to cover binary trees in detail Instead, we will just look at the
pertinent details If you are interested you should consult a book such as The Art of Computer Programming, Volume 3: Sorting and Searching by Donald E Knuth
(Addison-Wesley Professional; 2nd edition 1998)
A binary tree is a recursive (self-referencing) data structure that can either be empty or comprise three elements: some data that is typically referred to as the node and two sub-trees, which are themselves binary trees The two sub-trees are conventionally called the left sub-tree and the right sub-tree because they are typically depicted to the left and right
of the node respectively Each left sub-tree or right sub-tree is either empty, or contains a node and other sub-trees In theory, the whole structure can continue ad infinitum Figure 17-1 shows the structure of a small binary tree
The real power of binary trees becomes evident when you use them for sorting data If you start with an unordered sequence of objects of the same type, you can use them to construct an ordered binary tree and then walk through the tree to visit each node in an ordered sequence The algorithm for inserting an item into an ordered binary tree is shown below:
If the tree, T, is empty
Then
Construct a new tree T with the new item I as the node, and empty left and
right sub-trees
Else
Examine the value of the node, N, of the tree, T
If the value of N is greater than that of the new item, I
Then
If the left sub-tree of T is empty
Trang 2Then
Construct a new left sub-tree of T with the item I as the node, and
empty left and right sub-trees
Else
Insert I into the left sub-tree of T
End If
Else
If the right sub-tree of T is empty
Then
Construct a new right sub-tree of T with the item I as the node, and
empty left and right sub-trees
Else
Insert I into the right sub-tree of T
End If
End If
End If
Figure 17-1 A binary tree
Notice that this algorithm is recursive, calling itself to insert the item into the left or right sub-tree depending on the value of the item and the current node in the tree
NOTE
The definition of the expression greater than depends on the type of data in the item and node For numeric data, greater than can be a simple arithmetic comparison, for text data
it can be a string comparison, but other forms of data must be given their own means of comparing values This is discussed in more detail when we implement a binary tree in the section “Building a Binary Tree Class Using Generics” later in this chapter
If you start with an empty binary tree and an unordered sequence of objects, you can iterate through the unordered sequence inserting each one into the binary tree by using this algorithm, resulting in an ordered tree Figure 17-2 shows the steps in the process for constructing a tree from a set of 5 integers
Figure 17-2 Constructing an ordered binary tree
Once you have built an ordered binary tree, you can display its contents in sequence by visiting each node in turn and printing the value found The algorithm for achieving this task is also recursive:
If the left sub-tree is not empty
Trang 3Then
Display the contents of the left sub-tree
End If
Display the value of the node
If the right sub-tree is not empty
Then
Display the contents of the right sub-tree
End If
Figure 17-3 shows the steps in the process for outputting the tree constructed earlier Notice that the integers are now displayed in ascending order
Figure 17-3 Printing an ordered binary tree
Building a Binary Tree Class Using Generics
In the following exercise, you will use generics to define a binary tree class capable of holding almost any type of data The only restriction is that the data type must provide a means of comparing values between different instances
The binary tree class is a class that you might find useful in many different applications Therefore, you will implement it as a class library rather than an application in its own right You can then reuse this class elsewhere without needing to copy the source code and recompile it A class library is a set of compiled classes (and other types such as structs and delegates) stored in an assembly An assembly is a file that usually has the
“.dll” suffix Other projects and applications can make use of the items in a class library
by adding a reference to its assembly, and then bringing its namespaces into scope with using statements You will do this when you test the binary tree class
The System.IComparable and System.IComparable<T> Interfaces
If you need to create a class that requires you to be able to compare values according to some natural (or possibly unnatural) ordering, you should implement the IComparable interface This interface contains a method called CompareTo, which takes a single parameter specifying the object to be compared to the current instance and returns an integer that indicates the result of the comparison as shown in the following table Value Meaning
Less than zero The current instance is less than the value of the parameter
Zero The current instance is equal to the value of the parameter
Greater than zero The current instance is greater than the value of the parameter
Trang 4As an example, consider the Circle class that was described in Chapter 7, “Creating and Managing Classes and Objects,” and is reproduced below:
class Circle
{
public Circle(double initialRadius)
{
radius = initialRadius;
}
public double Area()
{
return 3.141593 * radius * radius;
}
private double radius;
}
You can make the Circle class comparable by implementing the System.IComparable interface and providing the CompareTo method In the example shown, the CompareTo method compares Circle objects based on their areas The area of a circle with a larger area is greater than a circle with a smaller area
class Circle : System.IComparable
{
public int CompareTo(object obj)
{
Circle circObj = (Circle)obj; // cast the parameter to its real type
if (this.Area() == circObj.Area())
return 0;
if (this.Area() > circObj.Area())
return 1;
return -1;
}
}
If you examine the System.IComparable interface, you will see that its parameter is
defined as an object However, this approach is not typesafe To understand why this is
so, consider what happens if you try to pass something that is not a Circle to the
CompareTo method The System.IComparable interface requires the use of a cast in order
Trang 5to be able to access the Area method If the parameter is not a Circle but some other type
of object then this cast will fail However, the System namespace also defines the generic IComparable<T> interface, which contains the following methods:
int CompareTo(T other);
bool Equals(T other);
Notice there is an additional method in this interface, called Equals, which should return true if both instances are equals, false if they are not equals
Also notice that these methods take a type parameter (T) rather than an object, and as such, are much safer than the non-generic version of the interface The following code shows how you can implement this interface in the Circle class:
class Circle : System.IComparable<Circle>
{
public int CompareTo(Circle other)
{
if (this.Area() == other.Area())
return 0;
if (this.Area() > other.Area())
return 1;
return -1;
}
public bool Equals(Circle other)
{
return (this.CompareTo(other) == 0);
}
}
The parameters for the CompareTo and Equals method must match the type specified in the interface, IComparable<Circle> In general, it is preferable to implement the
System.IComparable<T> interface rather than System.IComparable You can also
implement both, as many of the types in the NET Framework do
Create the Tree<T> class
1 Start Visual Studio 2005
Trang 62 Create a new Visual C# project using the Class Library template (on the File menu, point to New, and then click Project) Name the project BinaryTree and set the Location to \Microsoft Press\Visual CSharp Step By Step\Chapter 17
3 In the Solution Explorer change the name of the file Class1.cs to Tree.cs
4 In the Code and Text Editor window, change the definition of the Class1 class to Tree<T>
The definition of the Tree<T> class should look like this:
public class Tree<T>
{
}
NOTE
It is conventional to use a single letter (often T) to indicate a type parameter, although any C# legal identifier is allowed If a generic class uses multiple type parameters, you must use a different identifier for each type For example, the generic Dictionary class in the NET Framework is defined as Dictionary<K, V> The K type indicates the type used for dictionary keys, and the V type indicates the type used for dictionary values
5 In the Code and Text Editor window, modify the definition of the Tree<T> class
to specify that the type parameter T must denote a type that implements the
generic IComparable<T> interface
The modified definition of the Tree<T> class should look like this:
public class Tree<T> where T : IComparable<T>
{
}
6 Add three private variables to the Tree<T> class; a T variable called data, and two Tree<T> variables called left and right:
7 private T data;
8 private Tree<T> left;
private Tree<T> right;
9 Add a constructor to the Tree<T> class that takes a single T parameter called nodeValue In the constructor, set the data variable to nodeValue, and initialize the left and right variables to null, as shown below:
10 public Tree(T nodeValue)
Trang 711 {
12 this.data = nodeValue;
13 this.left = null;
14 this.right = null;
}
15 Add a public property of type T called NodeData to the Tree<T> class This
property should provide get and set accessors allowing the user to read and modify the data variable:
The NodeData property should look like this:
public T NodeData
{
get { return this.data; }
set { this.data = value; }
}
16 Add two more properties to the Tree<T> class called LeftTree and RightTree These properties are very similar to each other, and provide get and set access to the left and right Tree<T> variables respectively
The LeftTree property should look like this (the RightTree property is the same except that it uses the right variable):
public Tree<T> LeftTree
{
get { return this.left; }
set { this.left = value; }
}
17 Add a public method called Insert to the Tree<T> class This method will insert a
T value into the tree
The method definition should look like this:
public void Insert (T newItem)
{
}
The Insert method will follow the algorithm described earlier The programmer will have used the constructor to insert the initial node into the tree, so the Insert method can assume that the tree is not empty The next part of the algorithm is
Trang 8reproduced below to help you understand the code you will write in the following steps:
Examine the value of the node, N, of the tree, T
If the value of N is greater than that of the new item, I
Then
If the left sub-tree of T is empty
Then
Construct a new left sub-tree of T with the item I as the node, and empty
left and right sub-trees
Else
Insert I into the left sub-tree of T End If
18 In the Insert method, add a statement that declares a local variable of type T, called currentNodeValue Initialize this variable to the value of the NodeData property of the tree, as shown below:
19 public void Insert(T newItem)
20 {
21 T currentNodeValue = this.NodeData;
}
22 Add the following if-else statement to the Insert method, after the definition of the currentNodeValue variable This statement uses the CompareTo method of the IComparable<T> interface to determine whether the value of the current node is greater then the new item:
23 if (currentNodeValue.CompareTo(newItem) > 0)
24 {
25 //Insert the new item into the left sub-tree
26 }
27 else
28 {
29 // Insert the new item into the right sub-tree
}
30 Replace the //Insert the new item into the left sub-tree comment with the following block of code:
31 if (this.LeftTree == null)
32 {
33 this.LeftTree = new Tree<T>(newItem);
34 }
35 else
Trang 936 {
37 this.LeftTree.Insert(newItem);
}
These statements check whether the left tree is empty If so, a new left sub-tree is created using the new item; otherwise the new item is inserted into the existing left sub-tree by calling the Insert method recursively
38 Replace the //Insert the new item into the right sub-tree comment with the equivalent code that inserts the new node into the right sub-tree:
39 if (this.RightTree == null)
40 {
41 this.RightTree = new Tree<T>(newItem);
42 }
43 else
44 {
45 this.RightTree.Insert(newItem);
}
46 Add another public method called WalkTree to the Tree<T> class This method will walk through the tree, visiting each node in sequence and printing out its value
The method definition should look like this:
public void WalkTree()
{
}
47 Add the following statements to the WalkTree method These statements
implement the algorithm described earlier for printing the contents of a binary tree:
48 if (this.LeftTree != null)
49 {
50 this.LeftTree.WalkTree();
51 }
52
53 Console.WriteLine(this.NodeData.ToString());
54
55 if (this.RightTree != null)
56 {
57 this.RightTree.WalkTree();
}
Trang 1058 On the Build menu, click Build Solution The class should compile cleanly, so correct any errors that are reported and rebuild the solution if necessary
In the next exercise, you will test the Tree<T> class by creating binary trees of integers and strings
Test the Tree<T> class
1 On the File menu, point to Add and then click New Project Add a new project using the Console Application template Name the project BinaryTreeTest and set the Location to \Microsoft Press\Visual CSharp Step By Step\Chapter 17
NOTE
Remember that a Visual Studio 2005 solution can contain more than one project You are using this feature to add a second project to the BinaryTree solution for testing the Tree<T> class This is the recommended way of testing class libraries
2 Ensure the BinaryTreeTest project is selected in the Solution Explorer On the Project menu, click Set as Startup Project The BinaryTreeTest project will be highlighted in the Solution Explorer
When you run the application, this is the project that will actually be executed
3 On the Project menu, click Add Reference In the Add Reference dialog box, click the Projects tab Click the BinaryTree project and then click OK
The BinaryTree assembly will appear in the list of references for the
BinaryTreeTest project in the Solution Explorer You will now be able to create Tree<T> objects in the BinaryTreeTest project
NOTE
If the class library project is not part of the same solution as the project that uses it, you must add a reference to the assembly (the “.dll” file) and not to the class
library project You do this by selecting the assembly from the Browse tab in the Add Reference dialog box You will use this technique in the final set of exercises
in this chapter
4 In the Code and Text Editor window displaying the Program class, add the
following using directive to the list at the top of the class:
using BinaryTree;
5 Add the following statements to the Main method:
6 Tree<int> tree1 = new Tree<int>(10);