CSC148H1/Unit 5
Unit 5 · Introduction to Computer Science

Recursion

The idea the course is built around, and the one most people stall on. A recursive function solves a problem by calling itself on a smaller version. The difficulty is never the code — it is learning to stop tracing and start trusting.

By the end you can
  • Write a base case and a recursive case that shrinks the input.
  • Apply the recursive leap of faith instead of tracing every level.
  • Diagnose infinite recursion.
  • Say when recursion beats a loop and when it does not.
Why it matters & where it comes from
Why it exists

Some data has no fixed depth. A folder holds folders that hold folders, with no bound known in advance. No fixed number of nested loops can walk it; recursion handles arbitrary depth with one function.

In the real world

Parsing HTML and JSON, rendering nested user interfaces, searching a file system, and every compiler ever written. Wherever the data nests without limit, the code recurses.

Story & open questions

Early Fortran forbade recursion because each function had one fixed block of memory for its variables. Supporting it required the call stack of Unit 3, which is why the two ideas arrived together.

The two parts

Rule Every recursive function needs both
Base case. A version small enough to answer outright, with no further call.
Recursive case. Reduce to a strictly smaller problem, call yourself, combine.

Missing base case: it never stops. Recursive case that does not shrink the input: also never stops.

Example Factorial, annotated
python
def factorial(n: int) -> int:
    """Return n!, the product of the integers from 1 to n.

    Preconditions: n >= 0

    >>> factorial(0)
    1
    >>> factorial(4)
    24
    """
    if n == 0:              # base case
        return 1
    return n * factorial(n - 1)   # recursive case, strictly smaller
The base case is $0$, not $1$. Choosing $1$ would still work for positive input but would recurse forever on $0$.
The argument decreases by one each call, so it reaches the base case in exactly $n$ steps.
Rule The recursive leap of faith

When writing the recursive case, assume the recursive call already works. Do not trace it. Ask only: given a correct answer for the smaller problem, how do I build the answer for this one?

For factorial: assume factorial(n - 1) returns $(n-1)!$ correctly. Then $n!$ is $n$ times that. Done — no tracing required.

Tracing is the trap

Following every level by hand works for depth three and collapses at depth ten. The leap of faith is not laziness; it is the only technique that scales. Trace once, early, to convince yourself the mechanism works — then stop doing it.

Diagnosing failures

Example Three ways it goes wrong
python
# 1. no base case
def bad1(n: int) -> int:
    return n * bad1(n - 1)

# 2. base case unreachable from some inputs
def bad2(n: int) -> int:
    if n == 0:
        return 1
    return n * bad2(n - 2)      # odd n skips straight past 0

# 3. input does not shrink
def bad3(n: int) -> int:
    if n == 0:
        return 1
    return n * bad3(n)          # same argument forever
All three raise RecursionError after about a thousand calls — Python's limit, which exists to turn an infinite loop into an error rather than a crash.
The second is the subtle one. It has a base case and it does shrink, but odd inputs step over the base case and run to negative infinity. Always check the base case is reachable from every valid input.

Recursion versus iteration

Example The same job, both ways
python
def total_loop(xs: list) -> int:
    """Return the sum of xs, iteratively."""
    running = 0
    for x in xs:
        running = running + x
    return running

def total_rec(xs: list) -> int:
    """Return the sum of xs, recursively.

    >>> total_rec([1, 2, 3])
    6
    """
    if xs == []:
        return 0
    return xs[0] + total_rec(xs[1:])
The loop is better here. The data is flat, so there is no nesting for recursion to exploit, and xs[1:] copies the rest of the list every call — making the recursive version $O(n^2)$ in time and memory.
Recursion earns its place when the data is recursive. A flat list is not.
Rule When to reach for recursion
Use it when the data nests to unknown depth: nested lists, trees, file systems, expressions.
Use it when the problem naturally splits into smaller instances of itself: divide-and-conquer sorting.
Prefer a loop for flat sequences and simple counting.
Example Where it clearly wins
python
def depth(obj) -> int:
    """Return the nesting depth of obj, a nested list.

    >>> depth(5)
    0
    >>> depth([1, [2, [3]]])
    3
    """
    if not isinstance(obj, list):
        return 0
    if obj == []:
        return 1
    return 1 + max(depth(item) for item in obj)

No loop structure can do this, because the nesting depth is not known when the code is written. Unit 6 develops this pattern properly.

Common pitfalls

• No base case, or one unreachable from some inputs.
• A recursive call that does not strictly shrink the input.
• Tracing every level instead of trusting the call.
• Forgetting to return the recursive call's result.
• Slicing a list on every call and quietly making the function quadratic.

Practice

Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.

Practice 1 · Two parts

What must every recursive function have?

Answer
A base case, and a recursive case that reduces to a strictly smaller input.
Practice 2 · Diagnose

Why does bad2 above fail on odd input?

Answer
Subtracting two from an odd number never lands on zero, so the base case is skipped and the recursion runs past it.
Practice 3 · Countdown

Write a function printing $n$ down to $1$.

Solution
python
def countdown(n: int) -> None:
    """Print the integers from n down to 1, one per line.

    Preconditions: n >= 0
    """
    if n == 0:
        return
    print(n)
    countdown(n - 1)
Moving the print after the call would count up instead — the same code, opposite output.
Practice 4 · Fibonacci

Write it recursively and say why it is slow.

Solution
python
def fib(n: int) -> int:
    """Return the nth Fibonacci number, with fib(0) = 0.

    Preconditions: n >= 0

    >>> fib(6)
    8
    """
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)
It recomputes the same values enormously often — the call tree has about $2^n$ nodes. Correct but exponential. Storing results as they are computed fixes it.
Practice 5 · Leap of faith

Assuming rev(s[1:]) reverses correctly, write rev.

Solution
python
def rev(s: str) -> str:
    """Return s reversed.

    >>> rev('abc')
    'cba'
    """
    if s == '':
        return ''
    return rev(s[1:]) + s[0]
The reversed tail, with the first character stuck on the end. No tracing needed.
Practice 6 · Palindrome

Test a palindrome recursively.

Solution
python
def is_pal(s: str) -> bool:
    """Return whether s reads the same forwards and backwards.

    >>> is_pal('racecar')
    True
    """
    if len(s) < 2:
        return True
    return s[0] == s[-1] and is_pal(s[1:-1])
Two base cases collapse into one: empty and single-character strings are both palindromes.
Practice 7 · Which to use

Summing a flat list — loop or recursion?

Answer
A loop. The data is flat, so recursion adds call overhead and slicing cost for no structural benefit.
Practice 8 · Missing return

What does this return, and why?

python
def f(n: int) -> int:
    if n == 0:
        return 1
    n * f(n - 1)
Answer
None for any $n > 0$. The recursive case computes the value and discards it — the return is missing. A very common slip.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:

  • Structure. Base case and recursive case, and what each must guarantee.
  • The leap of faith. Trusting the recursive call rather than tracing it.
  • Failure modes. Missing, unreachable and non-shrinking cases.
  • Judgment. When recursion beats iteration and when it costs more than it gives.
Textbook/source alignment

The calendar names "recursion" explicitly. It is the concept the course is organised around and the most heavily assessed; check Quercus for the assigned exercises.

Core idea map

Reduce to a smaller instance, trust the result, combine. Unit 6 applies this to nested data and Units 7 and 8 to trees, where recursion is not a choice but the only reasonable approach.

Study routine. Write the base case first, every time. Then write the recursive case assuming the call works. If you find yourself tracing, stop and reread the docstring — the contract is what you are allowed to assume.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · Requirements

What two things must a recursive function have?

Guide
A base case, and a recursive case on a strictly smaller input that reaches it.
Extension 2 · The technique

What is the recursive leap of faith?

Guide
Assuming the recursive call is correct while writing the case that uses it, rather than tracing it.
Extension 3 · Infinite recursion

Name two causes.

Guide
No base case, or an input that never reaches it — either because it does not shrink or because it steps over.
Extension 4 · Choosing

When does recursion beat a loop?

Guide
When the data nests to unknown depth, or the problem splits into smaller copies of itself.