Recursion Self-Referencing Functions

advertisement
Recursion
Self-Referencing Functions
Problem # 1
Write a function that, given n, computes n!
n! == 1 * 2 * ... * (n-1) * n
Example:
5! == 1 * 2 * 3 * 4 * 5 == 120
Specification:
Receive:n, an integer.
Precondition: n >= 0 (0! == 1 && 1! == 1).
Return: n!, a double (to avoid integer overflow).
Preliminary Analysis
At first glance, this is a counting problem,
so we could solve it with a for loop:
double Factorial(int n)
{
double result = 1.0;
for (int i = 2; i <= n; i++)
result *= i;
return result;
}
But let’s instead learn a different approach...
Analysis
Consider:
so:
subsituting:
n! == 1 * 2 * ... * (n-1) * n
(n-1)! == 1 * 2 * ... * (n-1)
n!== (n-1)! * n
We have defined the ! function in terms of itself.
Historically, this is how the function was defined
before computers (and for-loops) existed.
Recursion
A function that is defined in terms of itself is called selfreferential, or recursive.
Recursive functions are designed in a 3-step process:
1. Identify a base case -- an instance of the problem
whose solution is trivial.
Example: The factorial function has two base cases:
if n == 0: n! == 1
if n == 1: n! == 1
Induction Step
2. Identify an induction step -- a means of solving the
non-trivial (or “big”) instances of the problem using
one or more “smaller” instances of the problem.
Example: In the factorial problem, we solve the “big”
problem using a “smaller” version of the problem:
n! == (n-1)! * n
3. Form an algorithm from the base case and induction
step.
Algorithm
// Factorial(n)
0. Receive n.
1. If n > 1:
Return Factorial(n-1) * n.
Else
Return 1.
Discussion
The scope of a function begins at its prototype
(or heading) and ends at the end of the file.
Since the body of a function lies within its scope, nothing
prevents a function from calling itself.
Coding
/* Factorial(n), defined recursively
* ...
*/
double Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1.0;
}
Behavior
Suppose the function is called with n == 4.
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
The function starts executing, with n == 4.
Factorial(4)
n
4
return
?
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
The if executes, and n (4) > 1, ...
Factorial(4)
n
4
return
?
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
and computing the return-value calls Factorial(3).
Factorial(4)
n
4
return
?
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
This begins a new execution, in which n == 3.
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
Its if executes, and n (3) > 1, ...
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
and computing its return-value calls Factorial(2).
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
This begins a new execution, in which n == 2.
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Behavior
Its if executes, and n (2) > 1, ...
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Behavior
and computing its return-value calls Factorial(1).
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Behavior
This begins a new execution, in which n == 1.
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Factorial(1)
n
1
return
?
Behavior
The if executes, and the condition n > 1 is false, ...
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Factorial(1)
n
1
return
?
Behavior
so its return-value is computed as 1 (the base case)
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
Factorial(1)
n
1
return
1
Behavior
Factorial(1) terminates, returning 1 to Factorial(2).
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
?
= 1 * 2
n
1
return
1
Behavior
Factorial(2) resumes, computing its return-value:
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Factorial(2)
n
2
return
2
= 1 * 2
Behavior
Factorial(2) terminates, returning 2 to Factorial(3):
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return ?
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
= 2 * 3
n
2
return
2
Behavior
Factorial(3) resumes, and computes its return-value:
Factorial(4)
n
4
return
?
Factorial(3)
n
3
int Factorial(int n) return 6
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
= 2 * 3
Behavior
Factorial(3) terminates, returning 6 to Factorial(4):
Factorial(4)
n
4
return
?
= 6 * 4
n
3
int Factorial(int n) return 6
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
Factorial(4) resumes, and computes its return-value:
Factorial(4)
n
4
return 24
= 6 * 4
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Behavior
Factorial(4) terminates, returning 24 to its caller.
Factorial(4)
n
4
return 24
int Factorial(int n)
{
if (n > 1)
return Factorial(n-1) * n;
else
return 1;
}
Discussion
If we time the for-loop version and the recursive
version, the for-loop version will usually win,
because the overhead of a function call is far
more time-consuming than the time to execute
a loop.
However, there are problems where the recursive
solution is more efficient than a corresponding
loop-based solution.
Problem # 2
Consider the exponentiation problem:
Given two values x and n, compute xn.
Example: 33 == 27
Specification:
Receive: x, n, two numbers.
Precondition: n >= 0 (for simplicity) &&
n is a whole number.
Return: x raised to the power n.
Analysis
The loop-based solution requires @ n “steps”
(trips through the loop) to solve the problem:
double Power(double x, int n)
{
double result = 1.0;
for (int i = 1; i <= n; i++)
result *= x;
}
return result;
There is a faster recursive solution.
Analysis (Ct’d)
How do we perform exponentiation recursively?
Base case:
n == 0
 Return 1.0.
Analysis (Ct’d)
Induction step: n > 0
We might recognize that
xn == x * x * ... * x * x // n factors of x
and
xn-1 == x * x * ... * x
// n-1 factors of x
and so perform a substitution:
 Return Power(x, n-1) * x.
However, this requires @ n “steps” (recursive calls),
which is no better than the loop version.
Analysis (Ct’d)
Induction-step: n > 0.
Instead, we again begin with
xn == x * x * ... * x * x
// n factors of x
but note that
xn/2 == x * ... * x
// n/2 factors of x
and then recognize that when n is even:
xn == xn/2 * xn/2
while when n is odd:
xn == x * xn/2 * xn/2
Analysis (Ct’d)
Induction step: n > 0.
We can avoid computing xn/2 twice by doing it once,
storing the result, and using the stored value:
a.
b.
Compute partialResult = Power(x, n/2);
If x is even:
Return partialResult * partialResult.
Else
Return x * partialResult * partialResult.
End if.
This version is significantly faster than the loop-based
version of the function, as n gets larger.
Algorithm
0. Receive x, n.
1. If n == 0:
Return 1.0;
Else
a. Compute partialResult = Power(x, n/2);
b. if n is even:
Return partialResult * partialResult;
else
Return x * partialResult * partialResult;
end if.
Coding
We can then code this algorithm as follows:
double Power(double x, int n)
{
if (n == 0)
return 1.0;
else
{
double partialResult = Power(x, n/2);
}
}
if (n % 2 == 0)
return partialResult * partialResult;
else
return x * partialResult * partialResult;
which is quite simple, all things considered...
How Much Faster?
How many “steps” (recursive calls) in this version?
Power(x, i)
i
Power(x,
Power(x,
Power(x,
Power(x,
Power(x,
Power(x,
Power(x,
...
Power(x,
0)
1)
2)
4)
8)
16)
32)
0
1
2
4
8
16
32
n)
n
Steps
(loop version)
0
1
2
4
8
16
32
n
Steps
(this version)
0
1
2
3
4
5
6
log2(n)+1
How Much Faster? (Ct’d)
The larger n is, the better this version performs:
Power(x, i)
i
Power(x,
Power(x,
Power(x,
Power(x,
...
1024
2048
4096
8192
1024)
2048)
4096)
8192)
Steps
(loop version)
1024
2048
4096
8192
Steps
(this version)
11
12
13
14
The obvious way to solve a problem may not be the
most efficient way!
Summary
A function that is defined in terms of itself is called a
recursive function.
To solve a problem recursively, you must be able to
identify a base case, and an induction step.
Determining how efficiently an algorithm solves a
problem is an area of computer science known as
analysis of algorithms.
Analysis of algorithms measures the time of an
algorithm in terms of abstract “steps”,
as opposed to specific statements.
Download