Lecture 19 More 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

  • Two new IntList methods: addFirst() and middle()
  • Recursive functions that return booleans
  • Recursive functions that return new lists
  • Guided practice: building intuition for today’s quiz

New IntList Methods

Last lecture we introduced head() and tail(). Today we add two more:

Method What it does
addFirst(x) Returns a new list with x at the front
middle() Returns a new list with the first and last elements removed
  • The original list is never modified; both return brand new lists

addFirst()

  • addFirst(x) prepends x and returns a new list

    IntList a = new IntList(); // [2, 3, 4]
    a.add(2); a.add(3); a.add(4);
    
    IntList b = a.addFirst(1); // [1, 2, 3, 4]
    // a is still [2, 3, 4]
  • Notice: add(x) appends to this list, addFirst(x) returns a new list

  • This makes addFirst natural for recursion: you build results on the way back up

addFirst() Implementation

public IntList addFirst(int x) {
    IntList result = new IntList();
    result.add(x);
    for (int i = 0; i < size(); i++) {
        result.add(get(i));
    }
    return result;
}
  • Put x in first, then copy every element of the original list after it
  • The original list is untouched

middle()

  • middle() removes both the first and last element

    IntList a = new IntList(); // [1, 2, 3, 4, 5]
    a.add(1); a.add(2); a.add(3); a.add(4); a.add(5);
    
    IntList m = a.middle(); // [2, 3, 4]
    // a is still [1, 2, 3, 4, 5]
  • A list of size 0 or 1 has no middle, so middle() throws an exception

  • A list of size 2 has an empty middle: [1, 2].middle() returns []

middle() Implementation

public IntList middle() {
    if (size() < 2) {
        throw new IllegalStateException("List must have at least 2 elements");
    }
    IntList result = new IntList();
    for (int i = 1; i < size() - 1; i++) {
        result.add(get(i));
    }
    return result;
}
  • Copies everything between index 1 and index size() - 2 (inclusive)
  • Skips index 0 (head) and index size() - 1 (last)

Boolean Recursion

  • A recursive function can return boolean just as easily as int
  • The structure is exactly the same:
    • Base case: an input small enough to answer directly
    • Recursive case: use the answer for a smaller input to get the full answer
  • Key insight: true and false can be combined with && and ||
    • allTrue(list) = list.head() && allTrue(list.tail())
    • anyTrue(list) = list.head() || anyTrue(list.tail())

allPositive: Thinking Recursively

  • Goal: return true if every element is greater than 0
  • Is [3, 7, 2, 5] all positive?
    • Is 3 positive? Yes.
    • Is the rest [7, 2, 5] all positive? If so, then yes.
  • Base case: an empty list has no negative elements, so return true
  • Recursive case: list.head() > 0 and allPositive(list.tail())

allPositive in Java

boolean allPositive(IntList list) {
}

allPositive in Java

boolean allPositive(IntList list) {
    if (list.isEmpty()) {
        return true; // base case: no elements to violate the rule
    }
}

allPositive in Java

boolean allPositive(IntList list) {
    if (list.isEmpty()) {
        return true; // base case: no elements to violate the rule
    }
    return list.head() > 0 && allPositive(list.tail()); // recursive case
}
  • If any element is not positive, && short-circuits and returns false
  • If all elements are positive, every recursive call returns true
  • The empty list base case acts as the “default true” at the bottom

Tracing allPositive([3, -1, 5])

boolean allPositive(IntList list) {
    if (list.isEmpty()) return true;
    return list.head() > 0
           && allPositive(list.tail());
}
allPositive([3, -1, 5])
  = (3 > 0) && allPositive([-1, 5])
  = true    && allPositive([-1, 5])
  = true    && ((-1 > 0) && allPositive([5]))
  = true    && (false && ...)
  = true    && false
  = false
  • Once we hit -1, the && short-circuits
  • We never even look at 5

List-Building Recursion with addFirst

  • Sometimes the result of a recursive function is a new list
  • The pattern: build the result on the way back up using addFirst
  • Think about doubleAll(list): return a new list with every element doubled
doubleAll([3, 7, 2])
  = addFirst(6, doubleAll([7, 2]))
  = addFirst(6, addFirst(14, doubleAll([2])))
  = addFirst(6, addFirst(14, addFirst(4, doubleAll([]))))
  = addFirst(6, addFirst(14, addFirst(4, [])))  // base case
  = [6, 14, 4]

doubleAll in Java

IntList doubleAll(IntList list) {
}

doubleAll in Java

IntList doubleAll(IntList list) {
    if (list.isEmpty()) {
        return new IntList(); // base case: empty in, empty out
    }
}

doubleAll in Java

IntList doubleAll(IntList list) {
    if (list.isEmpty()) {
        return new IntList(); // base case: empty in, empty out
    }
    IntList rest = doubleAll(list.tail()); // recursive case: handle the tail
    return rest.addFirst(list.head() * 2); // prepend the doubled head
}
  • First, recursively double everything in the tail
  • Then, prepend the doubled head onto that result
  • The final list is assembled as the calls return

The Keep-or-Skip Pattern

  • A powerful pattern for list-building: look at each element and decide to keep it or skip it
keepOrSkip(list):
  base case:  list is empty -> return empty list
  recursive case:
    result = keepOrSkip(list.tail())       // handle the rest
    if we KEEP list.head():
        return result.addFirst(list.head()) // put it at the front
    else:
        return result                       // skip it
  • This preserves the original order
  • The decision of what to keep or skip changes from problem to problem

Guided Practice: isPalindrome

  • isPalindrome(list) returns true if the list reads the same forwards and backwards
  • Think about [3, 1, 4, 1, 3]:
    • The first and last elements are both 3, so they match
    • Is the middle [1, 4, 1] also a palindrome?
  • Two base cases: size == ?1 and size == ?2 are always palindromes
  • Recursive case:
    • Check that list.head() equals list.get(list.size() - 1) (the last element)
    • And that list.middle() is a palindrome
    • middle() peels off both ends at once, making the problem smaller

Guided Practice: removeAll

  • removeAll(list, target) returns a new list with all occurrences of target removed
  • Think about removeAll([1, 2, 3, 2, 1], 2) = [1, 3, 1]
    • For each element: is it the target? Skip it. Otherwise? Keep it.
    • This is the keep-or-skip pattern!
  • What is the base case?
  • In the recursive case, you have list.head() and removeAll(list.tail(), target)
    • If list.head() == target, what do you return?
    • If list.head() != target, what do you return? (Hint: addFirst)

Guided Practice: isSorted

  • isSorted(list) returns true if every element is <= the one after it
  • Think about [1, 3, 2, 4]. Is it sorted?
    • Is 1 <= 3? Yes.
    • Is the rest [3, 2, 4] sorted? No, because 3 > 2.
  • What are the base cases? A list of size == ?1 or size == ?2 is always sorted.
  • What does the recursive case need to check?
    • Something about the head and the next element…
    • And something about the tail…
  • Hint: list.tail().head() gives you the second element of list

Summary

New IntList methods

Method Effect
addFirst(x) New list with x at front
middle() New list without first and last

Boolean recursion

  • Base case: smallest input, answer directly
  • Recursive case: combine with && or ||

List-building recursion

  • Base case: empty list -> return new IntList()
  • Recursive case: recurse on tail(), then addFirst the head

Keep-or-skip pattern

  • Recurse on the tail first
  • Then decide to addFirst or just return the result