sundeepblue

Computer Graphics, CAGD, Demoscene, intro [crack each line of code, cram each bit of byte, create each idea of mind]

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

http://www.sparknotes.com/cs/trees/library/summary.html
http://www.sparknotes.com/cs/trees/implementation/
http://www.sparknotes.com/cs/trees/binarysearch/
http://www.sparknotes.com/cs/trees/library/
http://www.sparknotes.com/cs/trees/review/quiz.html

A Tree Manipulation Library

Whenever you implement a new data type, it is usually a good idea to provide functions for manipulating it. There is a general concept in computer science referred to as the black box principle. It is the idea that the user of a data type should not need to be aware of how it is implemented because a library of functions is provided to interact with. This library separates the user from the implementation. This is a good idea because it allows you the programmer to change the implementation (and not the interface) at any point without the user of the data type having to worry about the change affecting his code. This concept is a central idea in Object-Oriented programming.

In the following SparkNote Topic, we will present some of the central functions that should be part of this library. The idea would then be to implement all of the functions together in the same .c file with their prototypes all in the same header file which the file using the tree data type should #include. For this particular implementation, we will use the tree node described in Section 1 of Topic I, which made use of structs and pointers.

Terms

Boolean  -  A variable or a function that is (or returns) either true or false. In C, a value of 0 is considered to be false and any other value is considered to be true.

Short Circuit Evaluation  -  This is the process by which only the minimal amount of calculation is done, proceeding from left to right, to evaluate a boolean expression in C.

Dereference  -  This is the process of taking a memory address and interpreting the bits at that location.

Black Box Principle  -  The idea that when implementing a concept, all of the details of the implementation should be hidden from the ultimate consumer. In this way, changing implementation details will not affect programs that have already been written.

.C File  -  A file where code written in C is stored. Often contains the implementations of functions declared in header files.

Prototype  -  A formal description of a function. Includes the return type, the function name, the arguments and their types. In other words, it contains the details of how a function is called.

Header File  -  This

Tree Creation and Destruction Functions

One of the most useful features of the tree data structure is that it can grow dynamically. That is, at any point in your code, you can make a new node and add it to the tree. Because of this you do not need to know the number of nodes beforehand. As a result, our function which will provide a new tree structure will need to allocate memory. Recall that we have a tree_t data type, defined as follows:

typedef struct _tree {
int  data;
struct _tree *left, *right;
} tree_t;
From this definition we can see that each node points to its left and right children.
To make our node creation function mesh easily with the rest of our implementation, it
should return a pointer to the memory we allocate. Here is one possible way to
implementation such a function:
 
tree_t *new_tree(int data)
{
tree_t *tree;
if ((tree = (tree_t *) malloc (sizeof(tree_t))) == NULL) {
return NULL;
}
tree->data = data;
tree->left = NULL;
tree->right = NULL;
return tree;
}
 
Alternatively, you could write a version where the caller is allowed to specify the 
children.
 
tree_t *new_tree(int data; tree_t *left; tree_t *right)
{
tree_t *tree;
if ((tree = (tree_t *) malloc (sizeof(tree_t))) == NULL) {
return NULL;
}
tree->data = data;
tree->left = left;
tree->right = right;
return tree;
}
 
Since each node in the tree will necessarily be allocated dynamically, it must also be
freed when it is no longer needed. The following function will take care of the freeing
of an individual node.
 
void  free_node (tree_t *tree)
{
if (tree != NULL) {
free(tree);
}
}
 
While it is useful to have a function that destroys an individual node, it would be far 
more useful if we could make one function call to destroy an entire tree. We mentioned in
the introduction that trees are naturally recursive. This function will take advantage of
that feature. Destroying a tree essentially requires destroying the tree headed by the
left child and the tree headed by the right child along with the root of the tree itself.
With that algorithm in mind, we produce the following function:
 
void destroy_tree (tree_t *tree)
{
if (tree == NULL)
return;
destroy_tree(tree->left);
destroy_tree(tree->right);
free_node(tree);
}
 
To break down the function above, we see that there is a base case for the NULL tree, a 
recursive case for other trees, and finally a call to free_node to destroy the tree's
root. You will find that this is a pattern that recurs frequently when writing functions
to manipulate trees.
There are a few things to now consider. This implementation was based on the data in each 
node being an integer. It is entirely possible, however, to have each node contain some
sort of dynamically allocated data. If you wanted to do this, then the new_tree function
would also have to allocate space separately for the additional data. Furthermore,
free_node would be need to be modified to free memory allocated to the data elements in
addition to that allocated for the tree nodes.
 


Miscellaneous Tree Functions 
 
In section 1 of this topic, we provided the fundamental functions for trees, namely 
those than construct and destroy them. There are, however, some other tree functions
that make a tree library more complete. We'll discuss a few of them here.
 


We have said that it is important to "hide" all of the details of an implementation from 
the user. With that in mind, if that user is ever going to need to check whether the tree
is empty, then having a condition (tree == NULL) would not be allowed. This implies that
the programmer knows that a NULL tree means an empty tree, and this is an implementation
detail. A more "black box" approach would be to have a
boolean function that returned a
value indicating whether the tree was empty.
 
int  is_empty(tree_t *tree)
{
return (tree == NULL);
}
 
Here we've taken the condition that the programmer would have put into the program and 
wrapped it in a function that explains what the condition does.
 
Another boolean function that applies to a condition that comes up often tells whether 
or not a given node is a leaf. The condition to check is simply whether or not a node
has any descendents. In other words, we simply need to check to see if both children
of the given node are NULL, which guarantees that it has no descendants.
 
int is_leaf (tree_t *tree)
{
return (tree != NULL && tree->left == NULL && tree->right == NULL);
}
 
Here we make use of short circuit evaluation. When evaluating the condition to return, the computer goes through each of the boolean expressions and if any one of them is false, it will return false immediately. This is how we guarantee that we never dereference a NULL pointer.
 
Another useful function is one to compute the depth of a tree. Again, as we did with the destroy_tree function, we will use recursion. We know that if the tree is empty, then the depth must be zero. Otherwise, the depth will be one more than whichever is greater, the depth of the left subtree or of the right subtree. To produce a function we simply translate these steps into C.
 
int depth (tree_t *tree)
{
int left_depth, right_depth;
if (is_empty(tree)) {
return 0;
}
left_depth = depth (tree->left);
right_depth = depth (tree->right);
return 1 + (left_depth > right_depth? left_depth : right_depth);
}
 
There is almost no end to additional helper functions you could write for trees. Doing the practice problems will hopefully trigger ideas for a few more. The three we have provided should serve as examples for all of the potential functions you may need. In addition, they should provide a framework for how to go about thinking of necessary functions. In general, you should first decide whether you need to go through the entire tree (or a section of it) to solve the function and if so you should try to think of the problem recursively.
 

 

posted on 2007-10-07 02:07  sundeepblue  阅读(282)  评论(0编辑  收藏  举报