CS-211 Fall 2015 Lab 9 Trees
This week, we extend last weeks lab, which used a structure to dynamically create a linked list of nodes. This week, we are going to do something very similar, but this time, we will put the nodes into a tree structure rather than a linked list. We have already seen a tree structure in project 2, where we turned a boolean expression into an expression tree. In this lab, we will again use a tree, where every node of the tree may have a left and right sub-tree. We keep track of the entire tree by keeping a pointer to the root of the tree (just like we kept track of linked lists by keeping a pointer to the head of the list.) As in linked lists, we can use dynamic allocation (malloc) to create a new instance of a tree node, and that node can be inserted anywhere in the tree by manipulating the "left" and "right" pointers in the tree.
For this lab, we will be creating an ordered binary tree. The payload part of each node in that tree, like last week, will consist of two integers - the first is the "value" of the node, just some arbitrary number; and the second is a "count", which counts the number of occurences of the value.
We are also going to impose a new rule on our tree... namely that the values in all nodes in the left sub-tree of a node will be less than the value in that node, and the values in all the node in the right sub-tree of a node will be greater than the value in that node.
Our tree list will be created from an arbitrary, unsorted list of integers read in from standard input.
After we have created our tree, we will print out the values in the tree in numericcal order, along with the counts.
Download the code that has been started for you in lab9.tar.gz. Use the command tar -xvzf lab9.tar.gz. This will unpack the tar file and make a new lab9 subdirectory. The lab9 directory will have a Makefile, and the C code to start from in file tree.c
The tree.c File
The tree.c file starts of with the standard includes for standard library functions, a definition of the tnode structure, a typedef for a tree data type, and prototypes for the eight lower level functions in the file.
What follows is just the normal function definitions that we are used to seeing. I've included a description of each of these functions below.
- int main() The main function performs the following actions:
You don't have to change the main function for this lab.
- Declares a root variable to point to the first node in our linked list. Since our linked list starts out empty, root is initialized to NULL.
- Declares a nextVal integer variable to hold the next value read from standard input
- Invokes the scanf standard library function, which reads then next number from standard input, and puts the value of that number in the nextVal variable. If scanf doesn't find a number on standard input, it returns a zero value. For each value that is read, the main function performs the following actions...
- If root is null, main simply makes a node from nextVal, and calls that node the root. If root is not NULL, main invokes the insertTree function, passing in as arguments the value read from standard input, and a pointer to the root of our linked tree. The insertTree accounts for the new value by adding it to the tree, somewhere below the root, or updating the count for an item already in the tree.
- Checks to make sure that the resulting tree is still in sort order. (As long as insertTree works correctly, our list should always be sorted... so this check just makes sure isertTree is working correctly.) It makes this check by invoking the orderedTree function, and asserting that the result of that function should always be true.
- When main is finished reading all the input values, it prints a message that says "Finished building our tree"
- main then invokes the printTreeStructure function. This is so you can get a visual representation of what the tree looks like.
- Then main invokes the printTree function to print out the results our our tree in numerical order However, this function is not yet coded.
- Finally, now that main is finished with our tree, it invokes the freeTree function to free all the nodes we have accumulated in our tree.
- void printTree(tree root) The printTree function takes the pointer to the root of the tree as the root argument, and is responsible for printing out the values and the counts of all the nodes in the tree in ascending value order. However, the printTree function has not yet been implemented. Your job is to write this function and test it to make sure it works. For hints on how to do this, see the hints section below.
The remaining functions have already been coded for you. You do not need to modify these functios.
- void insertTree(int val,tree root) The insertTree function has already been coded for you this week. This function creates a new node for val if it does not already appear in the tree. If val is in the tree, then insertTree adds one to the count for the matching node. It turns out that this implementation is pretty simple. If the input value matches the value at the root node, we just increment the count in the root node, and return. If the input value is less than the value of the root node, then we know this value must go in the left sub-tree of the root. If there is no left sub-tree, the input value becomes the left sub-tree. If there is a left sub-tree, we invoke insertTree recursively on the left sub-tree. If the input value is not less than or equal to the root node value, it must be greater than the root node value, and we do exactly the same thing on the right sub-tree. You don't have to change the insertTree function for this lab.
- tree makeTnode(int val) The makeTnode function dynamically allocates the memory required for a new instance of the tnode structure, and initializes the fields in that structure. It initializes the value field to whatever was passed in as the "val" argument. It initializes the count field to one (assuming this is the first istance of this value we have seen), and it initializes the left and right pointer fields to NULL (assuming that we will hook up the left and right pointers outside of this function.) The makeTnode function finally returns a pointer to this new instance.
- void printNode(tree node) The printNode function prints out a line of text relating to the specific node passed in as an argument to the printNode function. It is expected that you will call the printNode function from the printTree function to print out information about specific nodes of the tree.
The next three functions work on an entire binary tree.
- void freeTree(tree root) This function takes the pointer to the root of a tree as the root argument. As long as root is not null, the function frees the left sub-tree by invoking freeTree recursively on the left sub-tree, frees the right sub-tree by invoking freeTree recursively on the right sub-tree, then frees the memory associated with the root node of the tree. When this function is complete, all the nodes in the tree will be freed.
- int orderedTree(tree root) This function checks to make sure that the tree whose head is the root argument has the ordered tree property. It does this as follows... if the root has a left-subtree, orderedTree invokes itself recursively on the left sub-tree to make sure the left sub-tree is correctly ordered. If not, orderedTree returns a zero. If it is, then orderedTree checks to make sure that the root of the left sub-tree is less than the value at the root node. If not, the tree is not ordered, an orderedTree returns a zero. If so, then the same process happens on the right sub-tree (if there is a right sub-tree). The right sub-tree is checked to make sure it is ordered, and if so, the value at the root of the right sub-tree must be greater than the value at the root. If all of these checks pass, then the tree is ordered, and orderedTree returns a 1.
- printTreeStructure(tree root,char *prefix,int up,int down) This function prints out he values in a tree in a graphical way to show what the tree looks like. The printout from this function turns the tree on it's side, with the root on the left and the leaves of the tree on the right. The implementation of this function is fairly complicated, but if you have some extra time, there is nothing in this implementation we haven't studied... check it out and see if you ccan figure out what is going on.
The last function is a utility function used only by the printTreeStructure function
- void blanks(char *buf,int n) The blanks function fills in the character buffer passed in as the buf argument with n blanks, followed by a null terminator.
Using The Makefile
The Makefile file in the lab9 directory contains "targets" which allow you to do several things using the make command. The following targets are supported:
- make tree - Makes the tree executable if you have changed the source code.
- make test - Makes the tree executable if you have changed the source code, and then runs that executable using redirection to feed the contents of the input.txt file into standard input.
- make clean - Removes the tree executable.
Printing an Ordered Tree in Order
If you haven't realized it yet, it turns out that tree data structures are very easy to handle if you think about them recursively. In other words, if you are given the root node of the tree, and can figure out how to invode your function on the left and right sub-trees, and also how to deal with the root node of the tree, you are all set.
First, let's handle the special case... where the pointer to the root node is a NULL pointer. In that case, the tree is empty, and we don't want to print anything. So, to handle this special case, if the root is NULL, just return. (We don't have to return a value because the return type of this function is "void".
If the root node is not NULL, we know it is the root of an ordered tree, and we want to print that tree in order. Since every value in the left sub-tree of the root node is less than the value of the root, why not just invoke the printTree function recursively on the left sub-tree? Even if the left sub-tree is empty, we've already handled empty trees in our printTree function, so we can invoke printTree on the left sub-tree no matter what its value is.
Once we have printed out everything in the left sub-tree, then we can print out the value at the root node... we know that the root node is the next highest value after the left sub-tree. We can use the printNode function that has been defined for us to print out the value of the root onode.
After printing out the root node, we know that every value in the right sub-tree of the root is greater than the value of the root, so we can invoke printTree recursively on the right sub-tree to print out any remaining values. If the right sub-tree is NULL, no problem... we've already coed printTree to handle that case.
After printing out the left sub-tree, the root node, and the right sub-tree, that's everything in the tree, so we are done.
Checking for Memory Leaks
After you have written and tested your implentation of the printTree function, you can double check to see if the professor messed up the tree command by introducing memory leaks. To do this, we will use the valgrind command. To run valgrind, execute the following command:
valgrind --leak-check=full ./tree <input.txt
If the professor implemented insertTree correctly, you should get no memory leaks, and see the message at the end of the valgrind invocation that includes:
All heap blocks were freed -- no leaks are possible
Download and edit the following file: lab9_report.txt. Then submit your editted file on Blackboard in the Lab_09 report area.