Recursion

CS205 Data Structures

"To understand recursion, you must first understand recursion."

Use arrow keys to navigate · Interactive slides ahead!

What Is Recursion?

A recursive function is a function that calls itself to solve smaller instances of the same problem.

Analogy: Russian Nesting Dolls

Each doll contains a smaller doll inside it. You keep opening dolls until you reach the smallest one (the base case) — then you stop.

Real-World Examples

  • A folder that contains subfolders (which contain subfolders...)
  • A mirror reflecting another mirror
  • A story within a story within a story
  • Looking up a word in the dictionary, and the definition uses another word you need to look up

Key Idea

Recursion works because each call solves a smaller version of the same problem, until the problem is so small we know the answer directly.

The Two Essential Parts

Every correct recursive function needs exactly two things:

1. Base Case (Termination)

The condition under which the function does NOT call itself. This is the "smallest doll" — the answer we know directly.

2. Recursive Case (Progress)

The function calls itself with a smaller or simpler input, making progress toward the base case.

Warning

If you forget the base case, or if the recursive case doesn't make progress, you get infinite recursion and a StackOverflowError.

Interactive Flowchart

Click a path to highlight it:

Example: Factorial

n! = n × (n-1) × ... × 1  and  0! = 1

Recursive Definition

function factorial(n): if n == 0: // base case return 1 else: // recursive case return n * factorial(n - 1)
Click "Step" to trace factorial(4)

Call Stack

Key Idea

factorial(4) can't finish until factorial(3) finishes, which can't finish until factorial(2)...

The Call Stack

The call stack is how the computer keeps track of which function called which, what each function's local variables are, and where to return to.

How It Works

  • Each function call creates a stack frame
  • A frame holds: parameters, local variables, return address
  • Frames are pushed on call, popped on return
  • The stack grows upward in our diagrams

Stack Overflow

The stack has a finite size (typically a few MB). Too many recursive calls without returning will exhaust it, causing a StackOverflowError.

Build the Stack: factorial(4)

Click "Push Next Frame" to build the stack

Tracing Recursion Step by Step

Watch factorial(4) expand and collapse. The left shows the call chain, the right shows the stack.

Call Trace

factorial(4) = 4 × factorial(3) = 3 × factorial(2) = 2 × factorial(1) = 1 × factorial(0) = 1 ← base case! ── Collapsing ── factorial(0) = 1 factorial(1) = 1 × 1 = 1 factorial(2) = 2 × 1 = 2 factorial(3) = 3 × 2 = 6 factorial(4) = 4 × 6 = 24 ← answer!

Stack State

Step 0 of 11 — Ready

Example: Fibonacci

fib(0)=0, fib(1)=1, fib(n) = fib(n-1) + fib(n-2) for n ≥ 2

Code

function fib(n): if n == 0: return 0 // base if n == 1: return 1 // base return fib(n-1) + fib(n-2)

Sequence

n01234567
fib(n)011235813

Key Idea

Unlike factorial (one recursive call), Fibonacci makes two recursive calls per invocation. This creates a binary tree of calls.

Recursion Tree for fib(n)

Draws the full recursion tree — redundant calls in red

Why Naive Fibonacci Is Slow

The recursion tree has O(2n) nodes because we recompute the same values over and over.

Repeated Subproblems

In fib(5), look how many times each value is computed: fib(5): 1 time fib(4): 1 time fib(3): 2 times <-- waste! fib(2): 3 times <-- waste! fib(1): 5 times <-- waste! fib(0): 3 times <-- waste! For fib(40): ~331 million calls! But only 41 unique subproblems.

Exponential Growth

fib(50) would take minutes. fib(100) would take longer than the age of the universe.

Naive vs Memoized — Live Comparison

Naive O(2n)

0
calls made

Memoized O(n)

0
calls made
Click "Run Both" to compare call counts
// The Fix: Memoization function fib_memo(n, memo={}): if n in memo: return memo[n] if n == 0: return 0 if n == 1: return 1 memo[n] = fib_memo(n-1) + fib_memo(n-2) return memo[n]
ApproachTimeSpace
Naive recursionO(2n)O(n) stack
MemoizedO(n)O(n)
IterativeO(n)O(1)

Example: Sum of an Array

Compute the sum of an array recursively: peel off one element at a time.

Code

function sum(arr, n): // n = number of elements if n == 0: // base case return 0 return arr[n-1] + sum(arr, n-1)

Analogy

Imagine a stack of bills. Take the top bill, add its value to the total of the remaining stack. If empty, total is $0.

Click Step to trace sum([3, 7, 2, 5], 4)

Array & Stack

Example: Binary Search (Recursive)

Search a sorted array by repeatedly halving the search space. Classic divide and conquer.

Code

function binarySearch(arr, target, lo, hi): if lo > hi: // not found return -1 mid = (lo + hi) / 2 if arr[mid] == target: // found! return mid if target < arr[mid]: return binarySearch(arr, target, lo, mid-1) else: return binarySearch(arr, target, mid+1, hi)

Key Idea

Each call eliminates half the remaining elements → O(log n) time.

Interactive Search

Search arr = [1, 3, 5, 7, 9, 11, 13]

Example: Power Function

Compute xn recursively. Two approaches with very different performance.

Naive: O(n) Slow

function power(x, n): if n == 0: return 1 return x * power(x, n-1)

Fast: O(log n) Fast

function fastPower(x, n): if n == 0: return 1 half = fastPower(x, n / 2) if n is even: return half * half else: return x * half * half

Key Idea

Squaring the half-result halves the exponent each time. This is exponentiation by squaring.

Live Comparison

Naive O(n)

Fast O(log n)

Click Compare to see step-by-step traces

Linear vs Binary/Multiple Recursion

Recursive functions are classified by how many recursive calls they make.

Linear Recursion (1 call) Chain

Examples: factorial, sum, binary search

Depth & total calls: O(n) or O(log n)

Key Idea

Linear recursion creates a chain. Easy to convert to iteration.

Binary/Multiple Recursion (2+ calls) Tree

Examples: fibonacci, merge sort, Towers of Hanoi

Depth: O(n) or O(log n), but total calls up to O(2n)!

Warning

Binary recursion can be exponential in total calls even though stack depth is only O(n).

Tail Recursion

A recursive call is a tail call if it is the very last operation — nothing happens after the call returns.

NOT Tail Recursive

function factorial(n): if n == 0: return 1 return n * factorial(n-1) // multiply AFTER call

Tail Recursive Version

function factorial(n, acc=1): if n == 0: return acc return factorial(n-1, n*acc) // call IS the return

Note

Java and Python do not optimize tail calls. Functional languages (Haskell, Scheme, Scala) typically do.

Stack Comparison (n = 4)

Non-Tail (stack grows)

Tail (1 frame reused)

Click Animate to see the difference

Key Idea

Tail recursion carries the result in an accumulator. The compiler can reuse a single frame — same speed, O(1) stack.

Recursion vs Iteration

Any recursive algorithm can be converted to iteration (and vice versa). When should you use which?

Side by Side: Factorial

// RECURSIVE function factorial(n): if n == 0: return 1 return n * factorial(n-1)
// ITERATIVE function factorial(n): result = 1 for i in 1..n: result *= i return result
AspectRecursionIteration
ClarityCleaner for trees, D&CCleaner for simple loops
StackO(n) framesO(1)
SpeedCall overheadSlightly faster
RiskStack overflowInfinite loop

When to Use Recursion

  • Problem has a recursive structure (trees, graphs, nested data)
  • Divide and conquer algorithms (merge sort, quicksort)
  • Backtracking problems (N-queens, mazes)
  • The recursive solution is much simpler than iterative

When to Use Iteration

  • Simple linear computations (sum, factorial)
  • Performance-critical code
  • Very deep recursion possible (n > 10000)
  • Language lacks tail-call optimization

Analogy

Recursion is like giving instructions to a chain of helpers — each passes a smaller task down. Iteration is like doing it yourself in a loop.

Common Recursion Patterns

Most recursive algorithms fall into one of these three families. Click a pattern to see it animated.

Divide & Conquer

Split into independent subproblems, solve each, then combine.

Examples: merge sort, quicksort, binary search, fast power

Decrease & Conquer

Reduce by a constant amount (usually 1) each step. Linear chain.

Examples: factorial, sum of array, insertion sort

Backtracking

Try a choice, recurse. If fail, undo and try next option.

Examples: N-queens, maze, sudoku, permutations

Towers of Hanoi

Move n disks from peg A to peg C. Rules: one disk at a time; never place larger on smaller.

Recursive Solution

function hanoi(n, from, to, aux): if n == 1: move disk 1 from 'from' to 'to' return hanoi(n-1, from, aux, to) // top n-1 to aux move disk n from 'from' to 'to' hanoi(n-1, aux, to, from) // n-1 from aux to to

Key Idea

Move top n-1 out of the way, move the big disk, put n-1 back on top. Takes 2n - 1 moves.

Interactive Playground

Moves: 0 | Minimum: 7

Common Mistakes

Debugging recursion can be tricky. Watch out for these pitfalls.

1. Missing Base Case

// BROKEN — no base case! function factorial(n): return n * factorial(n-1) // → factorial(0) → factorial(-1) → ... OVERFLOW

Fix: Always define when to stop.

2. Not Making Progress

// BROKEN — n never changes! function countdown(n): if n == 0: return print(n) countdown(n) // should be n-1

Fix: Each call must move closer to the base case.

3. Wrong Base Case

// BROKEN — misses n=0! function factorial(n): if n == 1: return 1 return n * factorial(n-1) // factorial(0) → 0 * factorial(-1) → ...

Fix: Consider all inputs including edge cases.

4. Redundant Computation

// WORKS but O(2^n) slow! function fib(n): if n <= 1: return n return fib(n-1) + fib(n-2) // fib(5) computes fib(2) three times!

Fix: Use memoization or convert to iteration.

Debugging Tip

Add print statements at the start of the function showing parameters, and at the end showing the return value. This makes the call tree visible.

Summary & Cheat Sheet

The Recursion Recipe

  1. Identify the base case(s) — simplest inputs with known answers
  2. Identify the recursive case — how to break into smaller pieces
  3. Ensure each call makes progress toward a base case
  4. Trust the recursion — assume the recursive call works for smaller input
  5. Combine the result of the recursive call with the current step
Pattern Time Example ───────────────────────────────────────── Decrease by 1 O(n) factorial Decrease by half O(log n) bin search Two calls (dep.) O(2^n)* fibonacci* Two calls (indep.) O(n log n) merge sort Backtracking varies N-queens * without memoization

Key Concepts

ConceptWhat It Means
Base CaseWhen to stop recursing
Recursive CaseSelf-call with smaller input
Call StackFrames tracking each call
Stack OverflowToo-deep recursion
Tail RecursionRecursive call is last op
MemoizationCache repeated subproblems
Divide & ConquerSplit, solve halves, combine
BacktrackingTry, recurse, undo if fail

Final Analogy

Recursion is the art of solving a problem by saying: "If I could magically solve a slightly smaller version, how would I use that to solve the original?" Then handle the tiniest case directly.

CS205 Data Structures · Recursion

Challenge: Predict the Stack

At step 3 of factorial(5), which shows the correct call stack?

Option A

factorial(2) n=2, waiting factorial(3) n=3, waiting factorial(4) n=4, waiting factorial(5) n=5, waiting main()

Option B

factorial(5) n=5, waiting factorial(4) n=4, waiting factorial(3) n=3, waiting main()

Option C

factorial(3) n=3, waiting factorial(4) n=4, waiting factorial(5) n=5, waiting main()

Option D

factorial(2) n=2, waiting factorial(3) n=3, waiting factorial(5) n=5, waiting main()

Challenge: Fix the Memoized Fibonacci

This memoized fibonacci has a bug. Click the buggy line to fix it.

function fib(n, memo={}): if n in memo: return memo[n] if n <= 1: return n result = fib(n-1) + fib(n-2) return result
Click the line that has the bug

Challenge: How Many Calls?

Binary search in a sorted array of 15 elements. The target is at index 12. How many recursive calls to find it?

1 call

2 calls

3 calls

4 calls

Quiz Round

Q1: What happens if a recursive function has no base case?

Returns 0
StackOverflowError
Returns null
Compiler error

Q2: What is the time complexity of naive recursive fibonacci?

O(n)
O(n log n)
O(2n)
O(n2)

Q3: Which is NOT true about tail recursion?

The recursive call is the last operation
It can be optimized to O(1) stack space
Java automatically optimizes tail calls
It uses an accumulator parameter

Challenge: Solve Towers of Hanoi

Move all 4 disks from A to C in the minimum number of moves (15). Click pegs to move the top disk.

Moves: 0 / 15

Click a peg to pick up the top disk, then click another peg to drop it.

Challenge: Trace the Recursion

Given this function, what does mystery(4) return?

function mystery(n): if n <= 1: return 1 return n + mystery(n - 2)
4
6
7
10

Recursion: Interview vs Production

Recursion in Production meme