CMSC--202 , Section Computer Science II for Majors
Fall 1997 16 November 1997 Handout -- Binary Trees
We have already examined the mathematical notion of trees in
general and binary trees in particular. We now interpret those
mathematical ideas to a form useful for computation by developing
a binary tree ADT.
A binary tree is a homogeneous collection of hierarchically
organized data. Each datum contains a value, a left
child, and a right child (both of which are data). The value
stored at a datum is of type tree_item_type and a tree is of
type tree_type. There is a constant the_empty_tree that
designates a binary tree containing no data. The following non-destructive
operations are defined:
tree_type tree_cons(tree_item_type E, tree_type L, tree_type R)
constructs and returns a new tree with E as its root
and L and R as its left and right children, respectively.
tree_item_type tree_data(tree_type T) returns the value
at the root of T. It is an error to invoke tree_data on an empty
tree.
tree_type left_child(tree_type T) returns the left subtree of
T. It is an error to invoke left_child on an empty tree.
tree_type right_child(tree_type T) returns the right subtree of
T. It is an error to invoke right_child on an empty tree.
BOOL empty_tree(tree_type T) returns TRUE if T is empty,
FALSE otherwise.
The bintrees.h interface file for the binary tree operations
is in the directory

anastasi/Public/202/Includes .
The implementation of the operations
is in the lib202.a archive in the directory

anastasi/Public/202/Libraries .
This section presents one implementation of the binary tree ADT
presented above. This implementation follows closely our
implementation of linked lists. We assume that tree_item_type
has been defined to be the type of object we want to store at each
node of the tree. The underlying data structures are defined as follows:
typedef struct tree_tag *tree_type;
typedef struct tree_tag {
tree_item_type data;
tree_type left;
tree_type right;
} tree_struct_type;
Because a tree is a pointer in this implementation, it is natural to
use NULL to represent the empty tree:
#define the_empty_tree NULL
The code for the five basic functions is straightforward; comparison with the list routines presented in class reveals an almost token--for--token similarity:
tree_type
tree_cons(tree_item_type value, tree_type left, tree_type right)
{
tree_type result;
result = (tree_type) malloc(sizeof(tree_struct_type));
assert(result != NULL);
result->data = value;
result->left = left;
result->right = right;
return result;
}
tree_item_type
tree_data(tree_type tree)
{
assert(empty_tree(tree) == FALSE);
return tree->data;
}
tree_type
left_child(tree_type tree)
{
assert(empty_tree(tree) == FALSE);
return tree->left;
}
tree_type
right_child(tree_type tree)
{
assert(empty_tree(tree) == FALSE);
return tree->right;
}
BOOL
empty_tree(tree_type tree)
{
return (tree == the_empty_tree) ? TRUE : FALSE;
}
A traversal routine is one that visits every node of a tree, performing a given operation on each node when it is visited. Three orderings of the nodes during traversal are in common use:
The following code will perform any one of these traversal methods:
typedef enum {preorder, inorder, postorder} traversal_type;
void
traversal(tree_type tree, traversal_type method, void (*operation)(tree_type))
{
if (empty_tree(tree) == FALSE)
{
if (method == preorder)
operation(tree);
traversal(left_child(tree), method, operation);
if (method == inorder)
operation(tree);
traversal(right_child(tree), method, operation);
if (method == postorder)
operation(tree);
}
}
As an example of the use of the traversal routine, the
following code will print the contents of a tree of integers:
void
print_int_tree_data(tree_type tree)
{
printf("%d ", tree_data(tree));
}
void
print_int_tree(tree_type tree, traversal_type type)
{
traversal(tree, type, print_int_tree_data);
putchar('\n');
}
A search is like a traversal, except that when a desired node in the
tree is encountered, the search is terminated. A search returns the
desired node, or the_empty_tree if no appropriate node is
found.
Two orderings of the nodes in a search are in common use:
Each of the search routines in the following subsections takes two arguments: a tree to search, and a predicate that determines whether an acceptable node has been found.
Here is an implementation of depth--first search. It uses recursion to search downward in the tree:
tree_type
depth_first_search(tree_type tree, BOOL (*predicate)(tree_type))
{
tree_type result;
if (empty_tree(tree) == TRUE)
return the_empty_tree;
if (predicate(tree) == TRUE)
return tree;
result = depth_first_search(left_child(tree), predicate);
if (empty_tree(result) == FALSE)
return result;
else
return depth_first_search(right_child(tree), predicate);
}
Here is an implementation of depth--first search that uses an explicit stack. The stack is used to keep track of which nodes are yet to be examined:
tree_type
dfs_stack(tree_type tree, BOOL (*predicate)(tree_type))
{
stack_type stack = create_stack();
tree_type guess;
push(tree, stack);
while (empty_stack(stack) == FALSE)
{
guess = pop(stack);
if (empty_tree(guess) == TRUE)
continue;
if (predicate(guess) == TRUE)
return guess;
push(left_child(guess), stack);
push(right_child(guess), stack);
}
return the_empty_tree;
}
Here is an implementation of breadth--first search. Notice that a queue is used instead of a stack; otherwise, this routine is virtually identical to the second implementation of depth--first search:
tree_type
bfs_queue(tree_type tree, BOOL (*predicate)(tree_type))
{
queue_type queue = create_queue();
tree_type guess;
enqueue(tree, queue);
while (empty_(queue) == FALSE)
{
guess = dequeue(queue);
if (empty_tree(guess) == TRUE)
continue;
if (predicate(guess) == TRUE)
return guess;
enqueue(left_child(guess), queue);
enqueue(right_child(guess), queue);
}
return the_empty_tree;
}
it might return the tree:
Note that other solutions are possible; your program may return any legal
solution. If delete_root is called on the empty tree, or
on a tree containing exactly one node, it should return the empty tree.
Delete_root must not modify its argument in any way; it
should produce a brand new tree.