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