Thinking recursively

A function that calls itself sounds like a trick. It's actually the most natural way to solve a whole family of problems - once you learn to trust the smaller call. Watch factorial build a tower of calls and unwind it.

22 min read

Watch it run
Factorial.java
1 / 16

Call factorial(5) - a new frame is pushed onto the call stack.

step 1 / 16
Panels
Data structures
No arrays or objects in scope yet -
primitives are shown in Variables.
Legendcurrent linejust swapped / movedbeing comparedijpointers slide to the index they point atactive function framefilled DP cell

Real execution, step by step. Press play or scrub with the controls - every variable and array index updates live.

Start here: what this lesson is for

Recursion is the topic where people most often decide they are 'not a natural programmer'. That reaction is almost always caused by being taught it backwards - shown a clever example and asked to trace it in their head.

You are not going to trace anything in your head here. The whole approach in this lesson is to trust the smaller call and check only two things, and the animation above does the tracing for you so you can watch what your head cannot hold.

By the end you will be able to write a recursive method by answering two questions, read a stack of frames without losing your place, and know why deep recursion crashes rather than merely slowing down.

This lesson matters more than its length suggests. Trees, graphs, backtracking and dynamic programming - four later chapters - are all recursion in different clothes. Time spent here pays back four times.

Remember these
  • What you need first: lessons 1 to 3, and comfort with a method calling another method.
  • What you will be able to do: write and read recursive code without tracing it mentally.
  • How long: about 22 minutes, plus two problems at the end.
Words you just learned
recursion
- a method that solves a problem by calling itself on a smaller version of it

A function that calls itself

Every function you've written so far calls other functions. Recursion is the moment a function calls itself - and the first time you see it, it looks like either a typo or black magic. It's neither. It's the most natural way in the world to solve any problem that contains a smaller copy of itself.

Take factorial: 5! means 5 x 4 x 3 x 2 x 1. Look closely and there's a smaller factorial hiding inside - 5! is just 5 x 4!. And 4! is 4 x 3!. Each version of the problem contains a slightly smaller version, all the way down to 1!, which we simply know: it's 1. No further work needed.

That's the entire recipe, and every recursive function you will ever write has the same two parts: a base case - the input so small you can answer it on the spot - and a recursive case - answer the big problem using the answer to the smaller one. Press play above and watch factorial(5) do exactly this.

Imagine it like this: Russian nesting dolls. To count how many dolls there are, open one and ask 'how many are inside THIS one?' - then add 1. The tiniest doll doesn't open: that's the base case, and the counting unwinds from there.

Words you just learned
recursion
- a function solving a problem by calling itself on a smaller version of it
base case
- the input small enough to answer directly - where the calls stop
recursive case
- the step that reduces the problem and combines the smaller answer

Watch the tower rise and fall

Now turn your eyes to the call stack panel in the animation (toggle it on in Panels if it's off). Every time factorial calls itself, a new frame appears - a box holding that call's own private n. factorial(5) sits at the bottom, then factorial(4) stacks on top, then 3, 2, and finally factorial(1).

At factorial(1), the base case fires: it returns 1 without recursing. And now the beautiful part - the tower unwinds. factorial(2) had been waiting for that answer; it multiplies 2 x 1 and returns 2. factorial(3) wakes up, computes 3 x 2 = 6. Then 4 x 6 = 24, then 5 x 24 = 120. Each frame pops as it hands its answer down.

This is the mental model that beats all the confusion: a recursive call doesn't 'jump back into the function' - it creates a brand new copy of it, with its own variables, stacked on top of the caller, which simply pauses and waits.

Each call has its OWN n

There isn't one n changing value - there are five separate n's alive at once, one per frame: 5, 4, 3, 2, 1. Watch the Variables panel as you scrub: each frame remembers its own.

Remember these
  • Calling itself creates a NEW frame with its own variables
  • The caller pauses and waits for the callee's return value
  • The base case is where the tower stops growing
  • Answers combine on the way back DOWN the stack
Words you just learned
call stack
- the pile of active function calls; the top one is running, the rest wait
frame
- one call's private world - its parameters and local variables

What happens if you forget to stop

Delete the base case from factorial and run it in your head: factorial(5) calls factorial(4) calls factorial(3)... past zero, into the negatives, forever. Except not forever - each call costs a frame of memory, and the stack has a limit. When the tower hits it, the program dies with the most famous crash in programming: StackOverflowError (Java) - the error an entire website is named after.

So whenever a recursive function misbehaves, check the same two suspects in order: is there a base case, and does every recursive call actually move toward it? A call that doesn't shrink the problem is a call that never ends.

Imagine it like this: An out-of-office reply that says 'for details, email me'. Your auto-reply triggers theirs, theirs triggers yours - nothing shrinks, nothing stops, until a server gives up. That server's limit is your stack size.

The two-question checklist

Before running any recursion, ask: 1) What input stops it? 2) Does each call get closer to that input? If either answer is fuzzy, you've found tomorrow's stack overflow.

Words you just learned
stack overflow
- the crash when recursion piles up more frames than the stack can hold

Recursion vs loops - the honest comparison

Could you write factorial with a loop? Absolutely - and for factorial, the loop is arguably simpler. So why learn recursion at all? Because loops flatten; recursion branches. A loop walks a straight line. Recursion can explore a tree of possibilities - and trees, as you'll see from Chapter 8 onward, are everywhere: file systems, decision trees, search spaces, the DOM of every web page.

The cost side: factorial(n) makes n calls doing constant work each - O(n) time, same as the loop. But it also holds up to n frames alive at once, so it spends O(n) memory on the stack, where the loop spends O(1). That trade - elegance and tree-power for stack space - is the recurring theme of this chapter, and the next lesson makes the cost side dramatic.

Remember these
  • Anything a loop can do, recursion can do - and vice versa
  • Recursion wins when the problem branches like a tree
  • factorial(n): O(n) time like the loop, but O(n) stack memory too
  • Deep recursion risks stack overflow; loops never do

The two questions that write the method for you

Here is the practical technique, and it deliberately never asks you to picture the whole recursion.

Question one: when is it trivially over? That is the base case. For counting down, it is reaching zero. For a list, it is the empty list. For a tree, it is a null node. Write this first, always - a recursion without a base case is an infinite loop with extra steps.

Question two: assuming the smaller call already works, how do I use its answer? This is the leap people resist, because it feels like assuming the thing you are trying to prove. It is not. It is the same trust you place in any method you did not write: you call Math.max without re-deriving it, and you call factorial(n-1) the same way.

That is the whole method. Note what is missing: at no point did you trace five levels of calls in your head. If you find yourself doing that, you have skipped question two and you will get tired long before you get correct.

TwoQuestions.java
java
// Q1: when is it trivially over?   -> an empty array has sum 0
// Q2: if sumFrom(i+1) already works, how do I use it?
//                                  -> add a[i] to whatever it returns

int sumFrom(int[] a, int i) {
    if (i == a.length) return 0;            // Q1: base case
    return a[i] + sumFrom(a, i + 1);        // Q2: trust the smaller call
}

// the same two questions, on a string
boolean isPalindrome(String s, int lo, int hi) {
    if (lo >= hi) return true;              // Q1: 0 or 1 chars left
    if (s.charAt(lo) != s.charAt(hi)) return false;
    return isPalindrome(s, lo + 1, hi - 1); // Q2: trust the middle
}
What it prints
sumFrom([3,1,4], 0)
  3 + sumFrom([3,1,4], 1)
      1 + sumFrom([3,1,4], 2)
          4 + sumFrom([3,1,4], 3)
              0            <- base case
          = 4
      = 5
  = 8

isPalindrome("racecar", 0, 6) -> true
Read the trace bottom-up. The answers only become real on the way back out - which is why the base case is what starts the whole thing returning.

Recap, and where this goes next

You now have three things: a way to write recursion (the two questions), a way to read it (the stack of frames, newest on top), and an understanding of why it can crash rather than merely slow down.

The stack overflow is worth holding onto. Every call keeps its own frame alive until it returns, so a recursion a hundred thousand deep needs a hundred thousand live frames - and the call stack is small and fixed. That is a hard limit, not a performance problem, and chapter 7 shows how to escape it by carrying your own stack on the heap.

Next lesson turns recursion into backtracking: the same two questions, plus the discipline of undoing a choice when it fails. After that, recursion mostly disappears as a topic and shows up as the natural way to write everything else - which is the point.

Remember these
  • Write the base case first; a missing one is an infinite loop
  • Trust the smaller call - do not trace it mentally
  • Each live call holds a frame; deep recursion overflows the stack
  • The answers assemble on the way BACK OUT, not on the way in
  • Trees, graphs, backtracking and DP are all this, reused

Your turn

Reading is not learning. Open each one in NeonFlow and watch your own code run, step by step.

Problem 1

Answer the two questions

Write a recursive method that reverses a string. Do not plan the whole thing - answer the two questions only. When is it trivially over? and if the smaller call works, how do I use it? Then watch the frames stack and unwind in NeonFlow.

Starter
java
public class ReverseIt {
    static String reverse(String s) {
        // Q1: what string is ALREADY reversed, with no work?
        // TODO: base case

        // Q2: if reverse() works on a shorter string, how do you build
        //     the answer for this one?
        // TODO: recursive case

        return s;
    }

    public static void main(String[] args) {
        System.out.println(reverse("flame"));   // expect "emalf"
        System.out.println(reverse("a"));       // expect "a"
        System.out.println(reverse(""));        // expect ""
    }
}
Problem 2

Break it on purpose

Delete the base case and run it. You will get a StackOverflowError - the point is to see it happen and read the depth, rather than be told about it. Then put the base case back and watch the same code finish instantly.

Starter
java
public class BreakTheStack {
    static int depth = 0;

    static int countdown(int n) {
        depth++;
        // TODO: the base case is missing. Run it, read the crash,
        //       then add:  if (n <= 0) return 0;

        return countdown(n - 1);
    }

    public static void main(String[] args) {
        try {
            countdown(10);
        } catch (StackOverflowError e) {
            System.out.println("crashed at depth " + depth);
        }
    }
}