RECURSION
TOPICS
·
Examples
·
Sample Program: Hanoi Towers
OUTLINE
·
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);
}
·
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.
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:
|
(a) The initial state (b) move (n 1) disks from A to C (c) move one disk from A to B (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:
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.