Recursion

A recursive method is a method that is defined in terms of itself. The general idea behind recursion is that a problem lends itself to a recursive solution if the problem can be broken down into a smaller version of the same problem. A non-CS example of this is the Matroyshka doll. A mathematical example of this is the computation of the factorial of a number. factorial(N) is N*factorial(N-1).

Iterative Factorial

public int factorial(int n) {        
int product = 1;
for(int i = 1; i <= n; i++) {
product *= i;
System.out.println("Product " + product);
}
return product;
}

Recursive Factorial

(Write the previous method without using a loop!)

public int factorial(int n) {        
if(n == 1) {
return 1;
}
return n*(factorial(n-1));
}

Try running this for large values of n!

Comparison

  1. Some problems are more easily solved recursively.
  2. Recursion can be highly inefficient as resources are allocated for each method invocation.

If you can solve it iteratively, you usually should!

The Base Case

The number 1 rule of recursion:

You must always have some base cases, which can be solved without recursion.

Data Structures and Algorithms in C++ by Mark Allen Weiss

At some point, your recursion must end. There must be some trivial version of the problem that is easily solved without using recursion. In the factorial example, the factorial of 1 is 1. If you fail to specify a base case, you will end up with infinite recursion. In fact such a program will not execute infinitely as an infinite loop might, but will execute until you get a StackOverflowException.

The Recursive Case

The number 2 rule of recursion:

For cases that are to be solved recursively, the recursive call must always be a case that makes progress toward a base case.

Data Structures and Algorithms in C++ by Mark Allen Weiss

Consider what would happen if we mistakenly called the recursive factorial passing n instead of n-1 on the last line of the method. Because we would not be making progress toward the base case, the program would continue calling itself with the same input until we encountered a StackOverflow Exception.

Other Recursion Rules

The 3rd and 4th rules of recursion:

3. Assume that all recursive calls work.

4. Never duplicate work by solving the same instance of a problem in separate recursive calls.

Data Structures and Algorithms in C++ by Mark Allen Weiss

Linear Recursion

A linear recursive algorithm contains at most 1 recursive call at each iteration. Factorial is an example of this. The general algorithm followed by factorial is (1) test for the base case; (2) recurse. Factorial is also an example of tail recursion. In tail recursion, the recursive call is the last operation in the method. Tail recursive methods can easily be converted to iterative algorithms.

Exercise: Implement a recursive method that takes as input a String and prints the characters of the String in reverse order. Consider how you would implement the method if you were only able to either access the first character of the String, or extract a multi-character substring.

Another good example of linear recursion is Binary Search. Suppose you have a sorted array of numbers and you want to determine whether a particular number appears in the list. The linear algorithm would start at the beginning of the array and look at each element until it came to the desired element or the end of the list. At maximum, this would require n comparisons where n is the number of items in the list. To reduce the number of comparisons, we can take advantage of the fact that the array is sorted. Rather than start at the beginning, we can start in the middle. If the middle element is larger than the element we are looking for, we know that the element will be in the left side of the array if it exists in the array. Similarly, if the middle element is smaller than the element we are looking for, we know that the element will be in the right side of the array if it exists. At each step, we can ignore half of the remaining elements. It turns out that this algorithm will result in a maximum of log(N) comparisons.

A general search method would take as input the element you are looking for and the array in which you want to search. However, to implement binary search, your recursive method should take as input the element you are looking for, the array you want to search, and the starting and ending indices of the subarray where you want to search. To alleviate this inconsistency, it is typical to implement a driver method that supports the interface you would expect (input and element and an array). The driver method simply calls the recursive helper method passing the appropriate data. In this case, the driver method would call the recursive method passing the element, the array, the starting index 0, and the length of the array minus 1 as the ending index.

Exercise: Implement a method to perform a binary search. Think about the base case and how you will make progress toward the base case at each iteration.

Higher Order Recursion

An algorithm certainly can make more than 1 recursive call. A really bad example of this is the calculation of Fibonacci numbers. The nth Fibonacci number is the (n-1)st Fibonacci number plus the (n-2)nd Fibonacci number. The 0th Fibonacci number is 0 and the 1st Fibonacci number is 1. This problem lends itself well to a recursive solution, but violates rule number 4, never duplicate work. The following recursive algorithm to solve for the nth Fibonacci number duplicates lots of work:

fib(n)
if(n == 0) return 0
else if(n == 1) return 1
else
return fib(n-1) + fib(n-2)

A better example of higher order recursion is mergesort. The mergesort algorithm is also an example of binary recursion, a recursive algorithm that splits a problem in half and recursively solves each half. Mergesort is similar to binary search, except it makes 2 recursive calls and then must merge the results. The general algorithm for mergesort is as follows:

mergesort(elt, array, start, end)
if(start == end) //only 1 element, already sorted
return subarray containing element start
mid = (end+start)/2
lower = mergesort(elt, array, start, mid)
upper = mergesort(elt, array, mid+1, end)
for i = 0, j = 0; i<lower.length && j < upper.length;)
if lower[i] < upper[j]
result[i+j] = lower[i]
i++
else if upper[j] > lower[i]
result[i+j] = upper[j]
j++
else //elements are equal
result[i+j] = upper[j]
result[i+j+1] = lower[i]
i++
j++
//copy any left over items from lower (only should execute 1 of the following while loops)
while i < lower.length
result[i+j] = lower[i]
i++
//copy any left over items from upper
while j < upper.length
result[i+j] = upper[j]
j++
return result

Exercise: Implement a recursive method to print the following output. Assume your method takes as input the length of the longest row (in this case 5), and another integer that will represent the progress toward the base case.

0
0 1
0 1 2
0 1 2 3
0 1 2 3 4
0 1 2 3
0 1 2
0 1
0

Exercise: Implement a recursive method to solve the towers of hanoi. The idea of the puzzle is that you have three pegs and a set of disks where each disk is a different size. The goal is to move all of the disks from peg 1 to peg 3 while not violating the following rules:

  1. You may only move 1 disk at a time.
  2. A larger disk cannot be placed on top of a smaller disk.
  3. All disks must be on some peg except the disk in-transit.

Comments