22. Trees & Tree Traversal

Thursday November 19, 1998

[Previous Lecture] [Next Lecture]

Assigned Reading:  online notes on trees

Handouts (available on-line):

Programs from this lecture:

Topics Covered:

• We discussed terminology of general trees as well as binary trees. Most of the definitions appear in the online reading, so I won't repeat them here. We considered the UNIX file system as an example of a tree. The terminology for trees differ from textbook to textbook. You should become familiar with the terminology rather than attempt to memorize the exact terminology. One important thing to note is that in a full binary tree, the number of nodes at level i is 2i --- i.e., the number of nodes increases exponentially. Also, a full binary tree of height i has 2i+1-1 nodes and 2i (or roughly half) of these are leaves. Thus, full binary trees are very much bottom heavy. This becomes important when we discuss binary search trees.

• There are three standard methods for visiting every node in a binary tree: inorder traversal, preorder traversal and postorder traversal. (Actually, there are more than three ways to visit every node, but these are the three we will discuss for now.) These traversals are defined recursively:

• Inorder traversal: recursively use inorder traversal on the left subtree, visit the current node, and recursively use inorder traversal on the right subtree.

• Preorder traversal: First visit the current node. Then, recursively use preorder traversal on the left subtree and the right subtree (in that order).

• Postorder traversal: First, recursively use postorder traversal on the left and right subtrees. Finally, visit the current node.

• We discussed expression trees --- i.e., how to store an arithmetic expression as a binary tree (more in Lecture 24. Inorder, preorder and postorder traversal corresponds to infix, prefix and postfix notation for arithmetic.

• A completely different topic: throwing exceptions. When we write large complicated programs, we might encounter a situation where the place that an error is detected is not the place where the error should be handled. One example of this is when an error is detected inside many nested function calls and we want to handle the error in the main program. We could exit the program, but that is often undesirable. We could return an error condition as a return value from each function, but that is cumbersome. Every time a function is called, we must check the return value for the error condition. The alternative is to "throw an exception". To do this we would have a statement that looks like: throw(e) ;

Here, e can be an object of any type, but it is customary to define a class just for the purpose of throwing an exception. In order for this throw statement to have the intended behavior, we must have a try-catch block in an "active" function. Here, an "active" function might be the function that contains the throw statement, or a function that called the function that contains the throw statement, or a function that called that function, etc. That is, the currently active functions are the main() function and all the functions that have been called, but have not yet returned. At least one of these functions, must have a try-catch block that looks like:

try { // ... some code that eventually calls the function with // the throw statement } catch(Error& e) { // ... code to handle the error condition } The try block can be followed by several catch blocks. Each catch block must have a different signature. The catch block whose parameter matches that of the object that is thrown ( e in this case ) is the one that is invoked. A catch block that has ... as its parameter, will catch any type of exception.

• Next we look at an example of a program that throws an exception. In this program, the function foo() is recursive. The function foo() throws an exception if its parameter is negative. The sample run shows that after an exception is thrown, the rest of the function is bypassed and control is given directly to the catch block in the main program.

• Back to trees. We declare a binary tree template class: tree.h. Note that the Tree class itself is just a pointer to TreeNode and TreeNode actually contains the data. A TreeNode contains two Trees, a left subtree and a right subtree. This declaration has the advantage that an empty tree is really an object. You will see tree classes in other textbooks where the empty tree is represented as a NULL pointer. This is undesirable because in many cases we want an empty tree to be an actual object that we can work with. This is particularly the case when we look at binary search trees. (Also see implementation of the Tree template class.)

• One new feature of the Tree and TreeNode classes is that we overload the new and delete operators. When a new Tree or TreeNode object is dynamically created using the new operator, the programmer-defined new operator is used if it exists. The new operator should allocate enough memory to hold an object of that type. The size of the object is given as a parameter to new, so it is relatively simple to call malloc() to get this memory. If malloc() returns the NULL pointer, then we throw an exception. This is preferable to exiting the program, because it gives the client an opportunity to catch the exception and do something reasonable (try again, close files, deallocate memory).

• A simple main program shows how to use the Tree class to construct a company's organization chart. Note the use of the try and catch blocks in main(). Sample run.

[Previous Lecture] [Next Lecture]