Recursion

We will learn a powerful algorithmic technique called recursion We will spend several lectures on recursion -- don't worry if it does not make sense today. Your goal should be to do as many examples as possible.

recursive programming: Writing functions that call themselves to solve problems that are recursive in nature.

Recursive blue shirt

How can we solve this recursively?

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 
    } 
} 

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

Recursive Big-O

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.
How are the results of subproblems combined? Using "number_of_times_loop_runs" and "the_sum_of"
Where are the recursive calls? calls to findBigO(loop_inside) and findBigO(codeBlock)
What is the base case? single statement
Are we following the 3 rules of recursion? e.g., are we moving towards the base case in the recursive call?

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 hello
Expected output:
baz hello
Hello bar
Hello foo
Hello world
Is 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.

Example:
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).
Base cases? n=1,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?
If you compute fib(20), how many times is fib(16) called?
For each call to fib(16), how many times is fib(12) called

For each call to fib(12), how many times is fib(8) called

What could we have done better? e.g., the first time fib(16) gets called, we save the value. For each subsequent evaluation of fib(16), we just return the cached value.

Memoization

memoization: caching results of previous expensive function calls for speed so tht they do not need to be re-computed.

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.

Tail recursion

Tail recursion: when a recursive call is made as the final action of a recursive function.

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.