Recursion: The Mirrors

Carrano_5e.book Page 65 Wednesday, June 14, 2006 2:40 PM CHAPTER 2 Recursion: The Mirrors T 2.1 Recursive Solutions A Recursive Valued Function: T...
Author: Amber Carson
50 downloads 0 Views 1MB Size
Carrano_5e.book Page 65 Wednesday, June 14, 2006 2:40 PM

CHAPTER 2

Recursion: The Mirrors

T

2.1 Recursive Solutions A Recursive Valued Function: The Factorial of n A Recursive void Function: Writing a String Backward

2.2 Counting Things Multiplying Rabbits (The Fibonacci Sequence) Organizing a Parade Mr. Spock’s Dilemma (Choosing k Out of n Things)

2.3 Searching an Array Finding the Largest Item in an Array Binary Search Finding the kth Smallest Item of an Array

2.4 Organizing Data The Towers of Hanoi

2.5 Recursion and Efficiency Summary Cautions Self-Test Exercises Exercises Programming Problems

ISBN: 0-558-13856-X

he goal of this chapter is to ensure that you have a basic understanding of recursion, which is one of the most powerful techniques available to the computer scientist. This chapter assumes that you have had little or no previous introduction to recursion. If, however, you have already studied recursion, you can review this chapter as necessary. By presenting several relatively simple problems, the chapter demonstrates the thought processes that lead to recursive solutions. These problems are diverse and include examples of counting, searching, and organizing data. In addition to presenting recursion from a conceptual viewpoint, this chapter discusses techniques that will help you understand the mechanics of recursion. These techniques are particularly useful for tracing and debugging recursive functions. Some recursive solutions are far more elegant and concise than the best of their nonrecursive counterparts. For example, the classic Towers of Hanoi problem appears to be quite difficult, yet it has an extremely simple recursive solution. On the other hand, some recursive solutions are terribly inefficient, as you will see, and should not be used. Chapter 5 continues the formal discussion of recursion by examining more difficult problems. Recursion will play a major role in many of the solutions that appear throughout the remainder of this book.

65

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Carrano_5e.book Page 66 Wednesday, June 14, 2006 2:40 PM

66

Chapter 2

Recursion: The Mirrors

2.1 Recursive Solutions

Recursion breaks a problem into smaller identical problems

Some recursion solutions are inefficient and impractical Complex problems can have simple recursive solutions

A binary search of a dictionary

Recursion is an extremely powerful problem-solving technique. Problems that at first appear to be quite difficult often have simple recursive solutions. Like top-down design, recursion breaks a problem into several smaller problems. What is striking about recursion is that these smaller problems are of exactly the same type as the original problem—mirror images, so to speak. Did you ever hold a mirror in front of another mirror so that the two mirrors face each other? You will see many images of yourself, each behind and slightly smaller than the other. Recursion is like these mirror images. That is, a recursive solution solves a problem by solving a smaller instance of the same problem! It solves this new problem by solving an even smaller instance of the same problem. Eventually, the new problem will be so small that its solution will be either obvious or known. This solution will lead to the solution of the original problem. For example, suppose that you could solve problem P1 if you had the solution to problem P2, which is a smaller instance of P1. Suppose further that you could solve problem P2 if you had the solution to problem P3, which is a smaller instance of P2. If you knew the solution to P3 because it was small enough to be trivial, you would be able to solve P2. You could then use the solution to P2 to solve the original problem P1. Recursion can seem like magic, especially at first, but as you will see, recursion is a very real and important problem-solving approach that is an alternative to iteration. An iterative solution involves loops. You should know at the outset that not all recursive solutions are better than iterative solutions. In fact, some recursive solutions are impractical because they are so inefficient. Recursion, however, can provide elegantly simple solutions to problems of great complexity. As an illustration of the elements in a recursive solution, consider the problem of looking up a word in a dictionary. Suppose you wanted to look up the word “vademecum.” Imagine starting at the beginning of the dictionary and looking at every word in order until you found “vademecum.” That is precisely what a sequential search does, and, for obvious reasons, you want a faster way to perform the search. One such approach is the binary search, which in spirit is similar to the way in which you actually use a dictionary. You open the dictionary—probably to a point near its middle—and by glancing at the page, determine which “half” of the dictionary contains the desired word. The following pseudocode is a first attempt to formalize this process: // Search a dictionary for a word by using a recursive // binary search

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

ISBN: 0-558-13856-X

if (the dictionary contains only one page) Scan the page for the word else { Open the dictionary to a point near the middle

Carrano_5e.book Page 67 Wednesday, June 14, 2006 2:40 PM

67

Recursive Solutions Determine which half of the dictionary contains the word if (the word is in the first half of the dictionary) Search the first half of the dictionary for the word else Search the second half of the dictionary for the word }

Parts of this solution are intentionally vague: How do you scan a single page? How do you find the middle of the dictionary? Once the middle is found, how do you determine which half contains the word? The answers to these questions are not difficult, but they would only obscure the solution strategy right now. The previous search strategy reduces the problem of searching the dictionary for a word to a problem of searching half of the dictionary for the word, as Figure 2-1 illustrates. Notice two important points. First, once you have divided the dictionary in half, you already know how to search the appropriate half: You can use exactly the same strategy that you employed to search the original dictionary. Second, note that there is a special case that is different from all the other cases: After you have divided the dictionary so many times that you are left with only a single page, the halving ceases. At this point, the problem is sufficiently small that you can solve it directly by scanning the single page that remains for the word. This special case is called the base case (or basis or degenerate case).

A base case is a special case whose solution you know

Search dictionary OR

Search first half of dictionary

Search second half of dictionary

FIGURE 2-1

A recursive solution

ISBN: 0-558-13856-X

This strategy is called divide and conquer. You solve the dictionary search problem by first dividing the dictionary into two halves and then conquering the appropriate half. You solve the smaller problem by using the same divideand-conquer strategy. The dividing continues until you reach the base case. As you will see, this strategy is inherent in many recursive solutions. To further explore the nature of the solution to the dictionary problem, consider a slightly more rigorous formulation.

A binary search uses a divide-andconquer strategy

search(in aDictionary:Dictionary, in word: string) if (aDictionary is one page in size) Scan the page for word

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Carrano_5e.book Page 68 Wednesday, June 14, 2006 2:40 PM

68

Chapter 2

Recursion: The Mirrors else { Open aDictionary to a point near the middle Determine which half of aDictionary contains word if (word is in the first half of aDictionary) search(first half of aDictionary, word) else search(second half of aDictionary, word) }

Writing the solution as a function allows several important observations: A recursive function calls itself

1. One of the actions of the function is to call itself; that is, the function search calls the function search. This action is what makes the solution recursive. The solution strategy is to split aDictionary in half, determine which half contains word, and apply the same strategy to the appropriate half.

Each recursive call solves an identical, but smaller, problem

2. Each call to the function search made from within the function search passes a dictionary that is one-half the size of the previous dictionary. That is, at each successive call to search(aDictionary, word), the size of aDictionary is cut in half. The function solves the search problem by solving another search problem that is identical in nature but smaller in size.

A test for the base case enables the recursive calls to stop

3. There is one search problem that you handle differently from all of the others. When aDictionary contains only a single page, you use another approach: You scan the page directly. Searching a one-page dictionary is the base case of the search problem. When you reach the base case, the recursive calls stop and you solve the problem directly.

Eventually, one of the smaller problems must be the base case

4. The manner in which the size of the problem diminishes ensures that you will eventually reach the base case. These facts describe the general form of a recursive solution. Though not all recursive solutions fit these criteria as nicely as this solution does, the similarities are far greater than the differences. As you attempt to construct a new recursive solution, you should keep in mind the following four questions: KEY CONCEPTS

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

ISBN: 0-558-13856-X

Four Questions for Constructing Recursive Solutions 1. How can you define the problem in terms of a smaller problem of the same type? 2. How does each recursive call diminish the size of the problem? 3. What instance of the problem can serve as the base case? 4. As the problem size diminishes, will you reach this base case?

Carrano_5e.book Page 69 Wednesday, June 14, 2006 2:40 PM

69

Recursive Solutions

Now consider two relatively simple problems: computing the factorial of a number and writing a string backward. Their recursive solutions further illustrate the points raised by the solution to the dictionary search problem. These examples also illustrate the difference between a recursive valued function — returns a value—and a recursive void function.

A Recursive Valued Function: The Factorial of n Consider a recursive solution to the problem of computing the factorial of an integer n. This problem is a good first example because its recursive solution is easy to understand and neatly fits the mold described earlier. However, because the problem has a simple and efficient iterative solution, you should not use the recursive solution in practice. To begin, consider the familiar iterative definition of factorial(n) (more commonly written n!): factorial(n) = n * (n – 1) * (n – 2) * . . . * 1 for any integer n > 0 factorial(0) = 1

Do not use recursion if a problem has a simple, efficient iterative solution An iterative definition of factorial

The factorial of a negative integer is undefined. You should have no trouble writing an iterative factorial function based on this definition. To define factorial(n) recursively, you first need to define factorial(n) in terms of the factorial of a smaller number. To do so, simply observe that the factorial of n is equal to the factorial of (n – 1) multiplied by n; that is, factorial(n) = n * [(n – 1) * (n – 2) * . . . * 1] = n * factorial(n – 1)

A recurrence relation

The definition of factorial(n) in terms of factorial(n – 1), which is an example of a recurrence relation, implies that you can also define factorial(n – 1) in terms of factorial(n – 2), and so on. This process is analogous to the dictionary search solution, in which you search a dictionary by searching a smaller dictionary in exactly the same way. The definition of factorial(n) lacks one key element: the base case. As was done in the dictionary search solution, here you must define one case differently from all the others, or else the recursion will never stop. The base case for the factorial function is factorial(0), which you know is 1. Because n originally is greater than or equal to zero and each call to factorial decrements n by 1, you will always reach the base case. With the addition of the base case, the complete recursive definition of the factorial function is

⎧1

factorial(n) = ⎨ ISBN: 0-558-13856-X

⎩ n * factorial(n – 1)

if n = 0 if n > 0

A recursive definition of factorial

To be sure that you understand this recursive definition, apply it to the computation of factorial(4). Because 4 > 0, the recursive definition states that factorial(4) = 4 * factorial(3)

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Carrano_5e.book Page 70 Wednesday, June 14, 2006 2:40 PM

70

Chapter 2

Recursion: The Mirrors

Similarly, factorial(3) = 3 * factorial(2) factorial(2) = 2 * factorial(1) factorial(1) = 1 * factorial(0) You have reached the base case, and the definition directly states that factorial(0) = 1 At this point, the application of the recursive definition stops and you still do not know the answer to the original question: What is factorial(4)? However, the information to answer this question is now available: Because factorial(0) = 1, then factorial(1) = 1 * 1 = 1 Because factorial(1) = 1, then factorial(2) = 2 * 1 = 2 Because factorial(2) = 2, then factorial(3) = 3 * 2 = 6 Because factorial(3) = 6, then factorial(4) = 4 * 6 = 24

/** Computes the factorial of the nonnegative integer n. * @pre n must be greater than or equal to 0.

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

ISBN: 0-558-13856-X

You can think of recursion as a process that divides a problem into a task that you can do and a task that a friend can do for you. For example, if I ask you to compute factorial(4), you could first determine whether you know the answer immediately. You know immediately that factorial(0) is 1—that is, you know the base case—but you do not know the value of factorial(4) immediately. However, if your friend computes factorial(3) for you, you could compute factorial(4) by multiplying factorial(3) and 4. Thus, your task will be to do this multiplication, and your friend’s task will be to compute factorial(3). Your friend now uses the same process to compute factorial(3) as you are using to compute factorial(4). Thus, your friend determines that factorial(3) is not the base case, and so asks another friend to compute factorial(2). Knowing factorial(2) enables your friend to compute factorial(3), and when you learn the value of factorial(3) from your friend, you can compute factorial(4). Notice that the recursive definition of factorial(4) yields the same result as the iterative definition, which gives 4 * 3 * 2 * 1 = 24. To prove that the two definitions of factorial are equivalent for all nonnegative integers, you would use mathematical induction. (See Appendix D.) Chapter 5 discusses the close tie between recursion and mathematical induction. The recursive definition of the factorial function has illustrated two points: (1) Intuitively, you can define factorial(n) in terms of factorial(n – 1), and (2) mechanically, you can apply the definition to determine the value of a given factorial. Even in this simple example, applying the recursive definition required quite a bit of work. That, of course, is where the computer comes in. Once you have a recursive definition of factorial(n), it is easy to construct a C++ function that implements the definition:

Carrano_5e.book Page 71 Wednesday, June 14, 2006 2:40 PM

71

Recursive Solutions * @post None. * @return The factorial of n; n is unchanged. */ int fact(int n) { if (n == 0) return 1; else return n * fact(n - 1); } // end fact

Suppose that you use the statement cout 0) { // write the last character cout