Chapter 4
20 min read
Section 18 of 261

Anatomy of a Recursive Function

Recursion Mastery

Learning Objectives

By the end of this section, you will be able to:

  1. Identify the five essential components of every recursive function
  2. Understand parameter patterns and how data flows through recursive calls
  3. Master return value conventions for building up results
  4. Distinguish work placement—when to process before vs. after recursion
  5. Recognize different recursive patterns: linear, binary, multiple, and mutual recursion
  6. Write correct recursive code in multiple programming languages
Why This Matters: Understanding the anatomy of recursive functions is like understanding sentence structure in language—once you see the pattern, you can construct correct recursive solutions fluently. Every recursive algorithm, from simple factorial to complex tree traversals, follows the same structural principles.

The Structure Behind the Magic

In the previous section, we developed the recursive mindset—the ability to think about problems in terms of smaller instances of themselves. Now we need to translate that mindset into actual code. How do we structure a recursive function so that it works correctly?

The good news: every recursive function follows the same structural template. Once you internalize this template, writing recursive code becomes almost mechanical. The creativity lies in identifying the problem decomposition; the implementation follows a predictable pattern.

A Historical Perspective

When John McCarthy designed LISP in 1958, he made recursion the primary control structure. He realized that any computation could be expressed through recursive functions following a simple pattern: check for the simplest case, otherwise reduce and recurse. This insight—that recursion has a canonical structure—has influenced programming language design for over 60 years.

The Universal Structure

Whether you're computing Fibonacci numbers, traversing a binary tree, or solving the Tower of Hanoi, the structure is the same. Only the specifics of what constitutes a "base case," how to "make the problem smaller," and how to "combine results" change.

The Five Essential Components

Every well-structured recursive function contains five essential components. Let's examine each in detail:

1. Function Signature

The function signature defines what the function takes as input and what it returns. In recursion, the signature is especially important because the function will call itself with the same signature.

🐍python
1def sum_list(numbers: list[int]) -> int:
2    # Input: a list of integers
3    # Output: an integer (the sum)

Key considerations for the signature:

  • Parameters should capture the "problem size"—what gets smaller with each call
  • Return type should match what you're computing—what gets built up as you return
  • Consider accumulator parameters for tail recursion (covered in a later section)

2. Base Case(s)

The base case is the termination condition—the simplest instance of the problem that we can solve directly without recursion. A recursive function may have one or more base cases.

Base Case={The simplest inputwhere answer is known directly\text{Base Case} = \begin{cases} \text{The simplest input} \\ \text{where answer is known directly} \end{cases}
🐍python
1def sum_list(numbers):
2    # Base case: empty list has sum 0
3    if len(numbers) == 0:
4        return 0
5    # ... recursive case ...

Base Case Requirements

  1. Must be reachable from any valid input
  2. Must not make a recursive call
  3. Must return a valid result directly
  4. Should handle the simplest possible inputs

3. Problem Reduction

The problem reduction step creates a smaller instance of the same problem. This is where we ensure progress toward the base case.

🐍python
1def sum_list(numbers):
2    if len(numbers) == 0:
3        return 0
4
5    # Problem reduction: separate first element from rest
6    first = numbers[0]
7    rest = numbers[1:]  # This is smaller than 'numbers'
8    # ... recursive call ...

Common reduction strategies:

StrategyExampleTypical Use
Decrement by 1n → n-1Factorial, counting
Halvingn → n//2Binary search, divide-and-conquer
Remove elementlist → list[1:]List processing
Traverse childnode → node.leftTree traversal
Shrink range[lo, hi] → [lo, mid]Binary search

4. Recursive Call(s)

The recursive call invokes the function on the reduced problem. This is where the "leap of faith" comes in—we trust that the call returns the correct answer for the smaller problem.

🐍python
1def sum_list(numbers):
2    if len(numbers) == 0:
3        return 0
4
5    first = numbers[0]
6    rest = numbers[1:]
7
8    # Recursive call: trust that this returns correct sum of rest
9    rest_sum = sum_list(rest)  # ← The leap of faith
10    # ... combine ...

5. Combination Step

The combination step uses the result from the recursive call to build the final answer. This is where the "work" of the current level happens.

🐍python
1def sum_list(numbers):
2    if len(numbers) == 0:
3        return 0
4
5    first = numbers[0]
6    rest = numbers[1:]
7    rest_sum = sum_list(rest)
8
9    # Combination: add first element to the sum of rest
10    return first + rest_sum

The combination step answers: "Given the solution to the smaller problem, how do I solve the current problem?"

ProblemCombination Step
Sum of listfirst + sum(rest)
Factorialn × factorial(n-1)
List length1 + length(rest)
Tree height1 + max(height(left), height(right))
Fibonaccifib(n-1) + fib(n-2)

Quick Check

In the function sum_list(numbers), which of the following is the 'problem reduction' step?


Interactive: Dissecting a Recursive Function

Use this interactive visualizer to explore each component of a recursive function. Click on the component buttons to highlight the corresponding code, or switch to execution mode to see how the function runs step by step.

Anatomy of a Recursive Function
1def factorial(n):
2 # Base case: simplest problem
3 if n <= 1:
4 return 1
5
6 # Recursive case: reduce problem
7 smaller = n - 1
8 sub_result = factorial(smaller)
9
10 # Combine: use sub-result
11 result = n * sub_result
12 return result

Click to explore each component

Notice how each component plays a distinct role:

  • Function Signature: Defines the contract—what goes in, what comes out
  • Base Case: Stops recursion and provides a foundation
  • Problem Reduction: Ensures progress toward the base case
  • Recursive Call: Delegates the smaller problem
  • Combination: Builds the answer from sub-results

Parameter Patterns and Conventions

The parameters of a recursive function determine how information flows through the call chain. Understanding parameter patterns helps you design correct recursive functions.

Pattern 1: The Shrinking Parameter

The most common pattern: a parameter that gets smaller with each call until it reaches the base case.

Shrinking Parameter Pattern
🐍shrinking_param.py
1The shrinking parameter

Parameter 'n' will decrease with each recursive call until it reaches the base case.

2Base case check

When n reaches 0 or below, we stop recursing.

6The shrinking step

n - 1 ensures progress toward the base case. Each call gets n one step closer to 0.

3 lines without explanation
1def countdown(n):
2    if n <= 0:
3        print("Blastoff!")
4        return
5    print(n)
6    countdown(n - 1)  # n shrinks by 1 each call

Pattern 2: The Accumulator Parameter

An extra parameter that accumulates the result as we recurse. This pattern is essential for tail recursion optimization.

Accumulator Parameter Pattern
🐍accumulator_param.py
1Accumulator parameter

The second parameter 'accumulator' carries the partial result down through the calls. It starts at 1 (the identity for multiplication).

3Return accumulator at base

When we reach the base case, the accumulator holds the complete answer.

5Update accumulator

Each call multiplies the current accumulator by n, passing the updated value down.

4 lines without explanation
1def factorial_acc(n, accumulator=1):
2    if n <= 1:
3        return accumulator  # Return accumulated result
4    # Multiply accumulator by n, then recurse
5    return factorial_acc(n - 1, accumulator * n)
6
7# Usage: factorial_acc(5) → 120

Compare the standard factorial vs. accumulator version:

StandardAccumulatorBehavior
factorial(5)factorial_acc(5, 1)Start
5 × factorial(4)factorial_acc(4, 5)First call
5 × 4 × factorial(3)factorial_acc(3, 20)Second call
5 × 4 × 3 × factorial(2)factorial_acc(2, 60)Third call
5 × 4 × 3 × 2 × factorial(1)factorial_acc(1, 120)Fourth call
5 × 4 × 3 × 2 × 1 = 120return 120Base case

Pattern 3: The Index/Position Parameter

A parameter that tracks the current position in a data structure, commonly used when you can't easily slice the data.

🐍python
1def binary_search(arr, target, lo, hi):
2    if lo > hi:
3        return -1  # Not found
4
5    mid = (lo + hi) // 2
6    if arr[mid] == target:
7        return mid
8    elif arr[mid] < target:
9        return binary_search(arr, target, mid + 1, hi)  # Search right
10    else:
11        return binary_search(arr, target, lo, mid - 1)  # Search left

Pattern 4: The Helper Function Pattern

When you need extra parameters for recursion that users shouldn't provide, wrap the recursive function in a helper.

🐍python
1def reverse_list(lst):
2    """Public interface: takes just the list"""
3    def helper(lst, result):
4        if not lst:
5            return result
6        return helper(lst[1:], [lst[0]] + result)
7
8    return helper(lst, [])  # Initialize accumulator internally
9
10# Usage: reverse_list([1, 2, 3]) → [3, 2, 1]

When to Use Each Pattern

  • Shrinking: Default choice for most problems
  • Accumulator: When building up a result; enables tail recursion
  • Index/Position: When working with arrays you can't slice efficiently
  • Helper: When you need internal state users shouldn't see

Return Value Conventions

How a recursive function returns values determines how results are built up. There are several common conventions.

Convention 1: Return and Combine

The most common pattern: each level returns a value that the caller combines with local work.

🐍python
1def list_length(lst):
2    if not lst:
3        return 0  # Base: empty list has length 0
4    return 1 + list_length(lst[1:])  # Combine: 1 + length of rest
5
6# Call chain for [a, b, c]:
7# length([a, b, c]) = 1 + length([b, c])
8#                   = 1 + (1 + length([c]))
9#                   = 1 + (1 + (1 + length([])))
10#                   = 1 + (1 + (1 + 0))
11#                   = 3

Convention 2: Return Only at Base

With accumulators, the return only happens at the base case—all computation is done "on the way down."

🐍python
1def list_length_acc(lst, count=0):
2    if not lst:
3        return count  # Only return at base case
4    return list_length_acc(lst[1:], count + 1)  # Add 1 on the way down
5
6# Call chain for [a, b, c]:
7# length([a, b, c], 0) → length([b, c], 1)
8#                      → length([c], 2)
9#                      → length([], 3)
10#                      → return 3 (directly!)

Convention 3: Collect Results

When building up a collection, each call returns a collection that gets combined.

🐍python
1def flatten(nested):
2    """Flatten a nested list into a single list"""
3    if not nested:
4        return []
5    if not isinstance(nested[0], list):
6        return [nested[0]] + flatten(nested[1:])
7    return flatten(nested[0]) + flatten(nested[1:])
8
9# flatten([[1, 2], [3, [4, 5]]]) → [1, 2, 3, 4, 5]

Convention 4: Return Multiple Values

Some problems require returning multiple pieces of information from each recursive call.

🐍python
1def min_max(lst):
2    """Return both minimum and maximum of a list"""
3    if len(lst) == 1:
4        return lst[0], lst[0]  # Single element is both min and max
5
6    rest_min, rest_max = min_max(lst[1:])  # Get min/max of rest
7    return min(lst[0], rest_min), max(lst[0], rest_max)
8
9# min_max([3, 1, 4, 1, 5]) → (1, 5)

Quick Check

Which return convention does the accumulator pattern use?


Work Before vs. Work After Recursion

A crucial structural decision: do you do work before the recursive call or after? This determines the order of processing.

Work Before Recursion (Pre-order)

Process the current element first, then recurse. Work happens "on the way down."

🐍python
1def print_forward(n):
2    if n <= 0:
3        return
4    print(n)           # Work FIRST
5    print_forward(n - 1)  # Then recurse
6
7# print_forward(3) outputs: 3, 2, 1

Work After Recursion (Post-order)

Recurse first, then process the current element. Work happens "on the way back up."

🐍python
1def print_backward(n):
2    if n <= 0:
3        return
4    print_backward(n - 1)  # Recurse FIRST
5    print(n)               # Then work
6
7# print_backward(3) outputs: 1, 2, 3

Both functions make the same recursive calls, but the output order is reversed!

Work PlacementProcessing OrderCommon Uses
Before (pre-order)Top-down, parent before childrenPreorder tree traversal, prefix expression
After (post-order)Bottom-up, children before parentPostorder tree traversal, computing aggregates
Before AND afterComplex processingInorder traversal, tree serialization

Example: Tree Traversals

Work placement is most clearly illustrated in tree traversals:

Work Placement in Tree Traversals
🐍tree_traversals.py
4Preorder: process first

Visit the node BEFORE visiting children. Root is processed first.

13Postorder: process last

Visit the node AFTER visiting all children. Root is processed last.

19Inorder: process between

Visit left subtree, then node, then right subtree. For BST, gives sorted order.

17 lines without explanation
1def preorder(node):
2    if node is None:
3        return
4    print(node.value)   # Work BEFORE recursing
5    preorder(node.left)
6    preorder(node.right)
7
8def postorder(node):
9    if node is None:
10        return
11    postorder(node.left)
12    postorder(node.right)
13    print(node.value)   # Work AFTER recursing
14
15def inorder(node):
16    if node is None:
17        return
18    inorder(node.left)
19    print(node.value)   # Work BETWEEN recursive calls
20    inorder(node.right)

Order Matters for Side Effects

When your recursive function has side effects (like printing or modifying state), work placement fundamentally changes behavior. For pure functions that just return values, the placement often doesn't matter—but for side effects, it's crucial.

Interactive: Call Stack Deep Dive

Watch how the call stack grows and shrinks during recursive execution. Each frame on the stack represents an active function call with its own local variables.

Call Stack Visualization
Winding Up

Source Code

1def factorial(n):
2 if n <= 1:
3 return 1
4
5 smaller = n - 1
6 sub_result = factorial(smaller)
7
8 result = n * sub_result
9 return result

Call Stack (grows up)

Stack empty - about to start

Current Action

Starting: Call factorial(4) from main

Memory Usage

Stack frames:0
Max depth: 4 frames
FastSlow
Step 1 of 16

Key observations from the visualization:

  • Winding Phase: Stack grows as we make recursive calls, each waiting for the next
  • Hitting Base Case: The deepest call returns directly without recursing
  • Unwinding Phase: Stack shrinks as returns propagate back up, computing final result
  • Memory Usage: Stack depth = maximum recursion depth = O(n) for linear recursion

Stack Frames and Memory

Each stack frame contains: (1) parameters for that call, (2) local variables, (3) return address. Deep recursion uses significant memory. We'll explore this more in the Call Stack Visualization section.

Recursive Patterns

Recursive functions can be categorized by how many recursive calls they make per invocation. Each pattern has different characteristics and use cases.

Linear Recursion

One recursive call per function invocation. Forms a straight line of calls from start to base case.

T(n)=T(n1)+O(1)O(n)T(n) = T(n-1) + O(1) \Rightarrow O(n)
🐍python
1# Linear: one recursive call
2def factorial(n):
3    if n <= 1: return 1
4    return n * factorial(n - 1)  # Single call

Binary Recursion

Two recursive calls per invocation. Forms a binary tree of calls. Common in divide-and-conquer algorithms.

T(n)=2T(n/2)+O(1)O(n) (merge sort)T(n)=T(n1)+T(n2)+O(1)O(2n) (naive fib)T(n) = 2T(n/2) + O(1) \Rightarrow O(n) \text{ (merge sort)}\\ T(n) = T(n-1) + T(n-2) + O(1) \Rightarrow O(2^n) \text{ (naive fib)}
🐍python
1# Binary: two recursive calls
2def fibonacci(n):
3    if n <= 1: return n
4    return fibonacci(n-1) + fibonacci(n-2)  # Two calls
5
6def merge_sort(arr):
7    if len(arr) <= 1: return arr
8    mid = len(arr) // 2
9    left = merge_sort(arr[:mid])    # First call
10    right = merge_sort(arr[mid:])   # Second call
11    return merge(left, right)

Multiple Recursion

More than two recursive calls per invocation. Often seen in backtracking and exhaustive search.

🐍python
1# Multiple: variable number of recursive calls
2def permutations(arr, path=[]):
3    if not arr:
4        return [path]
5    result = []
6    for i, elem in enumerate(arr):
7        remaining = arr[:i] + arr[i+1:]
8        result += permutations(remaining, path + [elem])  # n calls!
9    return result

Mutual Recursion

Two or more functions call each other. Used when the problem has alternating states.

🐍python
1# Mutual: functions call each other
2def is_even(n):
3    if n == 0: return True
4    return is_odd(n - 1)
5
6def is_odd(n):
7    if n == 0: return False
8    return is_even(n - 1)
9
10# is_even(4) → is_odd(3) → is_even(2) → is_odd(1) → is_even(0) → True

Interactive: Recursive Pattern Explorer

Explore different recursive patterns and see how they differ in structure, complexity, and use cases. Adjust the input size to observe how the call tree changes.

Recursive Pattern Explorer

Linear Recursion

One recursive call per function invocation. The call chain forms a straight line from start to base case.

Example Code
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# Call pattern:
# factorial(4)
#   → factorial(3)
#       → factorial(2)
#           → factorial(1) ← base case
Complexity
Time:O(n)
Space:O(n)
Use Cases
  • Computing factorial for combinatorics
  • Summing elements in a linked list
  • Reversing a string character by character
Key Characteristics
  • \u2713Single recursive call per invocation
  • \u2713Forms a linear chain of calls
  • \u2713Stack depth = input size (O(n))
  • \u2713Easy to convert to iteration
  • \u2713Each call does constant work

Call Tree Visualization

f(4)
f(3)
f(2)
f(1)
Total Calls
4
Max Stack Depth
4
Recursive call
Base case

Notice how different patterns have dramatically different characteristics:

PatternCalls per LevelTotal Calls (n=4)Stack Depth
Linear144
Binary (Fib)294
Multiple (Perms)n, n-1, n-2, ...334
Mutual1 (alternating)55

Anatomy Across Languages

The recursive structure is universal, but syntax varies. Here's the same algorithm in four languages.

Python

🐍python
1def sum_array(arr: list[int]) -> int:
2    if len(arr) == 0:  # Base case
3        return 0
4    return arr[0] + sum_array(arr[1:])  # Recursive case

C

📄c
1int sum_array(int arr[], int n) {
2    if (n == 0) {  // Base case
3        return 0;
4    }
5    return arr[0] + sum_array(arr + 1, n - 1);  // Recursive case
6}
7
8// Note: Pass pointer arithmetic and explicit size

C++

📄cpp
1#include <vector>
2
3int sumArray(const std::vector<int>& arr, size_t start = 0) {
4    if (start >= arr.size()) {  // Base case
5        return 0;
6    }
7    return arr[start] + sumArray(arr, start + 1);  // Recursive case
8}
9
10// Note: Use index instead of slicing for efficiency

TypeScript

📘typescript
1function sumArray(arr: number[]): number {
2  if (arr.length === 0) {  // Base case
3    return 0;
4  }
5  const [first, ...rest] = arr;  // Destructuring
6  return first + sumArray(rest);  // Recursive case
7}
LanguageArray HandlingSize TrackingNotes
PythonSlicing arr[1:]len(arr)Convenient but creates new lists
CPointer arithmeticExplicit parameterManual memory, efficient
C++Index parametervector.size()Avoids copying
TypeScriptDestructuring/slicearr.lengthFunctional style

Efficiency Consideration

Python's arr[1:] creates a new list each call, which is O(n) per call, making the total O(n\u00B2). For efficiency in any language, use an index parameter instead of slicing.

Common Structural Mistakes

Even with the template in mind, programmers make predictable structural errors. Here's how to recognize and fix them.

Mistake 1: Missing Base Case

🐍python
1# WRONG: No base case!
2def bad_factorial(n):
3    return n * bad_factorial(n - 1)  # Infinite recursion
4
5# RIGHT: Include base case
6def good_factorial(n):
7    if n <= 1:
8        return 1
9    return n * good_factorial(n - 1)

Mistake 2: Base Case Not Reachable

🐍python
1# WRONG: Base case at n == 0, but n might be negative
2def bad_countdown(n):
3    if n == 0:  # What if n starts negative?
4        return
5    bad_countdown(n - 1)  # Will never hit 0!
6
7# RIGHT: Handle all terminating conditions
8def good_countdown(n):
9    if n <= 0:  # Catches negative and zero
10        return
11    print(n)
12    good_countdown(n - 1)

Mistake 3: No Progress Toward Base Case

🐍python
1# WRONG: n never changes!
2def stuck(n):
3    if n <= 0:
4        return
5    stuck(n)  # Oops! Should be stuck(n - 1)
6
7# RIGHT: Ensure progress
8def progress(n):
9    if n <= 0:
10        return
11    progress(n - 1)  # n gets smaller

Mistake 4: Forgetting to Return Recursive Result

🐍python
1# WRONG: Recursive result is lost
2def bad_sum(lst):
3    if not lst:
4        return 0
5    bad_sum(lst[1:])  # Missing return!
6
7# RIGHT: Return the recursive result
8def good_sum(lst):
9    if not lst:
10        return 0
11    return lst[0] + good_sum(lst[1:])

Mistake 5: Wrong Combination

🐍python
1# WRONG: Combination doesn't match problem
2def bad_length(lst):
3    if not lst:
4        return 0
5    return lst[0] + bad_length(lst[1:])  # Adding elements, not counting!
6
7# RIGHT: Correct combination for the problem
8def good_length(lst):
9    if not lst:
10        return 0
11    return 1 + good_length(lst[1:])  # Adding 1 for each element

The Recursive Function Design Checklist

Before implementing any recursive function, work through this checklist:

  1. What is the problem size?
    • What parameter represents "how big" the problem is?
    • How will you make it smaller?
  2. What are the base cases?
    • What is the smallest valid input?
    • What is the answer for that input?
    • Are there multiple base cases?
  3. What is the recursive relationship?
    • How does the answer for size n relate to size n-1 (or smaller)?
    • Write this as a formula before coding
  4. What is the combination step?
    • What do you do with the recursive result?
    • What "work" happens at each level?
  5. Verify termination:
    • Does every possible input eventually reach a base case?
    • Does every recursive call make the problem strictly smaller?

Design Before Code

Write the recurrence relation before writing code. If you can express:
f(n)={base_valueif n=base_casecombine(n,f(smaller(n)))otherwisef(n) = \begin{cases} \text{base\_value} & \text{if } n = \text{base\_case} \\ \text{combine}(n, f(\text{smaller}(n))) & \text{otherwise} \end{cases}
Then the code follows directly from this formula.

Summary

We've dissected the anatomy of recursive functions, identifying the essential structural components that every recursive solution shares.

The Five Essential Components

ComponentPurposeKey Question
Function SignatureDefine input/output contractWhat parameters shrink? What is returned?
Base Case(s)Terminate recursionWhat is the simplest case with a known answer?
Problem ReductionMake progressHow do I create a smaller instance?
Recursive CallDelegate smaller problemTrust it returns the right answer
CombinationBuild final answerHow do I use the sub-result?

Key Patterns

PatternCalls per LevelTypical Complexity
Linear1O(n) time, O(n) space
Binary2O(n) to O(2ⁿ) time
MultipleVariableOften O(n!) time
Mutual1 (alternating)O(n) time

Design Principles

  • Write the recurrence relation before coding
  • Verify all inputs eventually reach a base case
  • Ensure every call makes the problem strictly smaller
  • Consider work placement (before/after) for correct ordering
  • Use accumulator parameters for tail recursion when appropriate

What's Next

Now that you understand how recursive functions are structured, the next section will dive deep into Call Stack Visualization—how recursion actually executes at the machine level, why stack overflow happens, and how to analyze space complexity.


Exercises

Conceptual Questions

  1. Explain in your own words why the combination step is necessary. What would happen if we only had base cases and recursive calls without combination?
  2. Given the function signature def mystery(n, acc=0), identify which parameter pattern this uses and explain why the default value is 0.
  3. A function makes the call f(n-2) recursively. What base cases might be needed, and why might a single if n == 0 be insufficient?
  4. Explain the difference between doing work before vs. after the recursive call. Give an example where this distinction matters.

Identification Exercises

For each function below, identify: (a) the base case(s), (b) the problem reduction, (c) the combination step, and (d) the recursion pattern (linear/binary/multiple/mutual).

  1. 🐍python
    1def power(base, exp):
    2    if exp == 0:
    3        return 1
    4    return base * power(base, exp - 1)
  2. 🐍python
    1def max_value(lst):
    2    if len(lst) == 1:
    3        return lst[0]
    4    return max(lst[0], max_value(lst[1:]))
  3. 🐍python
    1def binary_search(arr, target, lo, hi):
    2    if lo > hi:
    3        return -1
    4    mid = (lo + hi) // 2
    5    if arr[mid] == target:
    6        return mid
    7    elif arr[mid] < target:
    8        return binary_search(arr, target, mid + 1, hi)
    9    else:
    10        return binary_search(arr, target, lo, mid - 1)

Writing Exercises

  1. Write a recursive function product(lst) that computes the product of all elements in a list. First write the recurrence relation, then implement.
  2. Write a recursive function count_occurrences(lst, target) that counts how many times target appears in lst.
  3. Write a recursive function replace_all(lst, old, new) that returns a new list with all occurrences of old replaced by new.
  4. Write a recursive function deep_sum(nested) that sums all numbers in a potentially nested list structure. Example: deep_sum([1, [2, [3, 4]], 5]) returns 15.
  5. Convert the following iterative function to a recursive one with an accumulator:
    🐍python
    1def iterative_sum_squares(n):
    2    total = 0
    3    for i in range(1, n + 1):
    4        total += i * i
    5    return total

Challenge Problems

  1. Write mutually recursive functions is_sorted_asc(lst) and is_sorted_desc(lst) that determine if a list is sorted in ascending or descending order. Each should call the other in some way.
  2. Implement generate_subsets(lst) that returns all possible subsets of a list. Identify what type of recursion pattern this is.
  3. The Collatz sequence starting from n is: n, then n/2 if n is even, or 3n+1 if n is odd, until reaching 1. Write a recursive function collatz_length(n) that returns the number of steps to reach 1. Is this linear recursion? Why or why not?

Solution Strategy

For each problem:
  1. Write the recurrence: f(input)=combine(local,f(smaller))f(\text{input}) = \text{combine}(\text{local}, f(\text{smaller}))
  2. Identify base cases
  3. Verify all inputs reach base case
  4. Implement following the template

In the next section, we'll explore Call Stack Visualization—how recursive calls are actually implemented at the machine level, what each stack frame contains, and why understanding the stack is crucial for avoiding stack overflow errors.