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 42How 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 N
th 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.