48
Chapter 11 – Recursion Recursive Processes Writing a Recursive Method A Recursive Factorial Method Comparison of Recursive and Iterative Solutions Recursive Method Evaluation Practice Binary Search Merge Sort Towers of Hanoi Drawing Trees with a Fractal Algorithm Performance Analysis 1

# Chapter 11 – Recursion Recursive Processes Writing a Recursive Method A Recursive Factorial Method Comparison of Recursive and Iterative Solutions Recursive

Embed Size (px)

Citation preview

Chapter 11 – Recursion

Recursive Processes Writing a Recursive Method A Recursive Factorial Method Comparison of Recursive and Iterative Solutions Recursive Method Evaluation Practice Binary Search Merge Sort Towers of Hanoi Drawing Trees with a Fractal Algorithm Performance Analysis

1

Recursive Processes

A recursive process calls itself. It’s a picture in a picture.

2

Relayed Flag Signals

Imagine a line of old-time warships. The admiral at one end wants confidential casualty

information. A flag signal can be read only by an adjacent ship.

Transmitted signal: “Report your casualties plus casualties in all further-away

ships.” Returned signal:

“My casualties plus casualties in all further-away ships are ...”

3

Writing a Recursive Method

Determine how to split the problem into successively smaller sub-problems.

Identify the stopping condition or base case. The calling process must not skip over the stopping

condition, and it is not good enough to just approach it asymptotically. It must actually reach it.

The recursive method body needs an if statement whose condition is the stopping condition:

if (<stopping-condition>)

Solve local problem and return.else

Make next recursive call(s).Process information returned by recursive calls and

return.

4

A Recursive Factorial Method

Formula for the factorial of a number n:n! = n * (n-1) * (n-2) * ... * 2 * 1

Example:5! = 5 * 4 * 3 * 2 * 1 = 120

Likewise:4! = 4 * 3 * 2 * 1 = 24

Therefore:5! = 5 * 4!

And by induction:n! = n * (n-1)!

This formula is the recurrence relation for a factorial. The stopping condition is when n equal 1.

5

A Recursive Factorial Method

10 public static void main(String[] args)11 {12 System.out.println(factorial(5));13 } // end main1415 public static int factorial(int n)16 {17 int nF; // n factorial18 if (n == 0 || n == 1)19 {20 nF = 1;21 }22 else23 {24 nF = n * factorial(n-1);25 }26 return nF;27 } // end factorial

6

Recursive Factorial Method Trace

Factorial

line#

factorial factorial factorial factorial factorial

n nF n nF n nF n nF n nF output12 5 ?                  24     4 ?              24         3 ?          24             2 ?      24                 1 ?  20                   1  24               2      24           6          24       24              24   120

12                     120

7

Cleaner Recursive Factorial Method

We used the local variable, nF, just to give substance to the trace.

In practice, that local variable is not necessary, and we would simplify the method to this:

public static int factorial(int n) { if (n == 0 || n == 1) { return 1; } else { return n * factorial(n-1); } } // end factorial

8

An Iterative Factorial Method

Alternately, we can reverse the order of multiplication and write the formula for a factorial like this: n! = 1 * 2 * ... * (n-2) * (n-1) * n

This suggests an iterative solution like this:

public static int factorial(int n) { int factorial = 1; // the factorial value so far

for (int i=2; i<= n; i++) { factorial *= i; } return factorial; } // end factorial

9

Characteristics of Recursive Solutions

All recursive programs can be converted to iterative programs that use loops instead of recursive method calls.

With some problems, a recursive solution is more straightforward than an iterative solution.

But recursive calls take a lot of overhead. The computer must:

Save the calling module’s local variables. Find the method. Make copies of call-by-value arguments. Pass the arguments. Execute the method. Find the calling module. Restore the calling module’s local variables.

Recursion uses a built-in stack, and if there are too many recursive calls, this stack can overflow.

10

Different Kinds of Recursions

Mutual recursion is when two or more methods call each other in an alternating cycle of method calls.

Binary recursion is when a method’s body includes two (or more) recursive calls to itself and the method executes both calls.

Linear recursion is when a method executes just one recursive call to itself, as in a factorial recursion.

Tail recursion is a special case of linear recursion. It’s when a recursive method executes its recursive call as its very last operation.

The relayed flag signals and the recursive factorial calculation are not tail recursions, because other operations occur after the recursive calls. For example, in

return n * factorial(n-1); a multiplication by n occurs after the recursive call. Tail recursions are the easiest to convert to iterations.

11

Converting from Iteration to Recursion

Suppose you have a method that uses iteration to print a string in reverse order, like this:

private static void printReverseMessage(String msg) { int index; // position of character to be printed index = msg.length() - 1; while (index >= 0) { System.out.print(msg.charAt(index)); index--; } } // end printReverseMessage

12

Converting from Iteration to Recursion

Since the iteration starts at the end and prints the current character before decrementing the character index, substitute a recursive method that prints the last character in a string before making a recursive call with the substring before that printed character:

private static void printReverseMessage(String msg) { int index; // position of last character in msg

if (!msg.isEmpty()) { index = msg.length() - 1; System.out.print(msg.charAt(index)); printReverseMessage(msg.substring(0, index)); } } // end printReverseMessage

13

Recursive Evaluation Practice

Here’s a compact description of the factorial algorithm, using recurrence relations described in terms of mathematical functions:

f(n) =

n * f(n-1)1

for n > 1for n <= 1

To evaluate a recurrence relation by hand, use this procedure:

Write the algorithm in function notation (as shown above). For the first line of your recursion trace, write the recurrence

relation with the variables replaced by the initial numbers. Under that, write the recurrence relation for the first

subordinate method call with the variables replaced by appropriately altered numbers on both left and right sides of the equations.

Continue like this until you reach a stopping condition. On subsequent rows, re-write what you previously wrote on

the rows above but in reverse order, replacing right-side unknowns with known values as you go.

14

Recursive Evaluation Practice − Factorial

f(5) = 5 * f(4) f(4) = 4 * f(3) f(3) = 3 * f(2) f(2) = 2 * f(1) f(1) = 1 f(2) = 2 * 1 ⇒ 2 f(3) = 3 * 2 ⇒ 6 f(4) = 4 * 6 ⇒ 24f(5) = 5 * 24 ⇒ 120

call sequence

return sequence

stopping condition

stopping condition

Factorial

15

Recursive Evaluation Practice − Bank Balance

B(n, D, R) =D + (1 + R) * b(n-1) for n >= 10 for n < 1

b(3, 10, 0.1) = 10 + 1.1 * b(2, 10, 0.1) b(2, 10, 0.1) = 10 + 1.1 * b(1, 10, 0.1) b(1, 10, 0.1) = 10 + 1.1 * b(0, 10, 0.1) b(0, 10, 0.1) = 0 b(1, 10, 0.1) = 10 + 1.1 * 0 ⇒ 10 b(2, 10, 0.1) = 10 + 1.1 * 10 ⇒ 21 b(3, 10, 0.1) = 10 + 1.1 * 21 ⇒ 33.1

call sequence

return sequence

stopping condition

stopping condition

Bank Balance

Next, consider a function that returns the balance, b, in a bank account after n = 3 equal periodic deposits of amount D = 10, with interest rate R = 0.1 for the time period between deposits. Here’s the recurrence relation:

16

Recursive Evaluation Practice with the Recurrence Relation Expressed as a Generic Math Function

Here’s a practice problem whose function has two parameters – x and y:

f(x, y) =

f(x-3, y-1) + 2f(y, x)0

x > 0, x > y x > 0, x <= yx <= 0f is a generic name for a function. To see how this works, evaluate

f(5, 4):

f(5, 4) = f(2, 3) + 2 f(2, 3) = f(3, 2) f(3, 2) = f(0, 1) + 2 f(0, 1) = 0 f(3, 2) = 0 + 2 ⇒ 2 f(2, 3) ⇒ 2 f(5, 4) = 2 + 2 ⇒ 4

Generic Example 1

call sequence

return sequence

stopping condition

stopping condition

17

Generic Recursive Evaluation Practice − Continued

Sometimes stopping conditions are not adequate. For example, suppose you have this recurrence relation:

Here’s how the evaluation of f(4, 3) would go:

f(x, A) =

x > 1x = 1

A * f(x-2, A)A

f(4, 3) = 3 * f(2, 3) f(2, 3) = 3 * f(0, 3) f(0, 3) = ?

Generic Example 2

call sequence

This skips the indicated stopping condition, and the recursion becomes unspecified.

This skips the indicated stopping condition, and the recursion becomes unspecified.

18

Generic Recursive Evaluation Practice − Continued

This example uses inequality, but it never reaches the stopping condition:

Here’s an example where the stopping condition is a maximum. That’s OK, but there’s still a problem. Do you see the problem?

f(x) = x > 0x <= 0

f(x/2)0

f(x) =

x < 3x >= 3

f(x) + 14

The function gets larger, but the stopping condition does not look at the function. It just looks at x, and x never changes.

19

Starting from the Stopping Condition − the Fibonacci Sequence.

If all the useful work occurs in the return sequence, it’s a head recursion, and you can jump immediately to the stopping condition and evaluate from there.

f(n) = f(n-1) + f(n-2)10

n > 1n = 1n = 0

f(0) = 0f(1) = 1f(2) = f(1) + f(0) = 1 + 0 ⇒ 1 f(3) = f(2) + f(1) = 1 + 1 ⇒ 2 f(4) = f(3) + f(2) = 2 + 1 ⇒ 3 f(5) = f(4) + f(3) = 3 + 2 ⇒ 5 f(6) = f(5) + f(4) = 5 + 3 ⇒ 8 ...

straightforward evaluation

Fibonacci Sequence

20

Binary Search

Suppose you want to find the location of a particular value in an array. With a sequential search, the number of steps equals the array length. If the array is already sorted, you can use a binary search, and then the

number of steps is only log2(length). A binary search uses “divide-and-conquer”:

Divide the array into two nearly equally sized sub-arrays, determine which sub-array contains the item if it is present, divide that sub-array into two equally sized sub-arrays, and so forth, until the sub-array has only one element.

Binary-search recursive method: Parameters: the whole array, first and last indices of the sub-array, the target value. If first equals last, stop and see if that one element is the target value. Otherwise, recursively call the sub-array which might contain the target value.

21

Binary Searchpublic static int binarySearch( int[] arr, int first, int last, int target) { int mid, index; System.out.printf("first=%d, last=%d\n", first, last); if (first == last) // stopping condition { if (arr[first] == target) return first; else return -1; } else // continue recursion { mid = (last + first) / 2; if (target > arr[mid]) first = mid + 1; else last = mid; return binarySearch(arr, first, last, target); } } // end binarySearch

22

Binary Search public static void main(String[] args) { int[] array = new int[] {-7, 3, 5, 8, 12, 16, 23, 33, 55}; System.out.println(BinarySearch.binarySearch( array, 0, (array.length - 1), 23)); System.out.println(BinarySearch.binarySearch( array, 0, (array.length - 1), 4)); } // end main

Sample session:first=0, last=8first=5, last=8first=5, last=6first=6, last=66first=0, last=8first=0, last=4first=0, last=2first=2, last=2-1

23

Merge Sort

For a binary search to work, the array must be already sorted. When an array is large, a merge sort is a relatively efficient sorting technique.

A merge sort also used “divide-and-conquer”, but instead of making a recursive call for just one of each half, it makes recursive calls for both of them.

Each recursive call divides the current part in half, until there is only one element, which represents the stopping condition for that recursive branch.

Return sequences recombine elements by merging parts, two at a time, until everything is back together.

24

Merge Sort Method

public static int[] mergeSort(int[] array) { int half1 = array.length / 2; int half2 = array.length - half1; int[] sub1 = new int[half1]; int[] sub2 = new int[half2];

if (array.length <= 1) { return array; } else { System.arraycopy(array, 0, sub1, 0, half1); System.arraycopy(array, half1, sub2, 0, half2); sub1 = mergeSort(sub1); sub2 = mergeSort(sub2); array = merge(sub1, sub2); return array; } } // end mergeSort

25

Merge Method

private static int[] merge(int[] sub1, int[] sub2) { int[] array = new int[sub1.length + sub2.length]; int i1 = 0, i2 = 0;

for (int i=0; i<array.length; i++) { // both sub-groups have elements if (i1 < sub1.length && i2 < sub2.length) { if (sub1[i1] <= sub2[i2]) array[i] = sub1[i1++]; else // sub2[i2] < sub1[i1] array[i] = sub2[i2++]; } else // only one sub-group has elements { if (i1 < sub1.length) array[i] = sub1[i1++]; else // i2 < sub2.length array[i] = sub2[i2++]; } // end only one sub-group has elements } // end for all array elements return array; } // end merge

26

Merge Sort Driver and Output

public static void main(String[] args) { Random random = new Random(0); int length = 19; int[] array = new int[length];

for (int i=0; i<length; i++) array[i] = random.nextInt(90) + 10; printArray("initial array", array); printArray("final array", mergeSort(array)); } // end main

private static void printArray(String msg, int[] array) { System.out.println(msg); for (int i : array) System.out.printf("%3d", i); System.out.println(); } // end printArray

Sample session:

initial array 70 98 59 57 45 93 81 31 79 84 87 27 93 92 45 24 14 25 51final array 14 24 25 27 31 45 45 51 57 59 70 79 81 84 87 92 93 93 98

27

The Towers of Hanoi

According to legend, there is a temple in Hanoi which contains 64 golden disks, each with a different diameter, and each with a hole in its center.

The disks are stacked on three towering posts. Initially, all the disks are stacked on post #1, with the largest-diameter disk on the bottom and progressively smaller-diameter disks placed on top of each other.

The temple's monks are tasked with moving disks from post 1 to post 3, while obeying these rules:1. Only one disk can be moved at a time.2. No disk can be placed on top of a disk with a smaller

diameter. Let's help the monks by writing a computer program

that specifies the optimum transfer sequence.

28

The Towers of Hanoi

Recursive algorithm for moving n disks from the source tower to a destination tower:1. Move the top n - 1 disks to the intermediate tower.

(This is a recursive step where n - 1 disks are moved.)

2. Move the bottom disk to the destination tower.3. Move the intermediate-tower group to the

destination tower.(This is a recursive step where n - 1 disks are

moved.)

Write a recursive method named move that simulates the movement of n disks from one specified tower to another specified tower.

29

Drawing Trees with a Fractal Algorithm

Computer recursion mimics the recurrence of similar patterns in nature.

To illustrate the analogy, we’ll now use recursion to draw a grove of trees, where each tree is a recursive picture.

Our simulated trees will repeat a simple geometrical pattern − a straight section followed by a fork with two branches:

The left branch goes 30 degrees to the left and has a length equal to 75% of the length of the straight section.

The right branch goes 50 degrees to the right and has a length equal to 67% of the length of the straight section.

An object created by repeating a pattern at different scales is called a fractal. A mathematical fractal displays self-similarity on all scales. In the natural world – and in computers – self-similarity exists only between upper and lower scale limits. In the case of a tree, the upper limit is the size of the trunk and the lower limit is the size of a twig. These upper and lower limits establish recursive starting and stopping conditions.

In each of a sequence of steps, the program slightly modifies size attributes and repaints the scene. This makes the trees grow larger and fill out with more branches as time passes.

31

Drawing Trees with a Fractal Algorithm

This example also illustrates: GUI animation. The Model-View-Controller (MVC) design pattern.

The program’s model is in the Tree class, which models the program’s key components.

The program’s view is in the TreePanel class, which implements updates in the screen display.

The program’s controller is in the TreeDriver class, which gathers user input and calls Tree and TreePanel methods to drive the model and manage alterations to the display.

32

The model – the Tree Class

import java.awt.Graphics; public class Tree{ private final int START_X; // where the tree sprouts private final int START_TIME; // when the tree sprouts private final double MAX_TRUNK_LENGTH = 100; private double trunkLength;  //**********************************************************  public Tree(int location, int startTime, double trunkLength) { this.START_X = location; this.START_TIME = startTime; this.trunkLength = trunkLength; } // end constructor

//**********************************************************  // <get-methods-for: START_X, START_TIME, and trunkLength>

33

The Tree Class − continued

public void updateTrunkLength() { trunkLength = trunkLength + 0.01 * trunkLength * (1.0 - trunkLength / MAX_TRUNK_LENGTH); } // updateTrunkLength

//*****************************************************

public void drawBranches(Graphics g, int x0, int y0, double length, double angle) { double radians = angle * Math.PI / 180; int x1 = x0 + (int) (length * Math.cos(radians)); int y1 = y0 - (int) (length * Math.sin(radians));  if (length > 2) { g.drawLine(x0, y0, x1, y1); drawBranches(g, x1, y1, length * 0.75, angle + 30); drawBranches(g, x1, y1, length * 0.67, angle - 50); } } // end drawBranches

34

The view – the TreePanel Class

import javax.swing.JPanel;import java.awt.Graphics;import java.util.ArrayList; public class TreePanel extends JPanel{ private final int HEIGHT; // height of frame private final int WIDTH; // width of frame private ArrayList<Tree> trees = new ArrayList<>(); private int time = 0; // in months  //**********************************************************  public TreePanel(int frameHeight, int frameWidth) { this.HEIGHT = frameHeight; this.WIDTH = frameWidth; } // end constructor

35

The TreePanel Class − continued

//**********************************************************  public void setTime(int time) { this.time = time; } // setTime  //**********************************************************  public void addTree( int location, double trunkLength, int plantTime) { trees.add(new Tree(location, plantTime, trunkLength)); } // end addTree  //**********************************************************  public ArrayList<Tree> getTrees() { return this.trees; } // end getTrees

36

The TreePanel Class − continued

public void paintComponent(Graphics g) { int location; // horizontal starting position of a tree String age; // age of a tree in years super.paintComponent(g); // draw a horizontal line representing surface of the earth: g.drawLine(25, HEIGHT - 75, WIDTH - 45, HEIGHT - 75); for (Tree tree : trees) { // draw the current tree: location = tree.getStartX(); tree.drawBranches( g, location, HEIGHT - 75, tree.getTrunkLength(), 90); // write the age of the current tree: age = Integer.toString((time - tree.getStartTime()) / 12); g.drawString(age, location - 5, HEIGHT - 50); } } // end paintComponent} // end TreePanel class

37

The controller – the TreeDriver Class

import javax.swing.JFrame;import java.util.ArrayList; public class TreeDriver{ private final int WIDTH = 625, HEIGHT = 400; private TreePanel panel = new TreePanel(HEIGHT, WIDTH); private int time = 0;  //**********************************************************  public TreeDriver() { JFrame frame = new JFrame("Growing Trees");  frame.setSize(WIDTH, HEIGHT); frame.add(panel); frame.setVisible(true); } // end constructor

38

The TreeDriver Class − continued

public void simulate() throws Exception { ArrayList<Tree> trees = panel.getTrees(); boolean done = false;  while(!done) { switch (time) { case 0: panel.addTree(400, 3, time); break; case 360: panel.addTree(100, 3, time); break; case 540: panel.addTree(300, 3, time); break; case 630: panel.addTree(200, 3, time); break; case 675: done = true; } // end switch

39

The TreeDriver Class − continued

panel.repaint();  time++; panel.setTime(time); for (Tree tree : trees) { tree.updateTrunkLength(); // to correspond to the new time } Thread.sleep(50); // throws an InterruptedExeption } // end while } // end simulate  //**********************************************************  public static void main(String[] args) throws Exception { TreeDriver driver = new TreeDriver();  driver.simulate(); } // end main} // end TreeDriver class

Because it extends JPanel, our TreePanel class acquires this repaint method from the JPanel class, and repaint automatically calls TreePanel’s paintComponent method.

Because it extends JPanel, our TreePanel class acquires this repaint method from the JPanel class, and repaint automatically calls TreePanel’s paintComponent method.

40

Drawing Trees with a Fractal Algorithm

Here is the final display produced by the simulation:

41

Performance Analysis

One way to quantify the performance of an algorithm is to measure execution time. We described that in the previous chapter.

Another way is to write a simple formula that approximates the dependence of time or space on data quantity.

Time and space requirements are positively correlated, and we usually focus on time by approximating the number of computational steps as a function of data quantity.

A typical for loop header gives the number of iterations. If each iteration takes the same amount of time, the time needed to iterate through the data set increases linearly with array length.

If a loop includes another loop nested inside it, the total number of iterations is the number of iterations in the outer loop times the number of iterations of the inner loop.

42

Insertion Sort Method

public static void insertionSort(int[] list)

{

int itemToInsert;

int j;

for (int i=1; i<list.length; i++)

{

itemToInsert = list[i];

for (j=i; j>0 && itemToInsert<list[j-1]; j--)

{

list[j] = list[j-1]; // upshift previously sorted items

}

list[j] = itemToInsert;

} // end for

} // end insertionSort

43

Insertion Sort Method

The inner loop starts at the outer loop’s current index and iterates down through previously sorted items, shifting them upward as the iteration proceeds, until the item to insert is less than a previously sorted item.

The portion of the array that is already sorted grows as the outer loop progresses.

If the array is already sorted, itemToInsert < list[j-1] is always false, and the inner loop never executes. In this best case, the total number of steps is just the number of steps in the outer loop, or:

minimumSteps = list.length – 1 If the array is initially in reverse order, itemsToInsert < list[j-

1] is always true. In this worst case, the total number of steps is: maximumSteps = (list.length – 1) * list.length / 2 If the array is initially in random order, on average, the total

number of steps is: averageSteps = (list.length – 1) * list.length / 4

44

Confounding Factors and Simplifications

The insertion-sort example shows that performance depends not only on the nature of the algorithm but also on the state of the data.

Also, other things being equal, the time needed per operation typically decreases as the number of similar operations increases.

For example, as an ArrayList’s length increases from 100 to 1,000 to 10,000, the average get and set time decreases from 340 ns to 174 ns to 122 ns, respectively.

As a LinkedList’s length increases from 100 to 1,000 to 10,000, the average get and set time increases from 1,586 ns to 1,917 ns to 18,222 ns. This is less than the linear rate of increase we might expect.

These hard-to-predict performance variations suggest that we should not expect high precision in performance analysis. For example, we could approximate the total time or space required to perform a task with something like this:time or space required ≈ a * f(n) + bwhere:

n = total number of elementsf(n) means “function of n”

45

Further Simplification and Big O Notation

Usually we simplify further by dropping a and b and writing:time or space required ≈ O(n) The right side is Big O notation. The O means “order of

magnitude.” If the required time or space is the same for all n, we say “It’s O(1).” If the required time or space is directly proportional to n, we say “It’s O(n).” If the required time or space increases as the square of n, we say “It’s O(n2).” If the required time or space increases as the cube of n, we say, “It’s O(n3).” If the required time or space increases as log(n), we say, “It’s O(log n).” If the required time or space increases as n * log(n), we say, “It’s O(n log n).”

For really some tough problems, the dependence might increase exponentially, like O(2n) or perhaps even O(nn). In such cases, we say the problem is intractable, which means it’s practically impossible to obtain exact answers.

46

Big O Dependencies

n O(1) O(log n) O(n) O(n log n) O(n2) O(n3) O(2n) O(nn)

4 1 2 4 8 16 64 16 256

16 1 4 16 64 256 4,096 65,536 1.8 E19

64 1 6 64 384 4,096 2.6 E5 1.8 E19 3.9 E115

256 1 8 256 2,048 65,536 1.7 E7 1.2 E77 #NUM!

1,024 1 10 1,024 10,240 1.1 E6 1.1 E9 #NUM! #NUM!

4,096 1 12 4,096 49,152 1.7 E7 6.9 E10 #NUM! #NUM!

16,384 1 14 16,384 2.3 E5 2.7 E8 4.4 E12 #NUM! #NUM!

47

Big O Examples from Chapters 9 and 10

[§9.7] sequential search: O(n) [§9.7] binary search: O(log n) [§9.8] selection sort: O(n2) [§9.9] two-dimensional array fill: O(n2) [§10.2] List’s contains method: O(n) [§10.7] ArrayList’s get and set methods: O(1) [§10.7] LinkedList’s get and set methods: O(n) [§10.7] List’s indexed remove and add methods: O(n) [§10.7] ArrayDeque’s offer and poll methods: O(1) [§10.9] HashMap’s put, get, contains, and remove methods: O(1) [§10.9] TreeSet’s add and get methods: O(log n) [§10.7] HashMap’s put, get and contains methods: O(1)

48

Big O Examples from Chapter 11

[§11.4] Factorial: O(n) [§11.4] PrintReverseMessage: O(n) [§11.6] binary search: O(log n) [§11.7] merge sort: O(n log n) [§11.8] Towers of Hanoi: O(2n) [§11.9] drawBranches: O(n) , where n is number of branches [§11.10] insertion sort: O(n2), or O(n) if already sorted or nearly sorted

49