Lecture 18 Recursion

Joseph Haugh

University of New Mexico

Free Recall

  • Get out a sheet of paper or open a text editor
  • For 2 minutes write down whatever comes to mind about the last class
    • This could be topics you learned
    • Questions you had
    • Connections you made

Today’s Topics

  • Recursion: a function that calls itself
  • Base cases and recursive cases: the two parts of every recursive function
  • Fibonacci, our first recursive function
  • Summing an IntList recursively
  • Appending two IntLists recursively

A Familiar Pattern

  • You have already seen loops solve repetitive problems:
int sum1ToN(int n) {
    int result = 0;
    for (int i = 1; i <= n; i++) {
        result += i;
    }
    return result;
}
  • Today we will learn a completely different way to solve repetitive problems
  • Instead of using a loop, a function can call itself
  • This is called recursion

The Fibonacci Sequence

  • The Fibonacci sequence is a famous sequence of numbers:

    0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
  • Each number is the sum of the two numbers before it

    • 0 + 1 = 1
    • 1 + 1 = 2
    • 1 + 2 = 3
    • 2 + 3 = 5
  • But what are the first two numbers? They are just given: 0 and 1

  • Our goal: write a function fib(n) that returns the n-th Fibonacci number

Fibonacci: The Definition

  • We can define fib precisely:

    fib(0) = 0
    fib(1) = 1
    fib(n) = fib(n-1) + fib(n-2)    for n >= 2
  • The first two lines are the starting values: we just know fib(0) and fib(1)

  • The third line says: to find fib(n), look at two smaller problems

  • The definition refers to itself, which is exactly what recursion means

  • To compute fib(5), we need fib(4) and fib(3); to compute fib(4) we need fib(3) and fib(2); and so on down to the starting values

From Definition to Code

  • The definition translates almost word for word into Java:
Definition Java
fib(0) = 0 if (n == 0) return 0;
fib(1) = 1 if (n == 1) return 1;
fib(n) = fib(n-1) + fib(n-2) return fib(n-1) + fib(n-2);

Fibonacci in Java

int fib(int n) {
}

Fibonacci in Java

int fib(int n) {
    if (n == 0) return 0; // base case
    if (n == 1) return 1; // base case
}
  • These are called base cases: inputs small enough that we know the answer directly
  • They are where the recursion stops

Fibonacci in Java

int fib(int n) {
    if (n == 0) return 0; // base case
    if (n == 1) return 1; // base case
    return fib(n - 1) + fib(n - 2); // recursive case
}
  • The last line is the recursive case: fib calls itself with a smaller input
  • We trust that fib(n-1) and fib(n-2) will give the right answers
  • They will, because eventually every call reaches a base case

Calling Fibonacci

void main() {
    IO.println(fib(0)); // 0
    IO.println(fib(1)); // 1
    IO.println(fib(5)); // 5
    IO.println(fib(7)); // 13
}

Tracing fib(4)

fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2)
fib(4)
  = fib(3) + fib(2)
  = (fib(2) + fib(1)) + fib(2)
  = ((fib(1) + fib(0)) + fib(1)) + fib(2)
  = ((1 + 0) + 1) + fib(2)
  = 2 + (fib(1) + fib(0))
  = 2 + (1 + 0)
  = 3
  • Each call breaks the problem into two smaller calls
  • The calls stop when they hit a base case (n == 0 or n == 1)
  • The answers bubble back up to the original call

Anatomy of a Recursive Function

  • Every correct recursive function has exactly two parts:
    1. Base case(s)
      • The input is simple enough to answer directly, no recursion needed
      • This is what stops the recursion
    2. Recursive case
      • Break the problem into a smaller version of the same problem
      • Call the function on that smaller version and use that result

What Happens Without a Base Case?

int fib(int n) {
    return fib(n - 1) + fib(n - 2); // no base cases!
}
  • fib(4) calls fib(3), which calls fib(2), which calls fib(1), which calls fib(0), which calls fib(-1), …
  • The calls never stop
  • Java tracks every active function call in something called the call stack
  • When the stack grows too deep, you get a runtime error: StackOverflowError
  • The fix: always include a base case that the input will eventually reach

IntList Review

  • Last lecture we built IntList: a resizable list of int values
Method What it does
add(x) Appends x to the end
get(i) Returns the element at index i
pop() Removes and returns the last element
size() Returns the number of elements
isEmpty() Returns true if the list has no elements
  • To work naturally with recursion, we need two more operations: head() and tail()

head() and tail()

  • head() returns the first element of the list
    • For [3, 7, 2, 5], head() returns 3
  • tail() returns a new list with everything except the first element
    • For [3, 7, 2, 5], tail() returns [7, 2, 5]
  • Together they let us peel a list apart one element at a time, exactly what recursion needs
  • Both throw an exception if the list is empty

Adding head() to IntList

public class IntList {
    // fields, constructor, add, get, pop, size, isEmpty ...

    public int head() {
        if (isEmpty()) {
            throw new IllegalStateException("List is empty");
        }
        return get(0);
    }
}
  • head() is just get(0) with an empty-list check
  • The first element is always at index 0

Adding tail() to IntList

public class IntList {
    // fields, constructor, add, get, pop, size, isEmpty, head ...

    public IntList tail() {
        if (isEmpty()) {
            throw new IllegalStateException("List is empty");
        }
        IntList result = new IntList();
        for (int i = 1; i < size(); i++) {
            result.add(get(i));
        }
        return result;
    }
}
  • tail() builds a fresh IntList containing every element after index 0
  • The original list is not modified

Sum of an IntList

  • Now let’s write a function that sums all the elements:

    IntList scores = new IntList();
    scores.add(3);
    scores.add(7);
    scores.add(2);
    scores.add(5);
    
    IO.println(sum(scores)); // 17
  • How do we think about this recursively?

Sum: Thinking Recursively

  • Imagine the list [3, 7, 2, 5]
  • The sum is: 3 + 7 + 2 + 5
  • Another way to see it: 3 + (sum of [7, 2, 5])
  • And (sum of [7, 2, 5]) = 7 + (sum of [2, 5])
  • And so on…
  • Base case: an empty list has sum 0, there is nothing to add
  • Recursive case: sum(list) = list.head() + sum(list.tail())

Sum in Java

int sum(IntList list) {
}

Sum in Java

int sum(IntList list) {
    if (list.isEmpty()) {
        return 0; // base case: nothing left to add
    }
}

Sum in Java

int sum(IntList list) {
    if (list.isEmpty()) {
        return 0; // base case: nothing left to add
    }
    return list.head() + sum(list.tail()); // recursive case
}
  • list.head() is the first element
  • list.tail() is the rest of the list, one element shorter
  • Each recursive call works on a smaller list, so eventually it is empty

Tracing sum([3, 7, 2, 5])

int sum(IntList list) {
    if (list.isEmpty()) {
        return 0;
    }
    return list.head() + sum(list.tail());
}
sum([3, 7, 2, 5])
  = 3 + sum([7, 2, 5])
  = 3 + 7 + sum([2, 5])
  = 3 + 7 + 2 + sum([5])
  = 3 + 7 + 2 + 5 + sum([])
  = 3 + 7 + 2 + 5 + 0  // base case
  = 17

Calling sum

void main() {
    IntList scores = new IntList();
    scores.add(3);
    scores.add(7);
    scores.add(2);
    scores.add(5);

    IO.println(sum(scores)); // 17
}

Appending Two IntLists

  • Now let’s write a function that appends two lists:
IntList a = new IntList(); // [1, 2, 3]
a.add(1); a.add(2); a.add(3);

IntList b = new IntList(); // [4, 5]
b.add(4); b.add(5);

IntList c = append(a, b);
// c is [1, 2, 3, 4, 5]

Append: The Plan

  • To build the result, we need to copy all elements of a, then all elements of b, into a new list
  • Let’s write a helper: copyFrom(src, i, dest)
    • Copies elements of src starting at index i into dest
  • Base case: i == src.size(), nothing left to copy, just return
  • Recursive case: add src.get(i) to dest, then copy the rest starting at i + 1

copyFrom

void copyFrom(IntList src, int i, IntList dest) {
}

copyFrom

void copyFrom(IntList src, int i, IntList dest) {
    if (i == src.size()) {
        return; // base case: nothing left to copy
    }
}

copyFrom

void copyFrom(IntList src, int i, IntList dest) {
    if (i == src.size()) {
        return; // base case: nothing left to copy
    }
    dest.add(src.get(i));       // copy the current element
    copyFrom(src, i + 1, dest); // recursive case: copy the rest
}
  • Each call copies one element and then recurses with i + 1
  • When i reaches src.size(), the copying is done

append

IntList append(IntList a, IntList b) {
    IntList result = new IntList();
    copyFrom(a, 0, result); // copy all of a
    copyFrom(b, 0, result); // then all of b
    return result;
}
  • append creates a fresh IntList to hold the combined result
  • It calls copyFrom twice: once for a, once for b
  • The recursive work is inside copyFrom; append just coordinates

Tracing append([1, 2, 3], [4, 5])

void copyFrom(IntList src, int i,
              IntList dest) {
    if (i == src.size()) {
        return;
    }
    dest.add(src.get(i));
    copyFrom(src, i + 1, dest);
}
copyFrom(a, 0, result)
 -> dest.add(1); copyFrom(a, 1, result)
   -> dest.add(2); copyFrom(a, 2, result)
     -> dest.add(3); copyFrom(a, 3, result)
      -> return  // base case
// result is now [1, 2, 3]

copyFrom(b, 0, result)
 -> dest.add(4); copyFrom(b, 1, result)
   -> dest.add(5); copyFrom(b, 2, result)
     -> return  // base case
// result is now [1, 2, 3, 4, 5]

Summary

Recursion

  • A function that calls itself
  • Must have a base case that stops the recursion
  • The recursive case must make progress toward the base case

Common mistake

  • No base case (or unreachable base case) causes StackOverflowError

Three Examples

Function Base case Recursive case
fib(n) n == 0 or 1 fib(n-1) + fib(n-2)
sum(list) list.isEmpty() head() + sum(tail())
copyFrom(src, i, dest) i == size add get(i), recurse i+1