Tutorial outline

Unit 1

  1. 01Getting started
  2. 02Names and functions
  3. 03Control
  4. 04Higher-order functions
  5. 05Environments and closures
  6. 06Functional abstraction
  7. 07Recursion
  8. 08Tree recursion

Unit 2

  1. 09Sequences
  2. 10Dictionaries, sets, and tuples
  3. 11Optionals
  4. 12Structures and enumerations
  5. 13Linked lists and trees
  6. 14Mutation and value semantics
  7. 15Classes and inheritance
  8. 16Protocols and generics
  9. 17Iterators and lazy sequences
  10. 18Efficiency and memoization

Unit 3

  1. 19Algebraic data types and pattern matching
  2. 20Writing an interpreter
  3. 21Error handling
  4. 22Concurrency with async and await
  5. 23Testing
  6. 24Building a Swift package

Tree recursion

Functions that make more than one recursive call, the tree of calls they create, and the classic counting problems: partitions, stairs, coins, and paths.

CS61A
Tree Recursion · Composing Programs 1.7
Swift book
Functions

The recursive functions in the last lesson made one recursive call each. A function that makes two or more calls is tree recursive: drawn out, its calls branch like a tree.

Fibonacci, again

The Fibonacci numbers are defined in terms of the two before them, so the most direct recursive definition makes two calls:

func fib(_ n: Int) -> Int {
    if n < 2 {
        return n
    }
    return fib(n - 2) + fib(n - 1)
}
print(fib(10))   // 55

Here is the tree of calls for fib(4):

                    fib(4)
            ┌─────────┴──────────┐
         fib(2)                fib(3)
       ┌───┴───┐          ┌──────┴──────┐
    fib(0)  fib(1)     fib(1)        fib(2)
                                   ┌───┴───┐
                                fib(0)  fib(1)

fib(2) is computed twice, and the repetition gets much worse as n grows. We can count the calls with a global variable:

What would Swift print?

var calls = 0
func fib(_ n: Int) -> Int {
    calls += 1
    return n < 2 ? n : fib(n - 2) + fib(n - 1)
}
_ = fib(5)
print(calls)

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer15

fib(5) calls fib(3) and fib(4), and so on. Counting every node in the tree gives 15 calls to compute a single number. _ = fib(5) discards the result, since only the count matters here.

For fib(20) the count is 21,891. The iterative version from Lesson 3 needs only 19 trips through its loop. Tree recursion is often the clearest way to describe a computation, and later, in the lesson on efficiency, we will see how to keep that clarity without the repeated work.

Counting partitions

Tree recursion shines when a problem splits into cases. CS61A’s favorite: in how many ways can a positive integer n be written as a sum of positive parts, each at most m, in increasing order? For n = 6, m = 4 there are 9:

2 + 4    1 + 1 + 4    3 + 3    1 + 2 + 3    1 + 1 + 1 + 3
2 + 2 + 2    1 + 1 + 2 + 2    1 + 1 + 1 + 1 + 2    1 + 1 + 1 + 1 + 1 + 1

The key insight is to split every partition into two groups: those that use at least one m, and those that use no m.

  • A partition that uses m is m plus a partition of n - m with parts up to m.
  • A partition that does not use m is a partition of n with parts up to m - 1.

Both groups are smaller instances of the same problem.

func countPartitions(_ n: Int, _ m: Int) -> Int {
    if n == 0 {
        return 1
    } else if n < 0 || m == 0 {
        return 0
    }
    return countPartitions(n - m, m) + countPartitions(n, m - 1)
}
print(countPartitions(6, 4))   // 9

The base cases need care. n == 0 means we have exactly used up the number, which is one successful partition. n < 0 means we overshot, and m == 0 means there are no parts left to use; both count zero.

What would Swift print?

func countPartitions(_ n: Int, _ m: Int) -> Int {
    if n == 0 {
        return 1
    } else if n < 0 || m == 0 {
        return 0
    }
    return countPartitions(n - m, m) + countPartitions(n, m - 1)
}
print(countPartitions(5, 3))

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer5

The partitions of 5 with parts at most 3 are 1+1+3, 2+3, 1+2+2, 1+1+1+2, and 1+1+1+1+1: five of them.

Put the lines in order

Put the lines in order so the program prints 9.

  1. }
  2. return 0
  3. }
  4. func countPartitions(_ n: Int, _ m: Int) -> Int {
  5. print(countPartitions(6, 4))
  6. return countPartitions(n - m, m) + countPartitions(n, m - 1)
  7. if n == 0 {
  8. } else if n < 0 || m == 0 {
  9. return 1

Show the correct program
func countPartitions(_ n: Int, _ m: Int) -> Int {
    if n == 0 {
        return 1
    } else if n < 0 || m == 0 {
        return 0
    }
    return countPartitions(n - m, m) + countPartitions(n, m - 1)
}
print(countPartitions(6, 4))

Printing a whole tree

A tree-recursive function can do work at every node, not just combine answers. The Towers of Hanoi moves a stack of n disks from one peg to another, one disk at a time, never putting a larger disk on a smaller one. To move n disks, move n - 1 out of the way, move the largest, then move the n - 1 back on top:

func hanoi(_ n: Int, from start: Int, to end: Int) {
    if n == 0 {
        return
    }
    let spare = 6 - start - end   // the third peg, since 1 + 2 + 3 == 6
    hanoi(n - 1, from: start, to: spare)
    print("Move disk \(n) from \(start) to \(end)")
    hanoi(n - 1, from: spare, to: end)
}
hanoi(3, from: 1, to: 3)

What would Swift print?

func hanoi(_ n: Int, from start: Int, to end: Int) {
    if n == 0 {
        return
    }
    let spare = 6 - start - end
    hanoi(n - 1, from: start, to: spare)
    print(n, start, end)
    hanoi(n - 1, from: spare, to: end)
}
hanoi(2, from: 1, to: 3)

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer1 1 2\n2 1 3\n1 2 3

To move two disks from peg 1 to peg 3: move disk 1 to the spare peg 2, move disk 2 to peg 3, then move disk 1 from peg 2 onto it. Each line prints the disk, where it came from, and where it went.

Lab 7: Tree recursion

Q1: Climbing stairs

You can climb a staircase one or two steps at a time. Write countStairWays(n), the number of different ways to climb n steps.

func countStairWays(_ n: Int) -> Int {
    // your code here
}
Show solution
func countStairWays(_ n: Int) -> Int {
    if n <= 1 {
        return 1
    }
    return countStairWays(n - 1) + countStairWays(n - 2)
}

assert(countStairWays(1) == 1)
assert(countStairWays(2) == 2)   // 1 + 1, or 2
assert(countStairWays(4) == 5)
assert(countStairWays(10) == 89)

The first step is one stair or two, leaving n - 1 or n - 2. The structure is exactly Fibonacci’s, shifted by one.

Q2: Making change

Count the ways to make total cents from quarters (25), dimes (10), nickels (5), and pennies (1). nextSmallerCoin is given; it returns 0 after pennies. Order does not matter: 5 + 10 and 10 + 5 are the same way.

func nextSmallerCoin(_ coin: Int) -> Int {
    switch coin {
    case 25: return 10
    case 10: return 5
    case 5: return 1
    default: return 0
    }
}

func countCoins(_ total: Int) -> Int {
    // your code here, probably with a helper
}
Show solution
func nextSmallerCoin(_ coin: Int) -> Int {
    switch coin {
    case 25: return 10
    case 10: return 5
    case 5: return 1
    default: return 0
    }
}

func countCoins(_ total: Int) -> Int {
    func count(_ total: Int, _ largest: Int) -> Int {
        if total == 0 {
            return 1
        } else if total < 0 || largest == 0 {
            return 0
        }
        let usingLargest = count(total - largest, largest)
        let withoutLargest = count(total, nextSmallerCoin(largest))
        return usingLargest + withoutLargest
    }
    return count(total, 25)
}

assert(countCoins(15) == 6)
assert(countCoins(10) == 4)
assert(countCoins(20) == 9)
assert(countCoins(100) == 242)

This is countPartitions again, with coins in place of parts: either use the largest coin allowed, or never use it again. Allowing coins only in decreasing order is what makes 5 + 10 and 10 + 5 count once.

Q3: Grid paths

An insect starts in the bottom-left corner of an m by n grid and wants to reach the top-right corner, moving only up or right. Write paths(m, n), the number of different routes.

func paths(_ m: Int, _ n: Int) -> Int {
    // your code here
}
Show solution
func paths(_ m: Int, _ n: Int) -> Int {
    if m == 1 || n == 1 {
        return 1
    }
    return paths(m - 1, n) + paths(m, n - 1)
}

assert(paths(2, 2) == 2)
assert(paths(5, 7) == 210)
assert(paths(117, 1) == 1)
assert(paths(1, 157) == 1)

With only one row or one column there is a single straight route. Otherwise the first move is up or right, and each leaves a grid one smaller in one direction.

What’s next

That ends Unit 1. Unit 2 turns from functions to data, starting with sequences: arrays, ranges, and strings.