Lab 12: Binary trees in C

The goal of this lab is to practice using structs, pointers, and dynamic allocation in C.

1. Introduction

One advantage to object-oriented programming is how easy it is to write data structures. Data structures merely are a kind of object, and many aspects of managing the objects are built in to a language like Java. In this lab, we will transfer our knowledge of implementing data structures-- in this case, a binary tree-- to C, where we do not have object-oriented facilities. Many details handled by Java will need to be emulated or done by hand in C.

2. Set up

Copy the following files from course public directory.

cp /cslab.all/ubuntu/cs245/lab12/bintree.h .
cp /cslab.all/ubuntu/cs245/lab12/bintreedriver.c .

3. Defining a type

Open the file bintree.h in xemacs and inspect it. One common use of a header file is to define a type. Unlike in a class, the details of a struct type must be known by all modules that use it at compile time.

This file defines the struct for a node in the tree and renames the type struct Node_t to Node. Similarly BinaryTree is the typedef'd name for a struct type that is a placeholder for the root node of a tree.

The rest of the file provides prototypes for the functions--- things that would ordinarily be instance methods, but in this case must be like static methods.

Make sure you understand the structure of these types, and then look over bintreedriver.c. This program will test out the functions you write to implement the various binary tree operations. It repeatedly asks the user for values and inserts those values into the tree in a way to maintain it as a binary search tree. It then twice looks for values supplied by the user (I have this running two times so you that you can test it out both on a value that is in the tree and a value that isn't.) Then we print the tree as a depth-first in-order traversal. Finally, we destroy the tree.

4. Making a tree

Open a new file, bintree.c, in which you will implement the functions prototyped in bintree.h. Include bintree.h for the types and function prototypes and include stdlib.h for malloc().

Since BinaryTree is not a class, it does not have a constructor. While a constructor is written to initialize the instance variables of a class, instantiating a class does a bit more than what the constructor contains: it allocates memory for the new object.

In lieu of a constructor, implement the function makeTree(). This needs to allocate memory for the new tree (the address of which it will eventually return) and it needs to initialize the tree's field-- in this case, we're starting it out as an empty tree, so we make the root null.

You won't be able to test these functions very well until they are all written, but you should compile as you go along.

gcc -c bintree.c

Remember that the -c flag means "compile only" ("don't link or make an executable").

5. Adding nodes to the tree

Now we want to write a function which, given a value, will make a new node containing that value and place it in the right place in the tree, assuming that this tree is and will remain a binary search tree.

There is more than one way to do this; however, two observations: (1) You'll probably want to do it recursively (it's more "elegant" this way), and (2) you'll probably want some helper functions. Here are some pieces in the process:

First, at some point you will need to make a new node. So, just as we had a makeTree() function for trees, you probably should write a makeNode() function (or something similarly named) which will allocate a node and initialize its fields.

Second, we can restate this problem in terms of nodes instead of trees: given a node and a value, make a new node of the value and place it in the proper place in the subtree rooted at the given node. This is nicely solved recursively.

Finally, if the two above pieces are in place, then the implementation of add is just a matter of starting the recursion going on the root node-- if that node is not null.

Implement add() and any necessary helper functions. Remember to put the helper functions first, since the C compiler must see them before they are used in add().

6. Testing if a tree contains a value

Implementing contains() is straightforward once you have figured out how to add a value to a tree. Implement this and compile.

7. Performing an in-order traversal

Next, implement an in-order depth first traversal. I've chosen this because in a binary search tree, that will result in printing the numbers as a sorted list. I recommend also implementing this recursively, with a recursive helper function for visiting a node; let the function printInOrder(), then, simply start the recursive process going, and print a new line at the end. (Don't print a new line for every node; let the list appear on one line.)

Also, you'll need to include stdio.h.

8. Destroying a tree

Finally, we want to deallocate all the memory we are using in this tree. This is work that would be done by the garbage collector in Java. (In this case, since we've come to the end of the program, deallocating isn't much of an issue; all the memory the program is using gets returned to the available memory when the program exits anyway. I'm making you do the deallocation just for the practice.)

It might seem like a simple matter to write destroyTree(). Saying free(tree) will return the chunk of code for the tree to the list of available memory. However, it's not that simple; doing that would not return the memory for all the individual nodes to the available portion. Instead, we need to traverse the tree, deallocating nodes from the leaves up. Once again, a recursive helper function would be an appropriate solution.

To conform to standard practice, after you deallocate the memory stored in a pointer, you should set that pointer to NULL. It would be better to get a segmentation fault (a traceable error) if you were ever to reuse that variable than to have two pointers refering to the same piece of memory for completely different purposes (an error very difficult to trace).

9. Testing

Now compile the driver.

gcc -c bintreedriver.c
gcc bintreedriver.o bintree.o -o bintreedriver

Test, and don't be surprised if you get a segmentation fault. Fix it up until you're sure it works.

10. Turn in

Turn in a hard copy. In your typescript, make sure you search for one value that is in the tree and one value that isn't (so you get both a positive and a negative response from contains()).


Thomas VanDrunen
Last modified: Thu Apr 15 14:16:12 CDT 2010