CPS222 Lecture: Recursion  Last revised 1/13/2015
Materials:
1. Simple expression grammar projectable plus interpreter demo
2. Excerpt from Java grammar
3. Towers of Hanoi toy
4. Projectable of towers of hanoi program
5. Demos of towers of hanoi program  plain and graphic versions
6. Projectable of permute program
7. Demo of permute program
Objectives:
1. To introduce recursion and define "recursive"
2. To give examples of uses of recursion
3. To introduce the various classes of recursive algorithms
4. To introduce reasons why it might be desirable to avoid recursion.
I. Introduction
 
A. Today we discuss a algorithm design technique that is very powerful.
The technique is known as recursion.
B. Some definitions:
1. A recursive DEFINITION is one that defines some entity partially in
terms of itself.
2. A recursive METHOD is one that can call itself.
3. Recursive methods often arise in programs when one is
dealing with entities that are defined recursively.
C. An example:
1. The factorial operation can be defined mathematically as follows:
For all integers N >=0:
N! = IF N <= 1 THEN 1
ELSE N*(N1)!
2. In Java or C++, this can be coded as follows:
int factorial(int n)
{
if (n <= 1)
return 1;
else
return n * factorial(n1);
}
3. The book developed a diagram called a recursion trace for tracing how a recursive
algorithm works. The diagram is especially simple if recursion only occurs once
in the code. Let's develop the diagram.
GIVE class time to work on in pairs for factorial(3)
ASK volunteer to draw on the board
Initial call

v

 factorial (3) 


v

 factorial (2) 


v

 factorial (1) 

D. Note well that a recursive definition/procedure differs from a circular
definition/procedure. A recursive definition/procedure must have at
least one nonrecursive component; and it must be guaranteed that
the definition/procedure will terminate after a finite number of
recursions for any (finite) input.
1. The case(s) where no recursive calls is/are needed are called
BASE CASE(S). There must be at least one base, but there can be more.
Example: For factorial, the base case is n <= 1.
2. The case(s) that involve recursive calls are called GENERAL CASE(S).
Every general case must have the property that each recursion
moves closer to a base case.
Example: For factorial, the recursive cases have n > 1, and reduce
n by one on the recursion, thus moving closer to the
base case.
3. The correct operation of a recursive algorithm is often demonstrated by
the use of MATHEMATICAL INDUCTION.
Example: Thm: The recursive definition/function for factorial will
terminate after n1 recursions for all n >= 1.
Proof: By induction:
a. Basis: When n=1, the algorithm terminates after 0 recursions.
b. Hypothesis: Suppose that there exists a k such that for all
n 1 <= n <= k the algorithm terminates in n1 recursions.
c. Induction step: When n = k+1 the algorithm terminates after
n1 = k recursions.
Proof: We calculate k+1 factorial as (k+1) * k factorial. But
calculating k factorial involves k1 recursions by our hypothesis.
Therefore k+1 factorial involves k recursions QED.
The correctness of the final result is ensured because of conformity
to the definition of factorial, which is, itself, recursive
II. Why use recursion?
   
A. Some entities are most naturally defined using recursion:
1. Factorial
2. Fibonacci numbers: Fib(N) = IF N <= 2 THEN 1 ELSE Fib(N1) + Fib(N2)
Class exercise: write code based on the definition
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n2) + fib(n1);
}
3. Syntax of languages  both artificial and natural
a. Ex: You will see in CPS320 how recursive definitions can be
used to define several classes of formal language
b. Ex: Syntax for a simple expression grammar
PROJECT
Demo interpreter based on this grammar
c. This is true  though in a more complex way  of the full
grammar of a language like Java
PROJECT
4. Many Artificial Intelligence applications use recursive lists
5. Trees: A tree consists of a root and zero or more subtrees, each of
which is a tree. (Draw)
B. Some problems are most easily solved using recursion.
1. Ex: Towers of Hanoi
a. Illustrate using model.
b. Observe:
i. For the case N=1 the problem is trivial.
ii. For N>1 the problem can be solved in terms of two subproblems
of size N1: To move N disks from peg A to peg B:
(1) Move top N1 from peg A to peg C
(2) Move 1 disk from peg A to peg B
(3) Move N1 disks from peg C to peg B
c. Demonstrate for case N=3
d. A portion of a program: PROJECTABLE OF PLAIN VERSION
void hanoi(int n, char start, char finish, char help)
/* Recursive procedure that does the work. Prints directions for
* moving n disks from start to finish using help.
*/
{
if (n <= 1)
cout << "Move disk from peg " << start << " to peg "
<< finish << endl;
else
{
hanoi(n1, start, help, finish);
cout << "Move disk from peg " << start << " to peg "
<< finish << endl;
hanoi(n1, help, finish, start);
}
}
void main(int argc, char * argv [])
{
int size;
cout << "How many disks? ";
cin >> size;
hanoi(size, 'A', 'B', 'C');
}
e. DEMO program:
plain version
graphic version
2. In general, recursion is an appropriate way to tackle a problem if:
a. It has a "size".
b. It is trivial for a certain size.
c. A "big" problem can always be reduced to one or more subproblems of
lesser size.
d. Examples:
i. Find the Greatest Common Divisor of A,B (A >= B)
(1) Let B be the "size"
(2) The problem is trivial when B is 0.
(3) For B > 0, the problem can be reduced to GCD(B, A MOD B)
(Euclid's Algorithm  for proof see Dromey p 97 f)
Now clearly A MOD B < B; therefore we have reduced the size
of the problem
Class exercise  develop code based on definition:
int gcd(int a, int b)
{
if (b == 0)
return a;
else
return gcd(b, a % b);
}
ii. Sorting a list of nonequal items into ascending order:
(1) Let the number of items in the list be the size (N).
(2) The problem is trivial when N=1
(3) For N>1 we can reduce the problem to 2 subproblems of
smaller size, as follows:
(a) Choose an arbitrary item from the list  say the
first. Call it M.
(b) Divide the list into two sublists  one consisting of
all items smaller than M and one consisting of all items
greater than M.
(c) Clearly each sublist contains less than N items, since M
is not a member of either.
(d) Sort the original list by sorting each sublist and then
gluing back together: sorted list of items smaller than
M; M; sorted list of items larger than M.
(4) This is the basis of a standard sorting algorithm known as
quicksort, which we will look at later in the course.
iii. Generating all permutations of a list of distinct items.
(1) Let the number of items in the list be the size (N).
(2) The problem is trivial when N=1
(3) For N > 1 we can reduce the size of the problem by 1 as
follows:
(a) Let each of the N items, in turn, serve as the last
item
(b) For each case, form all the permuations to the
N1 remaining items, then append the chosen item
at the end.
(4) Class exercise: develop code based on above
PROJECT Code
void exchange(char & c1, char & c2)
/* Exchange two characters in an array */
{
char temp = c1;
c1 = c2;
c2 = temp;
}
void permute(char values[], int n)
/* Finds all the permutations of the first n entries of values.
* When n = 1 (base case) prints out the current contents of values
*/
{
if (n == 1)
{
// BASE CASE
cout << values << endl;
}
else
{
for (int i = 0; i < n; i ++)
{
exchange(values[i], values[n1]);
permute(values, n  1);
exchange(values[i], values[n1]);
}
}
}
int main(int argc, char * argv[])
{
int N;
cout << "Enter N  realize that there are N! permutations!: ";
cin >> N;
char values[N+1];
for (int i = 0; i < N; i ++)
values[i] = 'A' + i;
values[N] = '\0';
permute(values, N);
}
DEMO FOR N = 4
III. Classes of Recursive Algorithms:
    
A. A linear recursive algorithm is one in which each nontrivial call to
the recursive procedure gives rise to one new call to that procedure 
i.e. the procedure calls itself only at one point:
Examples: ASK
factorial, gcd
 An important subcategory of linear recursive is tailrecursive. In a
tailrecursive algorithm, the recursive call is the very last step in
the algorithm. Tailrecursive algorithms can be easily converted to
nonrecursive form, replacing the recursion with a loop.
Example of transformation to a nonrecursive version: gcd
int gcd(int a, int b)
{
while (b != 0)
{
int oldA = a;
a = b;
b = oldA % b;
}
return a;
}
[ Note how the code becomes more complex due to the need to keep
track of both "old" and "new" values of a; having multiple "versions"
of a is handled automatically in the recursive version. ]
B. A binary recursive algorithm is one in which each nontrivial call to
the recursive procedure gives rise to two new calls to that procedure
ie the procedure calls itself at two points. Many of the most important
recursive algorithms fall into this category  including those based
on a "divide and conquer" approach.
Ex: Fibonacci, towers of Hanoi, quicksort
C. A multiple recursive algorithm is one in which each nontrivial call
to the recursive procedure gives rise to a varying number of new calls
to that procedure (often more than two). Frequently, such a procedure
calls itself from within a loop.
Ex: permute
D. A mutually recursive algorithm is one which contains several recursive
procedures which call themselves indirectly  e.g. A calls B, B calls
C, and C calls A. Parsers for languages (artificial and natural) are
often mutualrecursive.
Ex: In the grammar for expressions we looked at earlier, we have a
several cases of simple linear recursion, plus a simple case of
mutual recursion
PROJECT
Expressions are defined in terms of terms
Terms are defined in terms of factors
One alternative for a factor is a parenthesized expression
IV. Some Comments on the Efficiency of Recursion
       
A. Recursion is often a good strategy for finding a solution to a problem
(and may be about the only way to find a solution.)
B. Often, recursive solutions are more efficient than ones that might be
discovered other ways. In particular, "divide and conquer" algorithms
exploit this property. We will see several examples of this throughout
the course.
C. However, recursion can also lead to inefficient solutions.
1. Tail recursive algorithms can always be easily transformed to
nonrecursive ones using a simple loop, and linear recursive
algorithms can frequently be transformed this way as well. When
recursion can be easily replaced by a a loop, the resulting
algorithm is almost always more efficient, because there is
significant overhead associated with method calls.
Example: Nonrecursive (and preferred) calculation of factorial:
int factorial(int n)
{
int result = 1;
for (int i = n; i > 1; i )
result *= i;
return result;
}
2. Sometimes, recursion yields a really bad solution that can be
replaced by a much more efficient one using a loop. (This is the
strategy called dynamic programming, which we will discuss at
the end of the course.)
For example, the recursive version of Fibonacci requires fib(N)
iterations to calculate fib(N), which has exponential complexity.
An iterative solution that has linear complexity is as follows:
int fib(int n)
{
if (n <= 2)
return 1;
int last = 1;
int nextToLast = 1;
int current = 1;
for (int i = 3; i <= n; i ++)
{
current = nextToLast + last;
nextToLast = last;
last = current;
}
return current;
}