Binary Trees
An important structure which can be built dynamically using pointers
is the binary tree.
Conceptually a binary tree is a structure where each node may contain
some information,
and may have two child nodes, called left and right for convenience.
These child nodes themselves
may also each have two child nodes, and so
on. A node may also have only a left child
or only a right child, or no child nodes at
all. A node with no child node is called a "leaf"
node.
All other nodes are called "interior" nodes.
Here are some examples:
Exercise 1: Examine the tree T1 above to
see if you find any pattern or order to the arrangement.
Can you think of any interpretation of the structure of the tree T1?
Exercise 2: Examine the tree T2 above to
see if you find any pattern or order to the arrangement.
Can you think of any interpretation of the structure of the tree T2?
Exercise 3: Examine the tree T3 above to
see if you find any pattern or order to the arrangement.
Can you think of any interpretation of the structure of the tree T3?
A Tree Node
A node in a binary tree contains a field for the information
stored
at that node and two pointer fileds, one for the left
child and one
for the right child. For example, a binary tree
containing strings
could have its nodes defined as follows:
classTreeNode
{
public:
string
info;
TreeNode
* left;
TreeNode
* right;
TreeNode();
// default constructor
TreeNode(conststring
& item, TreeNode
* l, TreeNode
* r);
};
TreeNode::
TreeNode()
: info(""),
left(NULL), right(NULL)
{}
// constructor assigns info to be item, left
to be l and right to be r.
TreeNode::
TreeNode(const string & item, TreeNode * l, TreeNode * r)
: info(item),
left(l), right(r)
{}
With this definition, the following declaration of a pointer
to a node and
statement would create a new
node containing the string "example".
TreeNode *
ptr;
ptr = new
TreeNode("example", NULL, NULL);
At this point in the code the node pointed to by ptr would
contain the string
"example" in its data field and both its child pointers
would be NULL, meaning
it has no children.
Creating a Binary Tree
A binary tree is created by first creating a single node
with both child pointers NULL.
This node forms the simplest non-empty tree. The
additional nodes are created and inserted
into the tree to build a larger and larger tree.
The pattern of the tree and its interpretation
depend on how additional nodes are inserted. The simplest
means of adding a node to a tree
is to create a new node with both child pointers NULL
and make it the child of a node in the
tree with at least one NULL child. Even in this
case, different patterns can be developed,
depending on how the attachment location is selected.
Here is one example:
void Insert(TreeNode
* & T, const string & name)
// pre: T
is not NULL
{
if(name
< T->info){
if(T->left == NULL)
T->left = new TreeNode(name, NULL, NULL);
else
Insert(T->left, name);
}
else{
if(T->right == NULL)
T->right = new TreeNode(name, NULL, NULL);
else
Insert(T->right, name);
}
}
Note how the search for an insertion point is done recursively.
This is quite common.
Exercise 4: Which of the example trees T1,
T2, T3 given above could have been created
with this insertion function?
Tree Traversals
Many operations on trees can be characterized as "traversals"
of some sort.
To traverse a structure means to "visit" each item stored
in the structure and
carry out some operation. For example, we might
want to find the largest integer
in the tree given as example T2 above. This would
involve traversing the tree and
comparing each data item with the maximum so far.
We might want to print out all
the entries in a tree, as for example with the tree T1
above, which contains a list of names.
In this case the visit to a node would consist of printing
its contents.
For trees, traversals are most commonly (but not exclusively)
done using recursion.
Think of traversing a tree as the following three operations,
in no particular order:
1. Visit the item stored at this (root) node.
2. Traverse the left sub-tree
3. Traverse the right sub-tree.
If we have defined a function Visit, which carries out
the operation on a node of a tree
for a traversal, then we could implement a traversal
of a tree, using a function Traverse, as follows:
void Traverse(TreeNode
* t)
{
1
if(t != NULL){
2
Visit(t);
3
Traverse(t->left);
4
Traverse(t->right);
}
}
In line 1 we check for a null pointer, in which case this
(sub)tree is empty and there
is nothing to be done. Then lines 2, 3, 4, carry
out the steps suggested above. Note
that there is nothing magic about the order of steps
2, 3, 4. If we did these operations
in a different order we would traverse the whole tree,
but visit the nodes in a different
order. Sometimes the order of the visits is important,
in which case we need to select
the ordering of steps 2, 3, 4 correctly.
There are six ways in which the three steps can be placed
in order. Three of those orderings
are commonly used and are given names. We usually
view trees with a crude sense of order,
namely that the left side comes before the right side,
so the three common traversal orderings
always have the recursive call to traverse the left subtree
before the recursive call to traverse the
right subtree. Then there are three variations:
void Preorder(TreeNode
* t)
// executes a preorder traversal
of the tree t
{
if(t != NULL){
Visit(t);
Preorder(t->left);
Preorder(t->right);
}
void Inorder(TreeNode
* t)
// executes an inorder traversal
of the tree t
{
if(t != NULL){
Inorder(t->left);
Visit(t);
Inorder(t->right);
}
void Postorder(TreeNode
* t)
// executes a postorder traversal
of the tree t
{
if(t != NULL){
Postorder(t->left);
Postorder(t->right);
Visit(t);
}
For example, if the function visit simple printed the
contents of the node, we could use
each of these to traverse the list given in example T1
above and print it out:
void Visit(TreeNode
* t)
{
cout
<< t->info << endl;
}
Exercise 5: Write out the list which
would be printed for each of the three traversals
using the given Visit function applied to the tree T1
above.