Recursion Foundations

6 concept walkthroughs, each with a worked explanation and an interactive visualization, before you start solving problems in this area.

What is Recursion?

Recursion is when a function calls itself to solve a smaller version of the same problem. Instead of solving the whole thing at once, you break it into a tiny piece you CAN solve (the base case) and a smaller version of the original problem (the recursive case). The function keeps calling itself with smaller and smaller inputs until it hits the base case, then all the answers bubble back up. Let's see this in action with a classic example: computing factorial, given a number n, compute n! = n × (n-1) × ... × 1.

The intuition

Imagine you're in a line of people and someone asks 'What row am I in?' You don't count everyone. You just ask the person in front of you 'What row are YOU in?' They ask the person in front of them, and so on, until the person at the front says 'Row 1!' Then each person adds 1 and passes the answer back. That's recursion: delegating the work to a smaller version of yourself.

A real-world analogy

Russian nesting dolls (Matryoshka): to find the smallest doll, you open the outer one, then the next, then the next… until you find one that doesn't open. That's the base case. Then you close them back up, that's the unwinding.

function factorial(n):
    if n <= 1:           // BASE CASE, stop here!
        return 1
    return n * factorial(n - 1)  // RECURSIVE CASE, smaller problem

The Call Stack

Every time a function calls another function (or itself), the computer saves a 'frame' on the call stack, like stacking plates. Each frame remembers the function's local variables, where it was in the code, and what it's waiting for. When the function returns, its frame is popped off the stack. In recursion, each recursive call adds a new frame. Too many calls without returning = Stack Overflow!

The intuition

Think of a stack of sticky notes. Each time you make a recursive call, you write 'come back to this later' on a new sticky note and put it on top. When you hit the base case, you start peeling sticky notes off the top, finishing each task as you go. If you never stop adding sticky notes (no base case), you run out of desk space, that's a stack overflow.

A real-world analogy

A stack of cafeteria trays: you add trays on top (function calls) and remove from the top (function returns). If someone keeps adding trays without removing any, the stack topples over (stack overflow).

function sum_to(n):
    if n <= 0: return 0     // Base case: pop frame, return 0
    return n + sum_to(n-1)  // Push new frame, wait for result

// sum_to(4) builds this stack:
//   [sum_to(4)] waits for sum_to(3)
//   [sum_to(3)] waits for sum_to(2)
//   [sum_to(2)] waits for sum_to(1)
//   [sum_to(1)] waits for sum_to(0)
//   [sum_to(0)] returns 0  ← base case, start popping!
//   [sum_to(1)] returns 1+0 = 1
//   [sum_to(2)] returns 2+1 = 3
//   [sum_to(3)] returns 3+3 = 6
//   [sum_to(4)] returns 4+6 = 10

Base Case: The Safety Net

The base case is the condition where your recursive function STOPS calling itself and returns a value directly. Without it, recursion never ends: the function calls itself forever until the stack overflows. Every recursive function MUST have at least one base case. The base case answers: 'What's the simplest input I can solve immediately without any further recursion?'

The intuition

Imagine you're walking down a staircase in the dark. Each step, you feel for the next stair. The base case is the floor, solid ground where you stop. Without a floor, you'd fall forever. The base case is your safety net that guarantees the recursion terminates.

A real-world analogy

A mirror facing another mirror creates infinite reflections: that's recursion without a base case. Put a piece of cardboard behind one mirror (base case) and the reflections stop at a finite point.

// ❌ BAD: No base case, infinite recursion!
function broken(n):
    return broken(n - 1)   // Never stops!

// ✅ GOOD: Clear base case
function sum(n):
    if n == 0: return 0    // Base case: sum of nothing is 0
    return n + sum(n - 1)  // Recursive case

// ✅ Multiple base cases (Fibonacci)
function fib(n):
    if n == 0: return 0    // Base case 1
    if n == 1: return 1    // Base case 2
    return fib(n-1) + fib(n-2)

The Leap of Faith

The Leap of Faith is the #1 tip for writing recursive code. Here's the idea: ASSUME your recursive function already works correctly for smaller inputs. Don't trace through every call. Instead, focus on just THREE things: (1) What's the base case? (2) What does THIS one call need to do? (3) How do I combine my work with the result of the recursive call? If you get these three right, the recursion WILL work. Trust it.

The intuition

Imagine you're a manager. You don't do all the work yourself: you delegate to your team and trust they'll deliver. You only focus on: What's MY part? What do I do with THEIR result? If each person in the chain does their small part correctly and trusts the person below them, the whole chain works. That's the leap of faith.

A real-world analogy

You're organizing a line of 100 people. Instead of organizing all 100, you tell the first person: 'Organize the 99 people behind you, then stand at the end.' You TRUST person 2 will do the same for the 98 behind them, and so on. Person 100 has nobody behind them (base case). The whole line gets organized without anyone thinking about all 100 people.

// Sum of array using Leap of Faith
def sum_array(arr):
    if len(arr) == 0:        # Q1: simplest input → return 0
        return 0
    # Q2: TRUST sum_array(arr[1:]) gives correct sum of the rest
    return arr[0] + sum_array(arr[1:])
    # Q3: arr[1:] shrinks by 1 each time → guaranteed to reach []

Recursion vs Iteration

Every recursion can be converted to a loop (iteration), and vice versa. Recursion uses the call stack implicitly; iteration uses an explicit loop variable. Recursion is more natural for problems with a tree/branching structure (trees, graphs, backtracking). Iteration is simpler and more efficient for linear problems (summing an array, Fibonacci). Knowing WHEN to use each is a key skill.

The intuition

Recursion is like giving instructions by saying 'do this, then repeat from step 1 with a smaller version.' Iteration is like saying 'keep doing this until done.' Both get you to the same place, but some paths are easier to walk than others.

A real-world analogy

Washing dishes: Iteration is 'wash dish, grab next, wash dish, grab next...' Recursion is 'wash this dish, then tell your clone to wash the rest.' Both work, but you wouldn't clone yourself for dishes, you'd just loop. But for exploring a maze with multiple paths? Cloning yourself at each fork (recursion) makes more sense than backtracking manually (iteration with explicit stack).

// Factorial: both approaches

// RECURSIVE (implicit stack):
function factRec(n):
    if n <= 1: return 1
    return n * factRec(n - 1)

// ITERATIVE (explicit loop):
function factIter(n):
    result = 1
    for i = 2 to n:
        result = result * i
    return result

// Both compute the same thing!
// Iterative: O(1) space, no stack overhead
// Recursive: O(n) space from call stack

Common Recursion Patterns

Most recursive problems follow a few recurring patterns. Once you recognize which pattern a problem fits, writing the solution becomes formulaic. The main patterns are: (1) Counting down. Reduce n by 1 each call, (2) Building up return values. Accumulate results as calls return, (3) Passing state via parameters. Carry information DOWN the recursion, (4) Divide & conquer, split the problem in half. Learning these patterns gives you a toolkit for any recursive problem.

The intuition

Think of recursion patterns like recipe templates. A 'stir-fry' template works for hundreds of dishes, you just swap the ingredients. Similarly, once you know the 'accumulator pattern' template, you can apply it to sum, product, string building, and many more problems just by changing what you accumulate.

A real-world analogy

Like recognizing that 'find the tallest person' follows the same pattern whether it's 'find the max number,' 'find the longest string,' or 'find the heaviest box.' Once you see the pattern, the specific problem doesn't matter.

// Pattern 1: Reduce & Return (factorial)
function factorial(n):
    if n <= 1: return 1
    return n * factorial(n - 1)  // combine on way BACK

// Pattern 2: Accumulator (tail-recursive sum)
function sum(arr, i, acc=0):
    if i == arr.length: return acc
    return sum(arr, i+1, acc + arr[i])  // carry state DOWN

// Pattern 3: Divide & Conquer (merge sort)
function mergeSort(arr):
    if arr.length <= 1: return arr
    mid = arr.length / 2
    left = mergeSort(arr[0..mid])
    right = mergeSort(arr[mid..])
    return merge(left, right)

// Pattern 4: Build & Collect (subsets)
function subsets(nums, i, current, result):
    if i == nums.length:
        result.add(copy(current))
        return
    current.add(nums[i])        // include nums[i]
    subsets(nums, i+1, current, result)
    current.removeLast()        // exclude nums[i] (backtrack!)
    subsets(nums, i+1, current, result)

All DSA learning paths