🚀 Phases 1–5 are live — Days 1–17 cover the foundations and the algorithmic patterns. See the roadmap →
Day 5 - RecursionIntroduction

Introduction to Recursion

Definition

Recursion is when a function calls itself — directly or through a chain of other functions — to solve a problem by breaking it into a smaller version of the same problem.

GIF

The skeleton looks like this:

void func() {
    // Some work
    func();   // call myself with a smaller version of the problem
    // Some work
}

That’s the entire idea. What makes it work (and not loop forever) is the base case.

The three rules

Every correct recursive function obeys all three:

  1. The function calls itself. (Otherwise it isn’t recursion.)
  2. There is a base case — at least one input where the function returns without recursing.
  3. Each recursive call moves toward the base case. The argument shrinks, the range narrows, the tree gets smaller. Otherwise we recurse forever.

Miss any one of these and you get a stack overflow — the call stack grows until your program crashes.

// Counts down from N to 1, then stops.
void count(int N) {
    if (N == 0) return;        //  ← base case
    cout << N << " ";
    count(N - 1);              //  ← progress toward base case
}

Worked example: Factorial

Factorial of N is N · (N-1) · (N-2) · … · 1. The recursive definition is one line:

factorial(0) = 1
factorial(N) = N * factorial(N - 1)

The base case (N == 0) and the recursive case fall straight out of the math.

The recursion tree

factorial(5) produces a single chain of calls — one call per level. There’s no branching because each call makes exactly one recursive call.

factorial(5)
f(5)= 120f(4)= 24f(3)= 6f(2)= 2f(1)= 1f(0)= 1
recursive call    base case6 total calls

The green box is the base case. Each box shows what it eventually returned.

How the call stack does the bookkeeping

Every call to factorial(N) pushes a stack frame containing its local N. The frame waits — paused — until the recursive call returns. Then it multiplies and returns.

Call stack for factorial(3)
Step 0 / 8(start)
── top ──
(empty)
call stack

Notice the rhythm: push down, hit the base, then pop back up multiplying along the way. That’s the call stack doing your work for you.

Stack overflow — the danger

The call stack has a fixed maximum depth (a few thousand in most environments). Two things cause it to overflow:

  1. No base case. The function never stops calling itself.
  2. Wrong direction. The function recurses but the argument doesn’t shrink — for example, factorial(N + 1) instead of factorial(N - 1).
⚠️

Most languages cap recursion depth around 1,000–10,000. If you need deeper recursion, either raise the limit (Python’s sys.setrecursionlimit), rewrite iteratively with an explicit stack, or rely on tail-call optimization (only some languages have it — not Python or Java).

Recursion vs Iteration

Both can solve the same problems — the difference is mostly expressiveness and cost.

AspectIterationRecursion
Control flowA loop with a control variableA function that calls itself
TerminationThe loop condition becomes falseThe recursive call hits the base case
MemoryConstant — a few variablesOne stack frame per level — proportional to depth
Speed (in practice)Usually faster (no call overhead)Slower, but readable code wins for tree-shaped problems
Best forLinear sweeps, simple countingTrees, divide & conquer, backtracking, self-similar structure

The same idea, both ways

Both work. For this problem iteration is clearly better — same complexity, less overhead. The recursive form really pays off for tree-shaped problems where the iterative version would need its own explicit stack.

Activity — try it both ways

Write a function to reverse a string, once iteratively and once recursively.

Types of Recursion

There are four shapes of direct recursion worth recognizing — and one for cross-function calls.

Types of Recursion
Types of Recursion

Tail Recursion

The recursive call is the last thing the function does. Anything computed before it is already saved in the argument.

void printDown(int n) {
    if (n == 0) return;
    cout << n << " ";
    printDown(n - 1);   // ← last statement
}

Some languages (Scheme, Scala, some C++ compilers) optimize this into a plain loop, keeping memory at O(1). Python and Java do not — recursive depth still costs stack frames.

Head Recursion

The recursive call happens before any other work in the function.

void printUp(int n) {
    if (n == 0) return;
    printUp(n - 1);     // ← first statement
    cout << n << " ";   // happens on the way back up
}

Head recursion naturally reverses the natural order — work happens during the unwinding phase. Compare printUp(3) with printDown(3):

  • printDown(3) prints 3 2 1
  • printUp(3) prints 1 2 3

Same recursion, opposite output, just because of where the print sits.

Tree Recursion

The function calls itself more than once per invocation.

int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);   // two recursive calls
}

This is what gives us the “tree” shape — each call branches into multiple children.

fib(5) — see how many calls explode out
fib(5)= 5fib(4)= 3fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)= 1fib(1)= 1fib(0)= 0fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1
recursive call    base case15 total calls

Notice fib(2) and fib(3) show up multiple times. That’s massive duplicated work — exponential time for what should be a linear problem. We’ll fix this on the Memoization page.

Nested Recursion

A recursive call is passed itself as an argument. Rare in practice, but the Ackermann function is a classic example.

int ackermann(int m, int n) {
    if (m == 0) return n + 1;
    if (n == 0) return ackermann(m - 1, 1);
    return ackermann(m - 1, ackermann(m, n - 1));  // nested!
}

Indirect Recursion

Two (or more) functions call each other in a cycle.

bool isEven(int n) {
    if (n == 0) return true;
    return isOdd(n - 1);   // even(n) calls odd(n-1)
}
 
bool isOdd(int n) {
    if (n == 0) return false;
    return isEven(n - 1);  // odd(n) calls even(n-1)
}

Each function still has its own base case — but the recursion lives across function boundaries.

Fun fact: if you’ve seen the movie Inception, the dream-within-a-dream-within-a-dream structure is exactly tree recursion. Each level can spawn more levels, and when you wake up you “return” to the level above.

What the runtime actually does

When you call a function, the language runtime:

  1. Pushes a stack frame onto the call stack containing the function’s local variables, arguments, and return address.
  2. Jumps to the function’s first instruction.
  3. When the function returns, pops the frame and resumes the caller at the return address.

Recursion is just this same mechanism applied to itself. The stack tracks every “in-progress” call. Each return collapses one level. The reason recursion is so natural for trees is that the call stack already is a tree traversal in disguise.


Got the mental model? Next: how to design a recursive solution from scratch — without ever tracing through the call stack by hand.