recursive programming: Writing functions that call themselves to solve problems that are recursive in nature.
Recursive blue shirt
The first person looks behind them:
Recursion and cases: Every recursive algorithm involves at least two cases:
Key idea: In a recursive piece of code, you handle a small part of the overall task yourself (usually the work involves modifying the results of the smaller problems), then make a recursive call to handle the rest.
Ask yourself, "how is ths task self-similar?"
Recursion Tips
Three rules of recursion
Recursive Program structure
recursiveFunc ()
{
if (test for simple case) { // base case
Compute the solution without recursion
} else { // recursive case
1. Break the problem into subproblems of the same form
2. Call recursiveFunc() on each self-similar subproblem
3. Reassamble the results of the subproblems
}
}
Non-recursive factorial
// Returns n!, or 1 * 2 * 3 * 4 * ... * n.
// Assumes n >= 1.
int factorial(int n) {
int total = 1;
for (int i = 1; i <= n; i++) {
total *= i;
}
return total;
}
Important observations:
0!= 1! = 1 4! = 4 * 3 * 2 * 1 5! = 5 * (4 * 3 * 2 * 1) = 5 * 4!
Recursive factorial
// Returns n!, or 1 * 2 * 3 * 4 * ... * n.
// Assumes n >= 0.
int factorial(int n) {
if (n <= 1) {
// base case
return 1;
} else {
return n * factorial(n - 1);
// recursive case
}
}
n), then makes a recursive call to handle the rest
Recursive stack trace: Show the chain of function calls represented through a stack for fact(4)
Consider the following recursive function:
int mystery(int n) {
if (n < 10) {
return n;
} else {
int a = n / 10;
int b = n % 10;
return mystery(a + b);
}
}
What is the result of mystery(648)? (answer: 9)
isPalindrome exercise: write a recursive function isPalindrome that accepts a string and returns true if it reads the same forwards as backwards.
isPalindrome("madam") returns true
isPalindrome("racecar") returns true
isPalindrome("step on no pets") returns true
isPalindrome("able was I ere I saw elba") returns true
isPalindrome("Q") returns true
isPalindrome("Java") returns false
isPalindrome("rotater") returns false
isPalindrome("byebye") returns false
isPalindrome("notion") returns false
findBigO(codeSnippet):
if codeSnippet is a single statement:
return O(1)
if codeSnippet is a loop:
return number_of_times_loop_runs * findBigO(loop_inside)
for codeBlock in codeSnippet:
return the_sum_of findBigO(codeBlock)
What are the subproblems? loop_inside and codeBlock.
Example: compute bigO for the following code using this recursive pseudocode:
for (int i = 0; i < N * N; i += 3) {
for (int j = 3; j <= 219; j++) {
cout << "sum: " << i + j << endl;
}
}
cout << "Enjoy COL100\n";
Show the working of findBigO on this code snippet.
Exercise: Write a function power that accepts integer parameters for a base and exponent and computes base ^ exponent.
Method:
//Assumes exp >= 0
int power(int base, int exp)
{
if (exp == 0) {
return 1;
} else {
return base * power(base, exp - 1);
}
}
Show the execution of power(5, 3);
Can we do better? Notice that x^18 = (x*x)^9; x^9=x*x^8
//Assumes exp >= 0
int power(int base, int exp)
{
if (exp == 0) {
return 1;
} else if (exp % 2 == 0) {
return power(base * base, exp/2); //recursive case 1
} else {
return base * power(base, exp - 1); //recursive case 2
}
}
Exercise: write a recursive function convertFromBinary that accepts an a string of that number's representation in binary (base 2) and returns the base 10 int equivalent. e.g.,
convertFromBinary("111") returns 7
convertFromBinary("1100") returns 12
convertFromBinary("101010") returns 42
How is this problem self-similar? What is the smallest amount of work? When to stop?
// Returns the given int's binary representation.
// Precondition: n >= 0
int convertFromBinary(string binary) {
int length = binary.length();
if (length == 1) {
// base case: binary is same as base 10
return stringToInteger(binary);
}
// recursive case: break number apart
string lastCharacter = binary.substr(length - 1);
string beginning = binary.substr(0, length - 1);
return 2 * convertFromBinary(beginning) +
convertFromBinary(lastCharacter);
}
Exercise: write a recursive function reverseLines that accepts a file input stream and prints the lines of that file in reverse order.
Example input:
Hello world Hello foo Hello bar baz helloExpected output:
baz hello Hello bar Hello foo Hello worldIs this problem self-similar? What is a file that is very easy to reverse? Hint: reversing the lines of a file can be done by (1) reading a line L from the file, (2) printing the rest of the lines in reverse order --- self-similarity, (3) printing the line L.
void reverseLines(ifstream& input) {
string line;
if (getline(input, line)) {
// recursive case
reverseLines(input);
cout << line << endl;
}
}
Where is the base-case?
Exercise: write a function crawl that accepts a file name as a parameter and prints information about that file.
courses
col100
lab2
hello_world.cpp
order_of_evaluation.cpp
lab3
if_then_else.cpp
minor1.pdf
minor2.pdf
eel200
...
Assume following functions are available (using SPL's "filelib.h"):
isDirectory("name") |
Returns whether the filename represents a directory. Can use stat() method in standard C library but with more complicated syntax and semantics. |
listDirectory("name") |
returns a Vector<string> with the names of all files contained in the given directory. Can use a combination of opendir/readdir/closedir operations available in standard C library but with more complicated syntax and semantics. |
How is this problem self-similar? Crawling a directory can be expressed in terms of crawling the subdirectories, albeit with a different indentation.
Base-case? File
// Prints information about this file,
// and (if it is a directory) any files inside it.
void crawl(string filename, string indent) {
cout << indent << getTail(filename) << endl;
if (isDirectory(filename)) {
// recursive case; print contained files/dirs
Vector<string> filelist;
listDirectory(filename, filelist);
for (string subfile : filelist) {
crawl(filename + "/" + subfile,
indent + " ");
}
}
}
Exercise: Write a recursive function fib that accepts an integer N and returns the Nth fibonacci number.
fib(1) = 1 fib(2) = 1 fib(3) = 2 fib(4) = 3 fib(5) = 5 fib(6) = 8 fib(7) = 13 fib(8) = 21 fib(9) = 34 ...How is this problem self-similar? Computing fib(n) can be done easily if we know fib(n-1) and fib(n-2).
// Returns the nth Fibonacci number.
int fib(int n) {
if (n <= 2) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
If you compute fib(6), how many times is fib(2) called?
fib(20), how many times is fib(16) called?
fib(16), how many times is fib(12) called
fib(12), how many times is fib(8) called
fib(16) gets called, we save the value. For each subsequent evaluation of fib(16), we just return the cached value.
Pseudo-code template:
cache = {}. // empty
function f(args):
if I have computed f(args) before:
Look up f(args) result in cache.
else:
Actually compute f(args) result.
Store result in cache.
Return result.
We can create a new variable called cache and pass it as a parameter to our fib function as follows:
int fib(int n, Map<int, int> &cache) {
if (n <= 2) {
return 1;
} else if (cache.containsKey(n)) {
return cache[n];
} else {
int result = fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
}
}
Is this achieving the desired optimization? Can the cache be pass-by-value?
But how to initialize the cache? Should we tell the user that he needs to initialize and pass the cache --- seems ugly!
Wrapper functions
int fibHelper(int n, Map<int, int> &cache) {
if (n <= 2) {
return 1;
} else if (cache.containsKey(n)) {
return cache[n];
} else {
int result = fibHelper(n - 1, cache) + fibHelper(n - 2, cache);
cache[n] = result;
return result;
}
}
int fib(int n)
{
Map<int, int> cache;
return fibHelper(n, cache);
}
Show the evaluation of fib(6) with memoization.
g++ -O3 foo.cpp. -O2 represents that the compiler should optimize the code with optimization-level 3.Examples: Is this tail recursive?
int mystery(int n)
{
if (n < 10) {
return n;
} else {
int a = n/10;
int b = n %10;
return mystery(a + b);
}
}
Answer: yes
What about:
int fact(int n)
{
if (n <= 1) {
return 1;
} else {
return n * fact(n - 1);
}
}
Answer: no
Can we rewrite factorial so that it becomes tail-recursive? Tail-recursive factorial:
int factorial_helper(int n, int accum)
{
if (n <= 1) {
return accum;
} else {
return factorial(n - 1, accum * n);
}
}
int factorial(int n)
{
return factorial_helper(n, 1);
}
Tail recursive solutions often end up passing partial computations as parameters that would otherwise be computed after the recursive call.
Compare with non-recursive factorial (sometimes looking at the non-recursive version of a function can help you find the tail recursive solution):
int factorial(int n)
{
int accum = 1;
for (int i = 1; i <= n; i++) {
accum = accum * i;
}
return accum;
}
The tail-recursive version often looks like a non-recursive version with a variable or a parameter keeping track of the partial computations. The difference is: the loop is replaced by recursive call.
Another tail-recursion example: fib(n)
int fib_helper(int n, int i, int fib_i, int fib_prev)
{
if (i == n) {
return fib_i;
} else {
return fib_helper(n, i + 1, fib_i + fib_prev, fib_i);
}
}
int fib(int n)
{
return fib_helper(n, 1, 1, 0);
}
This implementation is similar to the following non-recursive implementation:
int fib(int n)
{
intt i = 1;
int fib_i = 1;
int fib_prev = 0;
while (i != n) {
int new_i = i + 1;
int new_fib_i = fib_i + fib_prev;
int new_fib_prev = fib_i;
fib_i = new_fib_i;
fib_prev = new_fib_prev;
i = new_i;
}
return fib_i;
}
Another tail-recursive implementation of fib(n) with fewer arguments to helper function:
int fib_helper(int n, int fib_n, int fib_prev)
{
if (n == 1) {
return fib_n;
} else {
return fib_helper(n - 1, fib_n + fib_prev, fib_n);
}
}
int fib(int n)
{
return fib_helper(n, 1, 0);
}
This implementation is similar to the following non-recursive implementation:
int fib(int n)
{
int fib_i = 1;
int fib_prev = 0;
while (n != 1) {
int new_n = n - 1;
int new_fib_i = fib_i + fib_prev;
int new_fib_prev = fib_i;
fib_i = new_fib_i;
fib_prev = new_fib_prev;
n = new_n;
}
return fib_i;
}
Notice the similarity of tail-recursive implementation to the non-recursive implementation (that uses loops). In general, any loop can be converted to a tail-recursive implementation. In fact, some programming languages do not support loops and only support recursion --- typically programmers write tail-recursive implementations of their programs in such languages.