RECURSION

TOPICS


·       Definitions

·       Examples

·       When to Use Recursion

·       Sample Program: Hanoi Towers


OUTLINE


Definitions:
 

·       Recursion is a very important problem-solving approach that is an alternative to iteration (remember that iterative solutions include loops).

·       Math background: There is an accepted form of mathematical definition that uses concepts to define themselves. Such definitions are called inductive definitions. When carefully used, such definitions are very concise, very powerful, and very elegant.

·       Definition: A recursive definition of an entity defines the entity in terms of itself. Also called self-referential definitions. Example: The recursive definition of a data structure could be used to build the data structure from scratch.

·       In programming languages, we also talk about recursive methods (functions).

·       Definition: Recursion = A problem solving / programming technique in which a method (function) can call itself in order to solve the problem. In the process, the problem is solved by reducing it to smaller versions of itself.

·       In Java a method can call itself --> if written that way, the method is called a recursive method.

·       Definition: Infinite recursion = the situation in which a function calls itself over and over endlessly. A definition with a missing or badly written base case causes infinite recursion, similar to an infinite loop (there is no stop!)  --> A  recursive solution  to a problem must be written carefully.

·       The idea: Each successive recursive call should bring you one step closer to a situation in which the problem can easily be solved. Each recursive definition has two separate parts: a base case and a general (or recursive) case.

1.     The easily solved situation is called the  base case. The base case is a simple case of the problem that we can answer directly; the base case does NOT use recursion. Each recursive algorithm must have at least one base case. Without at least one base case --> infinite recursion.

2.     The general (recursive) case is the more complicated case of the problem that isn't easy to answer directly, but can be expressed elegantly with recursion. The general case is the place where the recursive calls are made --> written so that with repeated calls it reduces to the base case (or brings you closer to the base case). Must eventually be reduced to a base case; if not --> infinite recursion. Must have at least one general case --> if not, no recursion!

·       In addition to the base and recursive case, each recursive definition has an implicit case. The base case states one or more cases that immediately satisfy the definition. The recursive case states how to apply the definition again to satisfy other cases. The implicit case (which is implied, not mentioned) states "And nothing else satisfies the definition".

·       General format for recursive functions:

if(some easily-solved problem) // base case
    solution statement
else                        // general case
    recursive function call

·       Definition: Direct recursion = the method is calling itself. A method that calls another method and eventually results in the original method call is called indirectly recursive.

·       Definition: Indirect recursion = the method calls another method and eventually results in the original method call. Indirect recursion requires the same careful analysis as direct recursion. Tracing through indirect recursion can be a tedious process.

·       Every recursion has a forward phase (a call at every level except the last makes a call to the next level and waits for the latter call to return control to it.

·       Every recursion also has a backing-out phase (a call at every level except the first passes control back to the previous level, at which point the call waiting at the previous level resumes the work.)

·       Definition: Tail recursion = A recursive method in which no statements are executed after the return from the recursive call (the recursive call is the last statement) It often indicates that the problem could be solved more easily with iteration.

·       To design a recursive method, you must:

1. Understand the problem requirements.
2. Determine the limiting conditions.
3. Identify the base case(s) and provide a direct solution to each base case.
4. Identify the general case(s) and provide a solution to each general case in terms of a smaller version of itself.


Examples of Recursive Methods:
 

·       Example #1 (sum): Write a recursive method to find the sum of all positive integers between 1 and n. The method call sum(5) should have value 15, because that is 1 + 2 + 3 + 4 + 5.
The iterative method:

public static int sum(int n){
    int total = 0;
    for(int i = 1; i <= n; i++)
        total += i;
    return total;
}

Recursive method analysis: For an easily solved situation, the sum of the numbers from 1 to 1 is 1. So, our  base case  could be
     

if (n == 1)
    return 1;


Now for the  general case. . .

sum(n) = 1 + 2 + . . . + (n-1) + n =  [1 + 2 + . . . + (n-1)] + n = sum(n - 1) + n

Notice that the recursive call  sum(n - 1) gets us “closer” to the base case of  sum(1).
The recursive method:

public static int sum(int n){
    if (n == 1) // base case
       return  1;
    else    // general case
       return n + sum( n - 1);//tail recursion
}

 

·       Example #2 (factorial): Write a recursive method to find n factorial (n! = n * (n-1) * (n-2)... 3 * 2 * 1). The method call factorial(5) should have value 120, because that is 5 * 4 * 3 * 2 * 1 .

The iterative method:

public static int factorial(int n){
    int fact = 1;
    for(int i = 1; i <= n; i++)
        fact *= i;
    return fact;
}

 

Recursive method analysis: For a situation in which the answer is known, the value of  1! is 1.  So our  base case  could be
      if (n == 1)
          return 1;
Now for the  general case . . .

n! = n * (n-1) * (n-2)... 3 * 2 * 1 = n * [(n-1) * (n-2)... 3 * 2 * 1] = n * (n-1)! OR:
factorial(n) = n * factorial(n-1)

Notice that the recursive call  factorial(n - 1) gets us “closer” to the base case of  factorial(1).
The recursive method:

public static int factorial(int n){
    if (n == 1)  // base case
       return  1;
    else     // general case
       return n * factorial(n - 1); //tail recursion
}

 

·       Example #3 (power): Write a recursive method to find xn recursively.
      The iterative method:

public static int power(int x, int n){
    int prod = 1;
    for(int i = 1; i <= n; i++)
        prod *= x;
    return prod;
}

 

Recursive method analysis: From mathematics, we know that  20 = 1     and     25  = 2 * 24.  In general,  x0 = 1  and     xn = x * xn-1    for integer x, and integer n > 0. Here we are defining xn recursively, in terms of  xn-1

The recursive method:

public static int power(int x, int n ){
   if (n == 0)   // base case
      return  1;
   else      // general case
      return (x * power ( x , n-1)); //tail recursion
}

 

What is the value of  2-3?   Again from mathematics, we know that it is  2-3 = 1/23 = 1/8
In general, 
xn = 1/ x-n  for non-zero x, and integer n < 0. Let's define xn  recursively, in terms of  x-n  when n < 0 .
The new recursive method:

public static double power(int x, int n){
   if (n == 0)     // base case
      return  1;
   else if(n > 0)  // first general case
      return (x * power(x , n - 1));
   else        // second general case
      return (1.0 / power(x , - n));
}

·       Example #4 (gcd): Write a recursive method to find the greatest common divisor of 2 integers (the largest integer that goes evenly into 2 integers or, in other words, the largest number that is a factor of both). We will use the Euclid's formula, which states that:  gcd(a, b) = gcd(b, a mod b), and gcd(a, 0) = a. Based on this formula, it is easy to figure out the base case and the general case for the recursive method.
 The iterative method:

public static int gcd(int x, int y){
    int temp = x % y;
    while(temp > 0) {
        x = y;
        y = temp;
        temp = x % y;
    }
    return y;
}

The recursive method:

public static int gcd(int x, int y){
    if(y == 0)           // base case
        return x;
    else if(x < 0 || y < 0) // first general case
        return gcd(Math.abs(x), Math.abs(y));
    else                // second general case
        return gcd(y, x % y);
}

·       Example #5: (print chars) Write a method to print a line of n characters.
The iterative method:

public static void printChar(char what, int n){
    for(int i = 1; i <= n; i ++)
        System.out.print(what);
    System.out.println();
}

 

The recursive method:

public static void printChar(char what, int n){
    if(n == 0) // base case
        System.out.println();
    else {     // general case
        System.out.print(what);
        printChar(what, n-1);
    }
}

 

·       Example #6: (Fibonacci) Write a recursive method to determine the n-th Fibonacci number. The first Fibonacci number is 1 and the second Fibonacci number is 1. A Fibonacci number in the sequence is the sum of the previous 2 Fibonacci numbers.
The recursive method:

public static int fibonacci(int n) {
     if(n == 1)      // first base case
         return 1;
     else if (n == 2)// second base case
         return 1;
     else           // general case
         return(fibonacci(n - 1) + fibonacci(n - 2));
}

 

·       Example #7: (Fibonacci, more general) Write a recursive method to determine the n-th Fibonacci number in a sequence where: the first number in the sequence is a and the second number is b. A Fibonacci number in the sequence is the sum of the previous 2 Fibonacci numbers
The recursive method:

public static int fibonacci(int a, int b, int n) {
     if(n == 1)       // base case
         return a;
     else if (n == 2) // second base case
         return b;
     else           // general case
         return(fibonacci(a, b, n - 1) + fibonacci(a, b, n - 2));
}

 

·       Example #8: (recursion with arrays) Write a method to print array elements in reverse order. If the array (a)  is:  74  36  87  95,  the call  printRev(a, 0, 3 )should produce this output:    95   87   36  74
The recursive method:

public static void printRev(int [] a, int i, int j) {
     if (i <= j) { // general case
        System.out.print(a[j] + "  "); // print last element
        printRev(a, i, j - 1); // then process the rest
     }
    // where is the base case???
}

 

·       Example #9: (recursion with arrays)  Write a recursive method that takes an array a  and two subscripts, i and j as arguments, and returns the sum of the elements a[i] + . . . + a[j].
The recursive method:

public static int sum(int [] a, int i, int j){
   if (i == j) // base case
       return (a[i]);
   else        // general case
       return (a[i] + sum(a, i + 1, j));
}

 

·       Example #10: (recursion with arrays)  Write a recursive method that takes an array a and its size as arguments, and returns the minimum element in the array. What is the base case here?
The recursive method:

public static int findMin(int[] a, int size) {
     int min;
     if(size == 1)  // base case: one element in list
         return a[0];
     else {         // general case
         min = findMin(a, size - 1);
         if(min < a[size - 1])
              return min;
         else
              return a[size - 1];
     }
}

 

·       Example #11: (recursion with arrays)  Write a similar recursive method to allow subarray processing (from a[i] to a[j])
The recursive method:

public static int findMin(int [] a, int i, int j) {
     int min;
     if(i == j)  // base case: one element in list
          return a[i];
     else {      // general case
          min = findMin(a, i + 1, j);
          if(a[i] <= min)
               return a[i];
          else
               return min;
     }
}

 

·       Example #12: (recursion with strings)  Write a recursive method to display in reverse order a string of characters. Take input characters until the user enters a period  and output all characters entered so far in reverse order.
The recursive method:

public static void printBack (){
    Scanner input = new Scanner(System.in);
    char inputChar;
    System.out.print("Enter a character. When finished press <.>: ");
    inputChar = input.next().charAt(0);
    if(inputChar != '.'){ // general case
        printBack();
        System.out.print(inputChar);
    }
}

Output:
Enter a character. When finished press <.>: r
Enter a character. When finished press <.>: e
Enter a character. When finished press <.>: c
Enter a character. When finished press <.>: u
Enter a character. When finished press <.>: r
Enter a character. When finished press <.>: s
Enter a character. When finished press <.>: i
Enter a character. When finished press <.>: o
Enter a character. When finished press <.>: n
Enter a character. When finished press <.>: .
noisrucer
 

·       Example #13: (recursion with linked lists) Write a recursive method to compare 2 linked lists for equality (given references to their first node).
The recursive method:

public static boolean equalLists(LinkedListNode<Integer> head1, LinkedListNode<Integer> head2){
    if(head1 == null || head2 == null)//base case #1
        return (head1 == head2);
    if(head1.data != head2.data)      //base case #2
        return false;
    return equalLists(head1.link, head2.link);// general case
}

 

·       Example #14:(recursion with linked lists) Write a recursive method to print a linked list in direct order (given a reference to its first node).
The recursive method:

public static void printLL(LinkedListNode<Integer> head) {
    if(head != null) {
        System.out.print(head.info + " ");
        printLL(head.link);//tail recursion
    }
}

 

·       Example #15:(recursion with linked lists)  Write a recursive method to print a linked list in reverse order (given a reference to its first node).
The recursive method:

public static void revPrintLL(LinkedListNode<Integer> head) {
    if(head != null) {
        revPrintLL(head.link); //forward phase
        System.out.print(head.info + " "); //backing-out phase
    }
}

·       Example #16:(recursion with linked lists)  Write a recursive method to count the nodes in a linked list (given a reference to its first node).
The recursive method:

public static int countNodesLL(LinkedListNode<Integer> head) {
    if(head == null)
        return 0;
    else
        return 1 + countNodesLL(head.link);
}


When to Use Recursion
 

·       The examples listed here could all have been written without recursion, by using iteration instead.  The iterative solution uses a loop, and the recursive solution uses an if statement. However, for certain problems the recursive solution is the most natural solution.

·       Recursion is a very important tool in supporting data abstraction. Recursive definitions are often necessary to define data and associated operations, and the recursive functions are (in many cases) the natural solution for the implementation of the operations on data.

·       Drawbacks of recursion: the amount of stack space used to implement recursion and possible redundancy. Every recursive method call produces a new instance of the method (with a new set of local variables and parameters).  Each set of local variables and parameters related to the current call is stored on the stack, to be picked up on return. For example, the recursive implementation for the factorial method is a good example for its simplicity, but not a practical solution (huge overhead --> huge usage of stack space).

·       There are no absolute rules when choosing between a recursive or an iterative solution. The most powerful benefit of recursive methods is the fact that they are concise, which makes them easier to maintain and read. On the other hand, recursive methods consume time and computer storage, which means that they may not be very efficient. These are some guidelines when considering the alternatives:

1.     Design a recursive method if the problem is stated recursively and the recursive algorithm is less complex. Keep in mind that in many cases, recursion is a technique that reduces the complexity of the algorithms you want to implement.

2.     Design an iterative method if similar complexities for the recursive and the iterative algorithms (the iterative solution is likely to be more efficient)

In many cases the choice is not very clear (concise vs. efficient). More experienced designers know when efficiency really matters and when it is less important.


Sample Program: Hanoi Towers

The problem: We have n disks of different sizes, and 3 pegs: A (the source), B (the destination), and C (the spare). Initially, all the disks are on peg A, ordered by size, with the smallest on the top. The problem is to move all the disks, one by one, from peg A to peg B, using peg C as an auxiliary/spare (it must be empty at the beginning and at the end of the game). Restriction: a disk cannot be placed on top of one that is smaller in size.
How could this be done?  Look at the following figure:
 

Description: C:\Courses-NOW\Webpage\237\LectureNotes\Hanoi(a-b).gif
 
 

Description: C:\Courses-NOW\Webpage\237\LectureNotes\Hanoi(c-d).gif


 
 

(a) The initial state 
 
 
 
 
 
 

(b) move (n – 1) disks from A to C
 
 
 
 
 
 

(c) move one disk from A to B 
This disk is in its final place now.
 
 
 
 
 
 
 

(d) move (n – 1) disks from C to B 

The algorithm: To free the n-th disk, we have to move (n-1) disks from top. Each stage could be the initial state (again) for a smaller problem (one less disk each time). To get closer to what we discussed so far:

Base case: If  n = 1 (only one disk), we just move it from peg A to peg B (figure (b) and (c): one disk from peg A moved to peg B).
General case: If n > 1:

·       Ignore the bottom disk and solve the problem for (n-1) disks with peg C the destination and peg B the spare.

·       After step 1, peg C will hold (n-1) disks and the largest disk will be left on peg A (figure (b)). Solve the problem for n=1 and move the large disk from peg A to peg B. (figure (c))

·       Move (n-1) disks from peg C to peg B (the original problem with a twist: peg C is the source, peg B is the destination and peg A is the spare)

More refined, if we write the solution as a recursive function that will take parameters n disks and 3 pegs (hanoiTowers(n, A, B, C)) and if we start with n disks on peg A and 0 disks on both pegs B and C, the algorithm is:

  1. In the initial state (all disks on peg A), solve the problem hanoiTowers(n-1, A, C, B). In other words, you have here step 1 from the previous variant. When finished, the largest disk will be on peg A and (n-1) disks will be on peg C.
  2. With the largest disk on peg A and (n-1) disks on peg C, solve the problem hanoiTowers(1, A, B, C). In other words, you have here step 2 from the previous variant. When finished, the largest disk will be on peg B (in place) and (n-1) disks will be on peg C.
  3. With the largest disk on peg B and all the others on peg C, solve the problem hanoiTowers(n-1, C, B, A).

Java recursive method:

public static void hanoiTowers(int how_many, char source, char destination, char spare){
     if (how_many == 1) { //base case
         System.out.print("Move top disk from peg " + source);
         System.out.println(" to peg " + destination);
     }
     else { //general case
         hanoiTowers(how_many-1, source, spare, destination);
         hanoiTowers(1, source, destination, spare);
         hanoiTowers(how_many-1, spare, destination, source);
     }
 }
The Java program:

import java.util.Scanner;
public class HanoiTowers {
    public static void main(String[] args) {
        int diskCount;// Number of disks on starting peg(A)
        Scanner input = new Scanner(System.in);
        System.out.print("Input number of disks: ");
        diskCount = input.nextInt();
        while(diskCount <= 0) {
             System.out.print("\nERROR! Should be positive. Reenter: ");
             diskCount = input.nextInt();
        }
        System.out.println("Hanoi Towers: Output with " + diskCount + " disks");
        System.out.println("INSTRUCTIONS === peg A = source, peg B = destination, peg C = spare ===\n");
        hanoiTowers(diskCount, 'A', 'B', 'C');//method call
     }

     public static void hanoiTowers(int how_many, char source, char destination, char spare){
         if (how_many == 1) { //base case
          System.out.print("Move top disk from peg " + source);
          System.out.println(" to peg " + destination);
        }
         else { //general case
             hanoiTowers(how_many-1, source, spare, destination);
             hanoiTowers(1, source, destination, spare);
             hanoiTowers(how_many-1, spare, destination, source);
         }
     }
}
OUTPUT:
Input number of disks: 4
Hanoi Towers: Output with 4 disks
INSTRUCTIONS === peg A = source, peg B = destination, peg C = spare ===

Move top disk from peg A to peg C
Move top disk from peg A to peg B
Move top disk from peg C to peg B
Move top disk from peg A to peg C
Move top disk from peg B to peg A
Move top disk from peg B to peg C
Move top disk from peg A to peg C
Move top disk from peg A to peg B
Move top disk from peg C to peg B
Move top disk from peg C to peg A
Move top disk from peg B to peg A
Move top disk from peg C to peg B
Move top disk from peg A to peg C
Move top disk from peg A to peg B
Move top disk from peg C to peg B


Key Terms
Base case: the case in a recursive definition in which the solution is obtained directly.
Directly recursive method: a method that calls itself.
General case (Recursive case): the case in a recursive definition in which the method is calling itself
Indirectly recursive: a method that calls another method and eventually results in the original method call.
Recursive definition: a definition in which an entity is defined in terms of a smaller version of itself.
Recursive method: a method that calls itself.
Tail recursive method: a recursive method in which no statements are executed after the return from the recursive call
Infinite recursion: the situation in which a function calls itself over and over endlessly.


References:
[1] Java Programming: From Problem Analysis to Program Design, by D.S. Malik, Thomson Course Technology, 2008.
[2] Building Java Programs: A Back to Basics Approach, by Stuart Reges, and Marty Stepp, Addison Wesley, 2008.
[3] Data Abstraction and Problem Solving with C++: Walls and Mirrors, third edition, by Frank M. Corrano, and Janet J. Prichard, Addison Wesley, 2002.