Binary Trees,
Recursion Elimination,
and Enumerations

  1. Here is a program technically in C++, but really entirely in the traditional C style. It defines a simple binary tree and functions for inserting, searching, and printing all the contents in order. Notice that the insert function takes and old tree as a parameter and returns the new tree as its result; this is to avoid reference parameters which are specific to C++, and too many pointers. This is what happens when it is run.
  2. Here is an alternative version of the print function; it makes the structure of the tree visible with parentheses (note how the original version shows the order of the items, but gives no hint of the shape of the tree). This is what happens when it runs. The output is not nice to look at, but at least all the information is there.
  3. Here is a cleverer version of the print function, it actually draws the tree (sideways). Before each string, an extra character appears, = to indicate the root, and / or \ to show where to draw the lines that connect a node to its parent. Here is the output. It is easier to understand if you see the output with the complete lines drawn in, as in this graphic.
  4. This version prints the tree the right way up. Although the output produced looks more tree-like, it is not at all easy to put in the special little characters / and \ to indictae which way the lines should be drawn. This print function uses a kind of breadth-first scan to run through the tree layer by layer.
    Here is a slightly more sophisticalted version that doesn't need to be told the maximum number of levels to print.

  5. Here is a version of the insert function that uses reference parameters. This makes it able to modify the node-pointer passed in, and thus removes the need to return a result. Unfortunately reference parameters are not available in plain C.
  6. This is what happens if that version of the function is written in plain C. Reference parameters have to be converted to pointers, so references to pointers become double pointers. Every time a function is called, the address of the parameter must be provided (see how that messes up the recursive calls). Every time the parameter is used, an extra pointer must be followed.

  7. This shows how to remove a string from the tree. At first sight, removing seems simple: just reverse what was done to add in the first place. But remember that things are only added at the bottom level of the tree. Removing something from the bottom level of a tree is as easy as you'd expect, but removing something from the middle of the tree is more tricky.
    This is how it works: To remove something that has no children, just remove it. To remove something that has only one child (even if that child is itself a whole big subtree) let the child replace the removed node. To remove something that has two children, we can't leave a gap in the tree, so find something from the bottom level of the tree (easy to remove) that could replace the removed thing, and use it as a replacement. The smallest thing that is larger than the item to be removed is guaranteed not to have two children, so is a suitable replacement.

  8. This shows the basic idea behind recursion elimination. We simply simulate what the computer does with a normal recursive function. Make our own version of the stack, the "LittleNote" objects exactly reflect the computer's stack frames, containing values of local variables and a note of where to continue execution when a function call is complete.
  9. This version is exactly the same as the above, except it is based on a nicer, simpler version of the recursive print function.
  10. This takes the above version a little further. Instead of having a non-recursive print function, we now have a non-recursive general tree-exploring function, which a now-trivial print function makes use of.
  11. That idea, of a non-recursive function that can be used whenever needed to get the next data item from a tree (or any other data structure) is a very useful little thing, known as an Enumeration, shown here. To run through the data stored in a tree, just create an enumeration for that tree, then whenever more data is needed just ask the enumeration if it still has data to give, and if it has, get that data from it.
  12. Finally, this shows non-recursive enumerations doing something that the original recursive functions could never do: we have two totally independent trees, and take data items from them one at a time, so that the first item from one tree is paired with the first item from the other tree, etc., etc. Trivial with an enumeration, almost impossible with recursion.

  13. Object orienting a binary tree definition takes a little thought. Here is the beginning of a naive attempt. Everything seems OK until we find that dealing with the empty tree (i.e. NULL) gets very unpleasant. This version of insert wasn't going to be completely successful.
  14. Here we see a much happier object oriented binary tree. A simple little object StringTreeNode represents all the structure of the tree and holds all the data, and in fact works exactly as the non-object-oriented versions did. Then a special wrapper object StringTree just contains one pointer to a StringTreeNode and has all the expected methods. Thus when the tree is empty, and we have no StringTreeNodes, we will still have a StringTree object to hold all the methods and gracefully continue to work.