Previous | Table of Contents | Next

Take a few minutes to look over Listing 59.4 and relate it to Listing 59.2. The structure is different, but upon examination it becomes clear that both listings reflect the same underlying model: For each node, visit the left subtree, visit the node, visit the right subtree. And although Listing 59.4 is longer, that’s mostly because I commented it heavily to make sure its workings are understood; there are only 13 lines that actually do anything in Listing 59.4.

Let’s look at it another way. All the code in Listing 59.2 does is say: “Here I am at a node. First I’ll visit the left subtree if there is one, then I’ll visit this node, then I’ll visit the right subtree if there is one. While I’m visiting the left subtree, I’ll just push a marker on a stack that tells me to come back here when the left subtree is done. If, after visiting a node, there are no right children to visit and nothing left on the stack, I’m finished. The code does this at each node—and that’s all it does. That’s all Listing 59.4 does, too, but people tend to get tangled up in pushes and pops and while loops when they use data recursion. When the implementation model changes to one with which they are unfamiliar, they abandon the perfectly good model they used before and try to rederive it in the new context by the seat of their pants.

| Here’s a secret when you’re faced with a situation like this: Step back and get a clear picture of what your code has to do. Omit no steps. You should build a model that is so consistent and solid that you can instantly answer any question about how the code should behave in any situation. For example, my interviewees often decide, by trial and error, that there are two distinct types of right children: Right children visited after popping back to visit a node after the left subtree has been visited, and right children visited after descending to a node that has no left child. This makes the traversal code a mass of special cases, each of which has to be detected by the programmer by trying out scenarios. Worse, you can never be sure with this approach that you’ve caught all the special cases.
| The alternative is to develop and apply a unifying model. There aren’t really two types of right children; the rule is that all right children are visited after their parents are visited, period. The presence or absence of a left child is irrelevant. The possibility that a right child may be reached via different code paths depending on the presence of a left child does not affect the overall model. While this distinction may seem trivial it is in fact crucial, because if you have the model down cold, you can always tell if the implementation is correct by comparing it with the model.

#### Measure and Learn

How much difference does all this fuss make, anyway? Listing 59.5 is a sample program that builds a tree, then calls WalkTree () to walk it 1,000 times, and times how long this takes. Using 32-bit Visual C++ 1.10 running on Windows NT, with default optimization selected, Listing 59.5 reports that Listing 59.4 is about 20 percent faster than Listing 59.2 on a 486/33, a reasonable return for a little code rearrangement, especially when you consider that the speedup is diluted by calling the Visit() function and by the cache miss that happens on virtually every node access. (Listing 59.5 builds a rather unique tree, one in which every node has exactly two children. Different sorts of trees can and do produce different performance results. Always know what you’re measuring!)

Listing 59.5 L59_5.C

    // Sample program to exercise and time the performance of
    // implementations of WalkTree().
    // Tested with 32-bit Visual C++ 1.10 under Windows NT.
    #include <stdio.h>
    #include <conio.h>
    #include <stdlib.h>
    #include <time.h>
    #include “tree.h”
    long VisitCount = 0;
    void main(void);
    void BuildTree(NODE *pNode, int RemainingDepth);
    extern void WalkTree(NODE *pRootNode);
    void main()
    {
       NODE RootNode;
       int i;
       long StartTime;
       // Build a sample tree
       BuildTree(&RootNode, 14);
       // Walk the tree 1000 times and see how long it takes
       StartTime = time(NULL);
       for (i=0; i<1000; i++)
       {
          WalkTree(&RootNode);
       }
       printf(“Seconds elapsed: %ld\n”,
               time(NULL) - StartTime);
       getch();
    }
    //
    // Function to add right and left subtrees of the
    // specified depth off the passed-in node.
    //
    void BuildTree(NODE *pNode, int RemainingDepth)
    {
       if (RemainingDepth == 0)
       {
          pNode->pLeftChild = NULL;
          pNode->pRightChild = NULL;
       }
       else
       {
          pNode->pLeftChild = malloc(sizeof(NODE));
          if (pNode->pLeftChild == NULL)
          {
             printf(“Out of memory\n”);
             exit(1);
          }
          pNode->pRightChild = malloc(sizeof(NODE));
          if (pNode->pRightChild == NULL)
          {
             printf(“Out of memory\n”);
             exit(1);
          }
          BuildTree(pNode->pLeftChild, RemainingDepth - 1);
          BuildTree(pNode->pRightChild, RemainingDepth - 1);
       }
    }
    //
    // Node-visiting function so WalkTree() has something to
    // call.
    //
    void Visit(NODE *pNode)
    {
       VisitCount++;
    }

Previous | Table of Contents | Next

Graphics Programming Black Book © 2001 Michael Abrash