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

Recursion

Functions that call themselves, the recursive leap of faith, the order in which recursive calls print, and mutual recursion.

CS61A
Recursion · Composing Programs 1.7
Swift book
Functions

A function is recursive when its body calls the function itself. That sounds circular, but it works as long as each call works on a smaller problem, and the smallest problems are answered directly.

The anatomy of a recursive function

Here is the sum of digits from Lab 2, written recursively:

func sumDigits(_ n: Int) -> Int {
    if n < 10 {
        return n                        // base case
    }
    return n % 10 + sumDigits(n / 10)   // recursive case
}

print(sumDigits(2026))   // 10

Every recursive function has the same parts:

  1. Base cases: inputs small enough to answer without recursion. A one-digit number is its own digit sum.
  2. Recursive cases: break the problem into a smaller version of itself, call the function on it, and use the result. The digit sum of 2026 is 6 plus the digit sum of 202.

The recursive leap of faith

How do you know sumDigits(202) is right while you are still writing sumDigits? CS61A’s answer: trust it. Check the base case, check that the recursive call is on a smaller input, and check that you use its result correctly assuming it is right. If all three hold, induction does the rest.

The same reasoning gives the recursive factorial. n! = n × (n − 1)!, and 0! = 1:

func factorial(_ n: Int) -> Int {
    n == 0 ? 1 : n * factorial(n - 1)
}
print(factorial(5))   // 120

What would Swift print?

func total(_ n: Int) -> Int {
    n == 0 ? 0 : n + total(n - 1)
}
print(total(4))

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

Show answer and explanation

Answer10

total(4) is 4 + total(3), which is 4 + 3 + total(2), and so on down to total(0), which is 0. The result is 4 + 3 + 2 + 1 + 0 = 10.

Without a base case the calls never stop. Each call takes a little memory on the call stack, and eventually it runs out.

What would Swift print?

func countdown(_ n: Int) -> Int {
    countdown(n - 1)
}
print(countdown(3))

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

Show answer and explanation

AnswerError

There is no base case, so countdown calls itself forever. Swift warns that “function call causes an infinite recursion”, and at run time the program crashes with a stack overflow.

Order of recursive calls

When a recursive call happens in the middle of a function body, the rest of the body waits until it returns. CS61A’s cascade shows this clearly:

func cascade(_ n: Int) {
    print(n)
    if n >= 10 {
        cascade(n / 10)
        print(n)
    }
}
cascade(123)

What would Swift print?

func cascade(_ n: Int) {
    print(n)
    if n >= 10 {
        cascade(n / 10)
        print(n)
    }
}
cascade(123)

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

Show answer and explanation

Answer123\n12\n1\n12\n123

cascade(123) prints 123, then calls cascade(12), which prints 12 and calls cascade(1). That call prints 1 and returns without recursing. Then cascade(12) finishes by printing 12 again, and finally cascade(123) prints 123 again.

Each call has its own frame with its own n. While cascade(1) runs, the frames for 12 and 123 are still waiting, each holding its own value.

Mutual recursion

Two functions can call each other. This pair decides whether a non-negative number is even or odd, using only subtraction:

func isEven(_ n: Int) -> Bool {
    n == 0 ? true : isOdd(n - 1)
}
func isOdd(_ n: Int) -> Bool {
    n == 0 ? false : isEven(n - 1)
}
print(isEven(10), isOdd(7), isEven(3))   // true true false

Top-level functions in a Swift file can refer to each other in any order, so isEven can call isOdd before isOdd is written.

Put the lines in order

Put the lines in order: a recursive digit sum that prints 10.

  1. if n < 10 {
  2. print(sumDigits(2026))
  3. return n % 10 + sumDigits(n / 10)
  4. func sumDigits(_ n: Int) -> Int {
  5. }
  6. }
  7. return n

Show the correct program
func sumDigits(_ n: Int) -> Int {
    if n < 10 {
        return n
    }
    return n % 10 + sumDigits(n / 10)
}
print(sumDigits(2026))

Lab 6: Recursion

Solve these without loops.

Q1: Count the eights

Return the number of times the digit 8 appears in a non-negative integer.

func numEights(_ n: Int) -> Int {
    // your code here
}
Show solution
func numEights(_ n: Int) -> Int {
    if n == 0 {
        return 0
    }
    let last = n % 10 == 8 ? 1 : 0
    return last + numEights(n / 10)
}

assert(numEights(3) == 0)
assert(numEights(8) == 1)
assert(numEights(88888888) == 8)
assert(numEights(2638) == 1)
assert(numEights(86380) == 2)
assert(numEights(12345) == 0)

The last digit contributes 0 or 1, and the rest of the number is a smaller instance of the same problem.

Q2: Hailstone

Starting from a positive integer n: if it is even, divide it by 2; otherwise multiply by 3 and add 1. Repeat until you reach 1. Write hailstone(n), which prints every number in the sequence and returns how many numbers were printed.

func hailstone(_ n: Int) -> Int {
    // your code here
}
Show solution
func hailstone(_ n: Int) -> Int {
    print(n)
    if n == 1 {
        return 1
    }
    let next = n % 2 == 0 ? n / 2 : 3 * n + 1
    return 1 + hailstone(next)
}

let steps = hailstone(10)   // prints 10 5 16 8 4 2 1, one per line
assert(steps == 7)
assert(hailstone(1) == 1)

Nobody has proved that the sequence reaches 1 for every starting number: this is the Collatz conjecture. It has been checked for every starting number anyone has tried.

Q3: Is it prime?

Write isPrime(n) using a helper function defined inside it, which checks candidate divisors starting from 2.

func isPrime(_ n: Int) -> Bool {
    // your code here
}
Show solution
func isPrime(_ n: Int) -> Bool {
    func noDivisorFrom(_ k: Int) -> Bool {
        if k * k > n {
            return true
        }
        if n % k == 0 {
            return false
        }
        return noDivisorFrom(k + 1)
    }
    return n > 1 && noDivisorFrom(2)
}

assert(!isPrime(1))
assert(isPrime(2))
assert(isPrime(13))
assert(!isPrime(91))   // 7 * 13
assert((1...20).filter(isPrime) == [2, 3, 5, 7, 11, 13, 17, 19])

The helper needs an extra parameter, the next candidate k, that the original problem does not have. Defining it inside isPrime lets it see n without passing it along. Stopping once k² > n works because any divisor larger than √n pairs with one smaller than it.

What’s next

Next lesson: tree recursion, where a function makes more than one recursive call.