Command Palette

Search for a command to run...

PHASE 3Beginner ~34 min· topic 7 of 11

Topic 3.7

Recursion

In one line

Recursion is when a method solves a problem by calling itself on a smaller version of the same problem, until it reaches a case small enough to answer directly. Every recursive method needs a base case that stops it and a recursive case that moves toward it.

Think of it like this

Russian nesting dolls. To find the tiny doll in the middle, you open the outer doll and find another doll, smaller. You do the same thing again, and again, until you open one that has no doll inside: that's the end. You never needed a different method for each doll: one action, repeated on a smaller doll each time, plus knowing when to stop.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Recursion
A method calling itself to solve a smaller version of the same problem.
Base case
An input small enough to answer directly, without another recursive call. It's what stops the recursion.
Recursive case
The part that calls the method again with a smaller input and combines the result.
Call stack
The stack of frames for all method calls that have started but not yet returned. Each recursive call adds one frame.
Recursion depth
How many recursive calls are active at the same time, which is how many frames are on the stack.
`StackOverflowError`
The error the JVM throws when a thread's call stack runs out of room, usually from runaway recursion.
Memoisation
Saving the result for each input the first time you compute it, and reusing it instead of recomputing.
Recursion tree
A drawing of every call a recursive method makes, with each call's children below it. It shows how much work is done.

Step by step

01Think in two questions

To write any recursive method, answer two questions. 1. What's the smallest input I can answer immediately? That's the base case. 2. If someone handed me the answer for a slightly smaller input, how would I build the answer for this one? That's the recursive case.

For factorial: the smallest input is 1 (answer 1). If I already knew factorial(n - 1), then factorial(n) is n * factorial(n - 1).

The hardest part for beginners is to trust the recursive call: don't try to trace every level in your head. If the base case is right and each step is right, the whole thing is right (this is proof by induction).

Main.javawhole filejava
static long factorial(int n) {
    if (n <= 1) {                    // base case
        return 1;
    }
    return n * factorial(n - 1);     // recursive case: a smaller problem
}

02Watch the call stack grow and shrink

factorial(3) can't finish until it knows factorial(2), which waits for factorial(1). So three frames sit on the stack at once, each paused at the multiplication.

factorial(1) hits the base case and returns 1. Its frame is removed. factorial(2) resumes, computes 2 * 1 = 2 and returns. Then factorial(3) computes 3 * 2 = 6.

The work happens on the way back up: going down only sets up the pending multiplications.

Watch the call stack grow and shrinkdiagram
Rendering diagram…

03Each frame has its own n

All three calls have a parameter called n, but they don't share it. Each call's n lives in its own frame: 3, 2 and 1 exist at the same time.

This is pass-by-value (Topic 3.3) and frame lifetime (Topic 3.6) at work. It's also why recursion can be memory-hungry: each level costs a frame, typically tens to a few hundred bytes.

04No base case: StackOverflowError

Forget the base case, or write one that's never reached (countDown(n - 2) with a base case of n == 0 and an odd start), and the calls go on until the thread's stack is full. The JVM then throws java.lang.StackOverflowError.

The stack trace shows the same line repeated, by default up to 1024 lines (the JVM's -XX:MaxJavaStackTraceDepth). A wall of identical at lines is the signature of runaway recursion.

The default stack size per thread is typically 1 MB on 64-bit platforms; you can change it with -Xss (for example java -Xss4m Main), but that only moves the limit. If depth grows with input size, convert the recursion to a loop.

05When recursion repeats work: Fibonacci

fib(n) = fib(n - 1) + fib(n - 2) with fib(0) = 0 and fib(1) = 1 is correct but slow. fib(5) computes fib(3) twice and fib(2) three times, and the waste doubles at each level: about 1.6 to the power n calls in total.

The fix is memoisation: keep an array of answers. Before computing fib(n), check whether it's stored. Now each value is computed once, and the number of calls is linear in n. You'll meet this as top-down dynamic programming in the DSA course (/dsa/dp-foundations).

When recursion repeats work: Fibonaccidiagram
Rendering diagram…

06Recursion vs loops

Anything recursive can be written with a loop (plus, if needed, your own stack data structure), and vice versa. Loops use constant stack space and are usually a bit faster in Java.

Choose recursion when the data is recursive (trees, nested structures), when the problem splits into independent halves (merge sort, quicksort, /dsa/sorting), or for backtracking searches (/dsa/backtracking). Choose a loop for simple linear repetition like summing a list or counting down.

07A checklist for every recursive method

1. Is there a base case, and is it checked before the recursive call? 2. Does every recursive call move strictly closer to the base case? 3. Is the maximum depth safe for the largest input (a few thousand at most)? 4. Does it recompute the same subproblem many times (if so, memoise)?

Also watch numeric limits: factorial(21) overflows long silently (Topic 1.3), so the recursion can be logically perfect and still print garbage.

Try it yourself

  1. 1

    Trace before you run

    Change the call to factorial(5, ""). Write out the full expected trace on paper first: how many lines, and what's the deepest indent? Then run and compare.

  2. 2

    Feel the exponential

    In the Fibonacci example, change fib(25) to fib(30) and then fib(35). Note how the call count multiplies by about 11 for every 5 added to n (1.618 to the power 5). Then call fibMemo(35) and compare.

  3. 3

    Overflow the stack on purpose

    Write static int depth(int n) { return depth(n + 1); } and call it from main. Look at the start and end of the stack trace, then delete it.

    terminal
    $ java Main
    ── expected output ──
    Exception in thread "main" java.lang.StackOverflowError
    at Main.depth(Main.java:2)
    at Main.depth(Main.java:2)
    at Main.depth(Main.java:2)
    ...

Code & diagrams

Factorial with a visible trace New tab

The indent grows by two spaces per level, so you can see the call stack.

Sign in to run this example in your browser.

Expected output

factorial(4)
  factorial(3)
    factorial(2)
      factorial(1)
      -> 1 (base case)
    -> 2
  -> 6
-> 24
4! = 24
Naive vs memoised Fibonacci: count the calls New tab
Sign in to run this example in your browser.

Expected output

fib(25) = 75025 using 242785 calls
fibMemo(25) = 75025 using 49 calls
fibMemo(90) = 2880067194370816120
Three small recursions New tab
Sign in to run this example in your browser.

Expected output

sumDigits(4096) = 19
reverse("recursion") = noisrucer
power(2, 40) = 1099511627776
power(3, 13) = 1594323
The same factorial as a loopjava

A fragment: constant stack space, no risk of StackOverflowError.

static long factorialLoop(int n) {
    long result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the base case

Write a countdown that always recurses.

Main.javawhole filejava
public class Main {
    static int countDown(int n) {
        return countDown(n - 1);
    }
    public static void main(String[] args) {
        countDown(5);
    }
}
terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.StackOverflowError
at Main.countDown(Main.java:3)
at Main.countDown(Main.java:3)
at Main.countDown(Main.java:3)
...
The same line repeats about 1024 times; the trace is cut off at the JVM's stack trace depth limit.

Break #2

Correct recursion, wrong answer: overflow

Call the recursive factorial with 21.

terminal
$ java Main
── what you'll see ──
factorial(20) = 2432902008176640000
factorial(21) = -4249290049419214848
From `System.out.println("factorial(20) = " + factorial(20));` and the same for 21.

Myth vs fact

Myth

Recursion is always slower and worse than loops.

Fact

For tree-shaped problems recursion is the clearest correct solution, and the cost per call is small. The real risks are depth (stack overflow) and repeated work, both of which you can check for.

Myth

A tail-recursive method in Java runs in constant stack space.

Fact

The JVM doesn't do tail-call elimination. Every call gets a new frame, tail position or not.

Myth

If my recursive method overflows, I just need a bigger stack.

Fact

-Xss only moves the limit. If the depth grows with the input, rewrite it as a loop or reduce depth (for example by halving the problem each step).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Recursion depth before StackOverflowError isn't a fixed number: it depends on -Xss (or the Thread constructor's stackSize), the frame size of each method (more locals, bigger frames), and whether the method is interpreted or JIT-compiled (compiled frames are often smaller). Never rely on a measured depth in production code.

  • ▸

    StackOverflowError is an Error, not an Exception. It can be caught, and frameworks sometimes do, but the stack may have unwound through code that was half-way through updating state (for example a lock or a data structure), so catching it to continue is risky.

  • ▸

    Divide-and-conquer keeps depth at about log2(n): power above recurses ~40 times for exponent 2^40. Recursing on n - 1 gives depth n. That difference is why recursive merge sort is fine on millions of elements while a recursive linked-list traversal is not.

  • ▸

    Java's own JDK avoids deep recursion in core libraries: Arrays.sort for primitives uses a dual-pivot quicksort with bounded depth, falling back to heap sort if partitions get unbalanced, and TreeMap uses iterative loops for lookups.

Remember this

  1. 1

    A recursive method calls itself. It always has two parts: a base case, a small input it answers directly without recursing (factorial(1) is 1), and a recursive case, which calls itself on a smaller input and builds the answer from that (factorial(n) = n * factorial(n - 1)).

  2. 2

    Every call gets its own stack frame with its own copy of the parameters, so factorial(4) and factorial(3) don't interfere. The calls pile up on the call stack until the base case returns, then they finish in reverse order, each using the result of the call above it.

  3. 3

    If the base case is missing or never reached, the calls never stop, the stack fills up, and the JVM throws StackOverflowError. The default stack holds thousands of frames, not millions, so deep recursion on large inputs fails even when the logic is right.

  4. 4

    Java does not optimise tail calls: even a recursive call in the last position uses a new frame. So any recursion whose depth grows with the input size (like recursing once per element of a long list) is risky in Java; a loop is safer there.

  5. 5

    Some recursions repeat work. Naive Fibonacci (fib(n - 1) + fib(n - 2)) calls itself an exponential number of times. Storing answers you've already computed (memoisation) brings it down to a linear number of calls. That idea is the start of dynamic programming.

  6. 6

    Recursion shines when the problem is naturally self-similar: trees, nested folders, divide-and-conquer sorts, backtracking searches. The DSA course's Recursion module (/dsa/recursion) trains the thinking pattern in depth: base cases, trusting the recursive call, and recursion trees.

Explain it without notes

01

What are the two parts every recursive method needs, and what goes wrong without each?

02

Walk through what happens on the call stack when factorial(3) runs.

03

Why is naive recursive Fibonacci so slow, and how does memoisation fix it?

04

When would you choose recursion over a loop in Java, and when not?

05

Does Java optimise tail recursion? What does that mean for your code?

Practice

01

Write a recursive static int sumTo(int n) that returns 1 + 2 + ... + n. Print sumTo(10) and sumTo(100).

02

Write a recursive static boolean isPalindrome(String s) that compares the first and last characters and recurses on the middle. Test "level", "racecar" and "java".

03

Write a recursive static int countOccurrences(int[] arr, int index, int target) that counts how many times target appears from index to the end. Test with {1, 3, 1, 1, 5} and target 1.

04

Write a recursive static int gcd(int a, int b) using Euclid's rule: gcd(a, 0) = a, otherwise gcd(b, a % b). Print gcd(48, 18) and gcd(17, 5).

Trade-offs

  • ↔

    Recursion expresses tree and divide-and-conquer logic clearly, but each level costs a stack frame and Java has no tail-call optimisation. Linear-depth recursion over large inputs should be a loop.

  • ↔

    Memoisation turns exponential recursion into linear time, at the cost of memory for the table of answers.

  • ↔

    Raising -Xss lets deeper recursion run, but it applies to every thread and multiplies memory use in programs with many threads.

Done when you can

  • Done when you can write a recursive method with a correct base case and a recursive case that shrinks the input.

  • Done when you can draw the call stack for a small recursive call and say when each frame returns.

  • Done when you can recognise StackOverflowError and its causes.

  • Done when you can explain why naive Fibonacci is exponential and memoise it.

  • Done when you can decide between recursion and a loop for a given problem.