Recursion
Recursion
Recursion
Phil Tayco
Slide version 1.0
Mar. 8, 2015
Recursion
Algorithm categories
We are used to seeing code written following the
3 categories of programming:
Recursion
A different representation
Some algorithms can be stated as functions similar
to mathematical induction for solving series:
Base case: the part of the solution that is represents the
first element of the series
Inductive case: the rest of the solution that states the
remainder of the series in terms of itself
Recursion
Factorials
Start with a math example
A factorial is an integer multiplied by the each number in
the series from that number descending to one
Represented with an exclamation point, examples include:
5!
4!
3!
2!
1!
=
=
=
=
=
5
4
3
2
1
*
*
*
*
4*3*2*1
3*2*1
2*1
1
Recursion
Patterns
1!
2!
3!
4!
=
=
=
=
1
2 * 1 = 2 * 1!
3 * 2 * 1 = 3 * 2!
4 * 3 * 2 * 1 = 4 * 3!
The factorials of 0 and 1 are also 1. They dont fit the general case so we
consider these to be special (or base) cases
Put the two together and you can state that the factorial for any given
number n (assuming negative numbers are already excluded):
If n = 0 or n = 1, the answer is 1
If n > 1, the answer is n * (n 1)!
We refer to the first part as the base case and the second as the inductive
case (also called the general case or the recursive case)
This can be stated the same way in program code
Recursion
long factorial (int n)
{
if (n == 0 || n == 1)
return 1;
return n * factorial(n 1);
}
Recursion
Code analysis
Recursion
Graphical view of x = factorial(3);
factorial(n = 2)
return 2 * factorial (2 1);
factorial(n = 3)
return 3 * factorial (3 1);
main()
x = factorial(3);
Recursion
Recursion analysis 1
The main function begins with a standard function call to factorial
passing a value of 3
In the first instance of factorial(3), the base case check is false (n
does not equal 0 nor 1)
Therefore, in factorial(3), the line return n * factorial(n-1); is
executed
This temporarily halts execution in factorial(3) as it must wait for
the return of factorial(n-1)
This takes us to the next function instance of factorial(2)
Note that factorial(3) and factorial(2) are separate execution
instances just like any other function call. These function calls just
happen to be using the same code
The same sequence occurs in factorial(2) where another recursive
function call will take place to factorial(1)
Recursion
Graphical view of x = factorial(3);
factorial(n = 1)
return 1;
factorial(n = 2)
return 2 * factorial (2 1);
factorial(n = 3)
return 3 * factorial (3 1);
main()
x = factorial(3);
Recursion
Recursion analysis 2
In the function instance of factorial(1), the base
case is now reached
No further recursive function calls occur, and a
value of 1 is returned
Like any other function that completes, the return
value goes back to the original function call for
use
In this case the original function call is the
previous instance of itself and the unwinding
begins
Recursion
Graphical view of x = factorial(3);
1
factorial(n = 2)
return 2 * 1;
factorial(n = 3)
return 3 * factorial (3 1);
main()
x = factorial(3);
Recursion
Recursion analysis 3
factorial(2) was the instance that made the function call to
factorial(1)
factorial(1) reached the base case and simply returns a
value of 1
That value comes back to factorial(2) at the point where
the function call was made
That point is in the line return n * factorial(n-1);
In this instance, then, the line of code in factorial(2) is now
return 2 * 1; because the recursive function call is
replaced with that functions return value
This results in a return 2; which now continues the
unwinding by returning that value to factorial(2)s original
function caller which was factorial(3)
Recursion
Graphical view of x = factorial(3);
2
factorial(n = 3)
return 3 * 2;
main()
x = factorial(3);
Recursion
Recursion analysis 4
factorial(3) now receives the return value of 2 exactly like
factorial(2) received the return value of 1 from factorial(1)
That value of 2 is applied to the line it was called from
which will result in return 3 * 2; in the factorial(3)
function
The unwinding now completes with factorial(3) returning a
value of 6 back to its original function caller. In this
example, that function is main
main called factorial(3) and is assigning that functions
return value into x and thus completing the recursive line of
execution
Recursion
Graphical view of x = factorial(3);
6
main()
x = 6;
Recursion
Function call stack
Recall in the stacks and queues discussion that one of the
examples of using a stack is function call management
When a function is called, in instance of that function is
pushed onto the stack and executes as coded. When the
function completes, the instance is popped from the stack
any value returned is passed back into the next instance on
top at the point where it made its function call
The same process is happening with recursion. The key
difference is that the function instances created are using
the same function code
The recursive functions are using the same code, but the
logical design of the base and inductive cases set it up so
that the recursive loop will eventually end when the base
case is reached
Recursion
Practice, practice, practice
Understanding recursion is not trivial
Just like other programming concepts,
understanding the theory and the code starts with
practicing different algorithms and walking
through the code, line by line
In this case, following the function call stack is
necessary as well. It is very easy to get lost in the
recursion without the visualization
Another famous example and mathematical
sequence: Fibonacci numbers
Recursion
Theory and efficiency
Can this factorial example be written
without recursion? Of course! In
previous classes, you probably did it
with a for or while loop
In theory, any recursive loop can be
written as a standard loop
Recursion
Fibonacci numbers
What is the pattern in this sequence? Starting at Fibonacci number 3, the number
equals the sum of the previous 2 numbers:
Fib
Fib
Fib
Fib
at
at
at
at
location
location
location
location
3
4
5
6
=
=
=
=
1
2
3
5
which
which
which
which
equals
equals
equals
equals
0
1
1
2
+
+
+
+
1
1
2
3
which
which
which
which
equals
equals
equals
equals
Fib
Fib
Fib
Fib
at
at
at
at
1
2
3
4
+
+
+
+
Fib
Fib
Fib
Fib
at
at
at
at
2
3
4
5
Like we did with the factorial, we can restate this in general terms:
The Fibonacci numbers at locations 1 and 2 dont fit the general case which imply
that these are the base cases
Put the two together and you can state that finding the Fibonacci number at location
n is:
If n = 1, the answer is 0
If n = 2, the answer is 1
If n > 2, the answer is Fib(n-2) + Fib(n-1)
Given this and our current understanding of recursive programming, the code is a
near direct translation of the formula
Recursion
int fib(int n)
{
if (n <= 1)
return 0;
if (n == 2)
return 1;
return fib(n-2) + fib(n-1);
}
Recursion
Code analysis
The base cases and inductive case follows a similar pattern
as the factorial
Notice here that the line of code with the recursion is
making 2 recursive functions calls on the same line
This means if the recursive line is reached, 2 recursive calls
are handled before that value is returned
Practice understanding this by drawing the function call
stack and following the execution with x = fib(4);
If you can do this on your own and feel comfortable with it,
you have a nice start to understanding recursion
Recursion
Main calls fib(4). In fib(4), base cases are not true.
Thus, we call return fib(2) + fib(3);
fib(n = 4)
return fib(2) + fib(3);
main()
x = fib(4);
Recursion
fib(2) is handled next and is a base case, so it
returns 1
fib(n = 2)
return 1;
fib(n = 4)
return fib(2) + fib(3);
main()
x = fib(4);
Recursion
1 is returned back to fib(4) where fib(2) was called.
Now the fib(3) part of the code must be executed
1
fib(n = 4)
return 1+ fib(3);
main()
x = fib(4);
Recursion
fib(3) is next. This is not a base case, so yet
another set of recursion occurs
fib(n = 3)
return fib(1) + fib(2);
fib(n = 4)
return 1 + fib(3);
main()
x = fib(4);
Recursion
fib(1) goes first and is a base case
fib(n = 1)
return 0;
fib(n = 3)
return fib(1) + fib(2);
fib(n = 4)
return 1 + fib(3);
main()
x = fib(4);
Recursion
0 is returned from fib(1). Back in fib(3), we call
fib(2) which we already know will return 1
0
fib(n = 3)
return 0 + fib(2);
fib(n = 4)
return 1 + fib(3);
main()
x = fib(4);
Recursion
fib(3) is now complete and will return 0+1 to fib(4)
fib(n = 3)
return 0 + 1;
fib(n = 4)
return 1 + fib(3);
main()
x = fib(4);
Recursion
With fib(3) complete for fib(4), fib(4)s recursion is
now complete and will return 2 to main and
complete all the recursion
1
fib(n = 4)
return 1+ 1;
main()
x = fib(4);
Recursion
All done!
2
main()
x = 2;
Recursion
Recursive Fibonacci analysis
Note again that the process of calling a function, pushing
the new function onto the stack for processing and
returning to the point of the function call is consistent
whether its a call to another function or a recursive call
Understanding the recursion call process requires drawing
out the different function instances and tracing the flow of
control
Simple mathematical series that can be stated inductively
are classic cases for recursion, but are not the only ones
A famous recursive function example is the Towers of Hanoi
Recursion
The Legend of the Towers of Hanoi
An Asian monk is tasked with transferring 64 disks
from one pillar to another
Each disk is different in size with a smaller disk
always on top of a larger disk (making the 64 th
disk on the bottom the largest of them all)
The are three pillars total (call them A, B and C)
and all 64 disks are on pillar A
The goal is to get them all to C following 2 rules:
Only one disk can move at a time
A larger disk cannot rest on top of a smaller disk
Recursion
Algorithm
Assuming it takes one second to move a disk and he
started right now, how long before the world ends?
More importantly for us, what is the algorithm to do
this?
Obviously, in the current context, recursion is
involved, but developing this algorithm is not as
intuitive as the previous examples
As with other situations, work out solutions with
smaller values to derive the base and inductive
cases
Lets start with 1 disk instead of 64
Recursion
With one disk, the move is obvious. Move disk from
A to C. Another way to say it is we are moving
the disk from start to destination
Recursion
Another way to say it is we are moving the disk
from start to destination.
Recursion
Establish our base
The move from start to destination is occurring
with 1 disk
Stated another way, if we are looking at one disk,
move it to where you want it to go
This sounds like a base case, but may seem
peculiar given that the overall rules for the
problem is that you can only move one disk at a
time anyway
Lets move to the 2 disk situation
Recursion
Here, if we move the first disk on A to C, the next
disk on A wont be able to go to C without the
smaller one out of the way
Recursion
Step 1: Move disk from A to B. B acts as a
temporary pillar, while A and C are start and
destination pillars respectively
Recursion
Step 2: Now we can move the disk from A (start) to
C (destination)
Recursion
Step 3: Last move is simple. From disk from B
(temp) to C (destination)
Recursion
2 disk case
The 2 disk situation shows a key 3 step process
Move 1 disk from start to temp
Move 1 disk from start to destination
Move 1 disk from temp to destination
Recursion
Start. Where do we go from here?
Recursion
If we follow the same moves (A to B, A to C, B to
C), we would have 2 disks at C, but the one big
disk still at A. Thus, we should not end up with
these 2 disks on C
Recursion
We should then try to get the 2 disks to a position
where they are both on B. Then, the big disk on A
can get to C
Recursion
How do we get to that point? Note that for these 2
disks, the steps in the previous example apply,
but B would be our destination and C would be
our temp
A (start)
B (dest)
C (temp)
Recursion
Given these labels, the 3 moves are the same as
before. Step 1: Move A to C (start to temp)
A (start)
B (dest)
C (temp)
Recursion
Step 2: Move A to B (start to dest)
A (start)
B (dest)
C (temp)
Recursion
Step 3: Move C (temp) to B (dest). Note now that
this temporary 2 disk goal of getting them to
destination B is complete
A (start)
B (dest)
C (temp)
Recursion
In the overall picture for 3 disks, our destination is
C and at this point, we have successfully moved 2
disks off A to the temporary pillar B
A (start)
B (temp)
C (dest)
Recursion
3 disk case so far
Recall the steps when there are only 2 disks
Move 1 disk from start to temp
Move 1 disk from start to destination
Move 1 disk from temp to destination
Recursion
Step 4: Move A to C (temp to dest)
A (start)
B (dest)
C (temp)
Recursion
Almost there!
This is an easy move leaving only the last 2 disks
from temp to move to dest:
Move (n-1) disks from start to temp
Move 1 disk from start to destination
Move 1 disk from temp to destination
Recursion
Step 5: Move B to A (start to temp)
A (temp)
B (start)
C (dest)
Recursion
Step 6: Move B to C (start to dest)
A (temp)
B (start)
C (dest)
Recursion
Step 7: Move A to C (temp to dest)
A (temp)
B (start)
C (dest)
Recursion
Done! So whats the formula?
The general case is complete:
Recursion
void hanoi(int n, char start, char temp, char dest)
{
}
main()
{
hanoi (3, A, B, C);
}
Recursion
void hanoi(int n, char start, char temp, char dest)
{
if (n == 1)
System.out.printf(Move disk from %c to %c\n, start,
dest);
}
main()
{
hanoi (3, A, B, C);
}
Recursion
void hanoi(int n, char start,
{
if (n == 1)
System.out.printf(Move
start, dest);
else
{
hanoi(n-1, start, dest,
System.out.printf(Move
start, dest);
hanoi(n-1, temp, start,
}
}
temp);
disk from %c to %c\n,
dest);
Recursion
Thats it?!
Thats it! The only way to truly internalize this is
to walk through the code
Like with factorial and Fibonacci, tracing through
the function call stack is important
Lets do that again here with attempting to move
2 disks from A to C with B as our temp
Recursion
Main calls hanoi (2, A, B, C);
hanoi(2)
main()
hanoi(2, A, B, C);
Recursion
hanoi(2) is not a base case, so we go to the
inductive case starting with hanoi(2-1, A, C,
B); Do you see why the function call values are
in that order?
hanoi(2)
hanoi(1, A, C, B);
print(A to C);
hanoi(1, B, A, C);
main()
hanoi(2, A, B, C);
Recursion
hanoi(1) will be a base case and print our first
instruction. Do you see why it prints A to B?
hanoi(1)
print(A to B);
hanoi(2)
hanoi(1, A, C, B);
print(A to C);
hanoi(1, B, A, C);
main()
hanoi(2, A, B, C);
Recursion
hanoi(1) is done and we return to hanoi(2). Next
code in hanoi 2 is another print
hanoi(2)
hanoi(1, A, C, B);
print(A to C);
hanoi(1, B, A, C);
main()
hanoi(2, A, B, C);
Output so far:
Move A to B
Recursion
hanoi(2) continues with the last line of its inductive
step and gets ready to make another recursive
call. Note again the order of the function call
values
hanoi(2)
hanoi(1, A, C, B);
print(A to C);
hanoi(1, B, A, C);
main()
hanoi(2, A, B, C);
Output so far:
Move A to B
Move A to C
Recursion
hanoi(1) is another base case with different start
and dest values
hanoi(1)
print(B to C);
hanoi(2)
hanoi(1, A, C, B);
print(A to C);
hanoi(1, B, A, C);
main()
hanoi(2, A, B, C);
Output so far:
Move A to B
Move A to C
Recursion
hanoi(1) and then hanoi(2) will be done and we
return to main with the correct output on the
screen!
Output so far:
main()
hanoi(2, A, B, C);
Move A to B
Move A to C
Move B to C
Recursion
Code is beautiful
This is a key (and historic) example of the Towers
of Hanoi solution
Tracing through with only 2 disks may be
interesting, but to truly appreciate it in action,
trace through the code with 3 or 4 disks. This is
an excellent way to practice recursion
Recursion
Analysis
How many steps will we see with 3 disks? How
many with 4? Can you generalize it to a formula?
1 disk = 1 move
2 disks = 3 moves
3 disks = 7 moves
4 = 15 moves
N = 2n 1
Recursion
Performance
Analyzing the Big-O for comparisons with recursive
solutions is noteworthy, but often not seen as an
improvement to a solution
Moreover, the function call stack is heavily utilized making
recursion higher in memory usage
Functions like factorial and Fibonacci can probably
perform faster and use memory better using standard
loops
Hanoi and some other solutions well see, though, could
be a challenge to do iteratively versus with recursion
Thus, recursive solutions are beneficial in developing a
functional algorithm, but not necessarily for performance
and memory usage
Recursion
Summary
Practice, practice, practice. Understanding recursive
code by tracing is the first step
There are many problems that have potential
recursive solutions. Once you are able to read and
trace recursive code, the next big challenge is
learning how to develop a recursive algorithm
The key is learning how to identify base and
inductive cases. They are not easy to do, but very
gratifying when developed
Examples of other solutions always help too. We will
see more as we revisit the advanced sorting
algorithms next