5.7 Recursive Binary-Tree Algorithms

   

The tree-traversal algorithms that we considered in Section 5.6 exemplify the basic fact that we are led to consider recursive algorithms for binary trees, because of these trees' very nature as recursive structures. Many tasks admit direct recursive divide-and-conquer algorithms, which essentially generalize the traversal algorithms. We process a tree by processing the root node and (recursively) its subtrees; we can do computation before, between, or after the recursive calls (or possibly all three).

We frequently need to find the values of various structural parameters for a tree, given only a link to the tree. For example, Program 5.17 comprises recursive methods for computing the number of nodes in and the height of a given tree. The methods follow immediately from Definition 5.6. Neither of these methods depends on the order in which the recursive calls are processed: they process all the nodes in the tree and return the same answer if we, for example, exchange the recursive calls. Not all tree parameters are so easily computed: for example, a program to compute efficiently the internal path length of a binary tree is more challenging (see Exercises 5.88 through 5.90).

Program 5.17 Computation of tree parameters

We can use simple recursive methods such as these to learn basic structural properties of trees.

 private static int count(Node h)    {      if (h == null) return 0;      return count(h.l) + count(h.r) + 1;    }  int count()    { return count(root); }  private static int height(Node h)    {      if (h == null) return -1;      int u = height(h.l), v = height(h.r);      if (u > v) return u+1; else return v+1;    }  int height()    { return height(root); } 

Another method that is useful whenever we write programs that process trees is one that prints out or draws the tree. For example, Program 5.18 is a recursive procedure that prints out a tree in the format illustrated in Figure 5.29. We can use the same basic recursive scheme to draw more elaborate representations of trees, such as those that we use in the figures in this book (see Exercise 5.85).

Figure 5.29. Printing a tree (inorder and preorder)

The output at the left results from using Program 5.18 on the sample tree in Figure 5.26 and exhibits the tree structure in a manner similar to the graphical representation that we have been using, rotated 90 degrees. The output at the right is from the same program with the print statement moved to the beginning; it exhibits the tree structure in a familiar outline format.

graphics/05fig29.gif

Program 5.18 is an inorder traversal if we print the item before the recursive calls, we get a preorder traversal, which is also illustrated in Figure 5.29. This format is a familiar one that we might use, for example, for a family tree, or to list files in a tree-based file system, or to make an outline of a printed document. For example, doing a preorder traversal of the tree in Figure 5.19 gives a version of the table of contents of this book.

Program 5.18 Quick tree-print method

This recursive program keeps track of the tree height and uses that information for indentation in printing out a representation of the tree that we can use to debug tree-processing programs (see Figure 5.29). It assumes that items in nodes are of type Item.

 static void printnode(Item x, int h)    {     for (int i = 0;i<h;i++)       Out.print(" ");     Out.println("[" + x + "]");    }  private static void showR(Node t, int h)    {      if (t == null) { printnode(null, h); return; }      showR(t.r, h+1);      printnode(t.item, h);      showR(t.l, h+1);    }  void show()    { showR(root, 0); }      

Program 5.19 Construction of a tournament

This recursive method divides an array a[l], ... , a[r] into the two parts a[l], ... , a[m] and a[m+1], ... , a[r], builds tournaments for the two parts (recursively), and makes a tournament for the whole array by setting links in a new node to the recursively built tournaments and setting its value to the larger of the values in the roots of the two recursively built tournaments.

 static class Node    { double val; Node l; Node r;      Node(double v, Node l, Node r)        { this.val = v; this.l = l; this.r = r; }    }  static Node max(double a[], int l, int r)    { int m = (l+r)/2;      Node x = new Node(a[m], null, null);      if (l == r) return x;      x.l = max(a, l, m);      x.r = max(a, m+1, r);      double u = x.l.val, v = x.r.val;      if (u > v)        x.val = u; else x.val = v;      return x;    } 

Our first example of a program that builds an explicit binary tree structure is associated with the find-the-maximum application that we considered in Section 5.2. Our goal is to build a tournament: a binary tree where the item in every internal node is a copy of the larger of the items in its two children. In particular, the item at the root is a copy of the largest item in the tournament. The items in the leaves (nodes with no children) constitute the data of interest, and the rest of the tree is a data structure that allows us to find the largest of the items efficiently.

Program 5.19 is a recursive program that builds a tournament from the items in an array. A modification of Program 5.6, it thus uses a divide-and-conquer recursive strategy: To build a tournament for a single item, we create (and return) a leaf containing that item. To build a tournament for N > 1 items, we use the divide-and-conquer strategy: Divide the items in half, build tournaments for each half, and create a new node with links to the two tournaments and with an item that is a copy of the larger of the items in the roots of the two tournaments.

Figure 5.30 is an example of an explicit tree structure that might be built by Program 5.19. Building a recursive data structure such as this one is perhaps preferable in some situations to finding the maximum by scanning the data, as we did in Program 5.6, because the tree structure provides us with the flexibility to perform other operations. The very operation that we use to build the tournament is an important example: Given two tournaments, we can combine them into a single tournament in constant time by creating a new node, making its left link point to one of the tournaments and its right link point to the other, and taking the larger of the two items (at the roots of the two given tournaments) as the largest item in the combined tournament. We also can consider algorithms for adding items, removing items, and performing other operations. We shall not consider such operations in any further detail here because similar data structures with this flexibility are the topic of Chapter 9.

Figure 5.30. Explicit tree for finding the maximum (tournament)

This figure depicts the explicit tree structure that is constructed by Program 5.19 from the input A M P L E. The data items are in the leaves. Each internal node has a copy of the larger of the items in its two children; by induction, the largest item is at the root.

graphics/05fig30.gif

Indeed, tree-based implementations for several of the generalized queue ADTs that we discussed in Section 4.7 are a primary topic of discussion for much of this book. In particular, many of the algorithms in Chapters 12 through 15 are based on binary search trees, which are explicit trees that correspond to binary search, in a relationship analogous to the relationship between the explicit structure of Figure 5.30 and the recursive find-the-maximum algorithm (see Figure 5.6). The challenge in implementing and using such structures is to ensure that our algorithms remain efficient after a long sequence of insert, remove, and other operations.

Our second example of a program that builds a binary tree is a modification of our prefix-expression evaluation program in Section 5.1 (Program 5.4) to construct a tree representing a prefix expression, instead of just evaluating it (see Figure 5.31). Program 5.20 uses the same recursive scheme as Program 5.4, but the recursive method returns a link to a tree, rather than a value. We create a new tree node for each character in the expression: Nodes corresponding to operators have links to their operands, and the leaf nodes contain the variables (or constants) that are inputs to the expression.

Figure 5.31. Parse tree

This tree is constructed by Program 5.20 for the prefix expression * + a * * b c + d e f. It is a natural way to represent the expression: Each operand is in a leaf (which we draw here as an external node), and each operator is to be applied to the expressions represented by the left and right subtrees of the node containing the operator.

graphics/05fig31.gif

Translation programs such as compilers often use such internal tree representations for programs, because the trees are useful for many purposes. For example, we might imagine operands corresponding to variables that take on values, and we could generate machine code to evaluate the expression represented by the tree with a postorder traversal. Or, we could use the tree to print out the expression in infix with an inorder traversal or in postfix with a postorder traversal.

We considered the few examples in this section to introduce the concept that we can build and process explicit linked tree structures with recursive programs. To do so effectively, we need to consider the performance of various algorithms, alternate representations, nonrecursive alternatives, and many other details. However, we shall defer consideration of tree-processing programs in further detail until Chapter 12, because we use trees primarily for descriptive purposes in Chapters 7 through 11. We return to explicit tree implementations in Chapter 12 because they form the basis of numerous algorithms that we consider in Chapters 12 through 15.

Exercises

to draw nodes. After this definition, the call node draws a black dot at the coordinates on the stack (see Section 4.3).

graphics/icon01.gif 5.86 Write a program that counts the leaves in a binary tree.

graphics/icon01.gif 5.87 Write a program that counts the number of nodes in a binary tree that have one external and one internal child.

graphics/icon01.gif 5.88 Write a recursive program that computes the internal path length of a binary tree, using Definition 5.6.

Program 5.20 Construction of a parse tree

To build a parse tree instead of just evaluating a prefix expression, we could use this recursive method instead of the eval method in Program 5.4. For simplicity, this code assumes that operands are single characters and that a Node class is defined like the one in Program 5.19, but with a char field for its data. Each call of the recursive method creates a new node with the next character from the input as the token. If the token is an operand, we return the new node; if it is an operator, we set the left and right pointers to the tree built (recursively) for the two arguments.

 static Node parse()    { char t = a[i++]; Node x = new Node(t);      if ((t == '+') || (t == '*'))        { x.l = parse(); x.r = parse(); }      return x;    } 

5.89 Determine the number of method invocations made by your program when it is computing the internal path length of a binary tree. Prove your answer by induction.

graphics/roundbullet.gif 5.90 Write a recursive program that computes the internal path length of a binary tree in time proportional to the number of nodes in the tree.

   
Top


Algorithms in Java, Part 1-4
Algorithms in Java, Parts 1-4 (3rd Edition) (Pts.1-4)
ISBN: 0201361205
EAN: 2147483647
Year: 2002
Pages: 158

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net