More about Recursion
reading: Dietel sections 3.13-3.14
Example: Fibonacci Numbers
- Represent growth processes in nature
- Recursively defined
- F(n) = F(n-1) + F(n-2)
- Need to define base cases: F(0) = F(1) = 1
- Can write recursive (or iterative) algorithm
int Fibonacci (int n)
{
if( n < 0 ) // Fibonacci not defined on negative numbers
return -1;
if( n == 0 || n == 1) // Can use non-recursive part of definition
return 1;
return ( Fibonacci(n-1) + Fibonacci(n-2) ); // Well-defined since n >= 2
}
int Fibonacci (int n)
{
int f0 = 1, f1 = 1; // Each Fibonacci based on previous two
if( n == 0 || n == 1) // Don't need recursive definition
return 1;
if( n < 0) // Not defined on negative numbers
return -1;
int new_fib, index = 2; // F(index) is current computation
while( index <= n ) // Iteratively compute result, one step at a time
{
new_fib = f0 + f1;
f0 = f1;
f1 = new_fib;
index++;
}
return new_fib;
}
Example: String Reversal
- Want to output string's characters in reverse order
- Can be recursively defined
- Print reverse of tail of string (all except 1st character), then
print first character- Need to define base case: if string empty, no need to output anything (or recurse)
- Can write recursive (or iterative) algorithm
void StringReverse (string s)
{
if( s.length() == 0 ) // No characters remain; done
return;
StringReverse( string( s.begin() + 1, s.end() ) ); // Reverse rest of
// stringcout << s[0]; // Print out first character last
} // Note array-style string access
void StringReverse (string s)
{
int length = s.length(); // Number of characters to output
for( int i = length-1; i > = 0; i--) // Reverse order of output
cout << s[i];
}
Recursion vs. Iteration
- Correspondence between loop iterations and recursive calls
- Advantages of iteration
- Speed: no need to save intermediate values on call stack
- Speed: no overhead from jumping back to start of function code
- Flexibility: no size limit due to stack (limits recursion depth)
- Advantages of recursion
- Often easier to translate recursive definition to recursive function
- Avoids a lot of the "bookkeeping" that an iterative solution requires
- Algorithms like Fibonacci probably more easily understood
- Code generally shorter
Approach to Writing Recursive Functions
- Try to find recursive definition or dependence
- Find the base cases -- solve problem right away
- First test for base cases (and error cases)
- Then perform recursive call if necessary
- Should be closer to base case than original call
- May need more than one call
- Results of recursive call(s) operated on to produce result
General Skeleton
if ( error condition )
return (error value);
if( base case 1)
return (terminal value 1);
.
.
.
if( base case x)
return (terminal value x);
make recursive call(s) (closer to some base case)
operate on returned value(s) as necessary
return answer
Correctness of Recursive Algorithms
- Form correct definitions of base, recursive cases
- Must test for base case before making recursive call
- Recursive call must be closer to base case
- Must not skip over base case! (watch step size)
- Assuming recursive calls work, all non-recursive calculations performed on result must be correct
Kinds of Recursion
- Direct Recursion: function contains an explicit call to itself (what we've seen so far)
- Indirect Recursion: function F calls function G, which in turn calls function F
- Tail Recursion: return value of function is return value of recursive call (no pending operations)
- Linear Recursion: no pending operation invokes another recursive call (Factorial, below)
- Tree Recursion: some pending operation invokes another recursive call (Fibonacci)
Converting Tail Recursion to Iteration
- Iteration usually more efficient
- Some algorithms more naturally expressed recursively
- Want to convert initial recursive algorithm to iterative algorithm
General Skeleton of Tail Recursion
type F ( type x )
{
if ( P(x) ) // Test for base case; form return value
return G(x);
return F( H(x) ); // Make recursive call; may need to modify input.
}
Iterative Version
type F( type x)
{
type temp_x;
while ( ! P(x) ) // As long as we haven't hit the base case
{
temp_x = x; // Compute new value of parameter
x = H( temp_x );
}
return G(x); // Perform any necessary modifications for base case
}
Example: Factorial
Tail-Recursive Version -- Initial call is Factorial(n,1);
int Factorial( int n, int result)
{
if ( n == 1 ) // Test for base case; no further multiplication necessary
return result;
return Factorial( n-1, n * result ); // Multiply n into result so far;
// recursive call computes rest.
}
Iterative Version
int Factorial( int n )
{
int temp_n, temp_result, result = 1;
while( n != 1 ) // As long as this isn't the base case
{
temp_n = n; // Save old values
temp_result = result;
n = temp_n - 1; // Compute new values
result = temp_result * temp_n;
}
return result; // No modifications necessary for base case
}
What happens when recursive calls are made?
- Same as when any other function call is made
- Call stack: memory to track data associated with function calls
- Calling function pushes any parameters onto stack
- Calling function pushes its return address on the stack
- Called function pushes any local variables on the stack
- Called function proceeds as usual
- When done, pops off local variables and returns to return address
- Calling function then pops off address, parameters
- Limited stack memory limits recursion (or any function call) depth