Recursion
Chapter
4
Understanding recursion
•
To really
understand what is happening with recursion, we need to look at what happens to
a program on the lowest level; i.e. on the machine level
• We will take a brief look at what the compiler
does, and what the loader (a component of the operating system) does.
• Then we will look at function calls and the activation
record.
The compiler
• Remember, a program consists of two parts:
–
The instructions
to the machine
–
The data your
program works with
–
Both of these
require space in RAM when you program is loaded so it will execute
•
Counting bytes
– When the compiler translates your source code into
machine language, it keeps track of how many bytes are in the program.
– In addition, it counts the bytes the program uses to
store data (this is the data stored in static RAM).
•
Communicating
with the operating system
– Part of the compiled program is a header which
tells the loader the total number of bytes needed
– This header also contains information from the linker,
so all the information for the executable program is here (in the header)
The loader
•
The loader is the
component of the operating system that loads the executable program into RAM
•
The entire
program with all its functions is not necessarily loaded contiguously.
•
The loader puts
the address where execution should begin into the program
counter (a special register in the CPU).
•
Then the
operating system turns control of the computer over to the program (i.e. it
executes).
•
The instruction
that is executed is the instruction whose address is in the program counter.
• For the program to loop or branch, or jump to a
function, the address where execution should go must be put into the program counter
Function calls & Activation records
• When a function is called the system must keep tract
of where to return when the function completes execution.
•
It also needs to
keep tract of the local environment for each function call. This includes:
–
The values of the
arguments of the function
–
Any local
variables
–
The value to be
returned
• This information is kept on the system stack and is
called an activation record.
• Example on overhead.
Recursive functions
• A function is recursive if it contains a call to
itself
• Recursion breaks a problem down into several smaller
problems
• A recursive solution solves a problem by
solving smaller and smaller instances of the same problem
• Eventually, the new problem will be so small that its
solution is known.
• This known solution helps solve each of the larger
problems
Example: Finding
the factorial of a number
•
What is a
factorial?
–
Factorial(5) = 5*4*3*2*1
–
Factorial(0) =1
–
Factorial(n) = n*(n-1)*(n-2)*…*1
–
The factorial of
a negative integer is undefined
• Note: a
recursive solution of this problem is not at all efficient
–
But it is a good
first example
• We will write this first as a function, then convert
the function to an algorithm for a computer
Recursively defined functions
• To define any function recursively:
1. Specify the value of the function at 0 or 1
-
this is the problem so small that you know the
answer; i.e. the base case
2. Give a rule for finding its value from its
values at smaller cases
•
To define the
factorial function recursively:
1. factorial(1) =
1; (base case)
2. factorial(n)
= n * factorial(n-1)
Converting the function to an algorithm
1. factorial(1) = 1;
(base case)
2.
factorial(n) = n * factorial(n-1)
int fact(int n)
if (n = 1) return 1;
else return n*fact(n-1);
•
Look at the
activation record for a call of fact(4)
Decimal to Binary conversions
• One way to do this makes use of the fact that:
–
The rightmost bit
has the value of n%2;
–
The second
rightmost bit (n/2)%2
–
Etc.
–
For example:
convert 29 to binary using this method
void writeBinary(int n)
{ if ( n==0 || n==1) cout << n;
else { writeBinary(n/2);
cout
<< n%2; }
}
• Look at the execution of writeBinary(29) on the
overhead.
void writeBinary(int
n)
{ if
(n==0 || n==1) cout<<n;
else
{ writeBinary(n/2);
void writeBinary(int
n)
{ if
(n==0 || n==1) cout<<n;
else
{ writeBinary(n/2);
void writeBinary(int
n)
{ if (n==0 || n==1) cout<<n;
else { writeBinary(n/2);
void writeBinary(int
n)
{ if (n==0 || n==1) cout<<n;
else { writeBinary(n/2);
void writeBinary(int n)
{ if (n==0 || n==1) cout<<n;
else { writeBinary(n/2);
cout
<< n%2; }
}
cout << n%2; }
}
cout << n%2; }
}
cout << n%2; }
}
cout << n%2; }
}
Form of recursive solutions
• 1. Every recursive call must make the problem smaller
• 2. Each smaller problem must be exactly the same as
the original problem, only smaller
• 3. The problem must become small enough that you know
the solution
– this is called the base case
•
4. This strategy
is called divide and conquer
Questions to ask about recursive solutions
• Can you define the problem in terms of a smaller
problem of the same type?
• Does each recursive call diminish the size
of the problem?
• Do you know the instance of the problem to be the base
case?
• As the problem size diminishes, will it reach the
base case?
Euclid's algorithm: finding the greatest common divisor
•
How would you
find the greatest common divisor of 42 and 147?
– Most people would find the common factors.
•
Euclid
was a Greek who discovered a long time ago that
– If a and b are positive integers with a>b such that
b is not a divisor of a, then gcd(a,b) = gcd(b,
a mod b)
•
Writing this as a
recursive function
Base
case:
gcd(a,
b) = b if a mod b = 0
Rule gcd(a,b)
= gcd(b, a mod b)
• Algorithm:
• int gcd(int a, int b)
{
if (a % b = = 0) return b;
else return gcd(b, a %
b);
•
}
Recursion and iteration
• Recursion is an alternative to iteration
• Anything that can be written recursively can also be
written iteratively.
• Often recursive solutions are more elegant and simpler
than iterative solutions
• But not all recursive solutions are better than
iterative solutions
• Some are so inefficient that it may take years on the
fastest computers to solve.
Counting things
• This problem illustrates two things
–
More than one
base case
–
Tremendously
inefficiency
• But right now we are interested in the process of
recursion, not its efficiency
•
Counting rabbits. Assume:
–
At two months of
age rabbits can reproduce
–
Rabbits are
always born in male/female pairs
–
Every month each
mature male/female pair reproduces exactly one male/female pair
–
Rabbits never die
• Problem: start
with one pair of young rabbits; how many will you have after 6 months?
How to compute rabbit(n)
• This is called the Fibonacci sequence
• We would like to be able to write a recursive function
for number of rabbit pairs at month n
•
Since this
function is recursive, we know we will use month
(n-1) but we also need to use
month (n-2)
• Base case: Month 1 there is one pair of rabbits;
rabbbit(1)
= 1
•
Rule: rabbit(n) = rabbit(n-1) + rabbit(n-2)
• int rab(int n)
if n <= 2 return 1;
else return rab(n-1)
+ rab(n-2);