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).
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.
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).
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
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
Feel the exponential
In the Fibonacci example, change
fib(25)tofib(30)and thenfib(35). Note how the call count multiplies by about 11 for every 5 added ton(1.618 to the power 5). Then callfibMemo(35)and compare. - 3
Overflow the stack on purpose
Write
static int depth(int n) { return depth(n + 1); }and call it frommain. Look at the start and end of the stack trace, then delete it.terminal$ java Main── expected output ──Exception in thread "main" java.lang.StackOverflowErrorat Main.depth(Main.java:2)at Main.depth(Main.java:2)at Main.depth(Main.java:2)...
Code & diagrams
The indent grows by two spaces per level, so you can see the call stack.
Expected output
factorial(4)
factorial(3)
factorial(2)
factorial(1)
-> 1 (base case)
-> 2
-> 6
-> 24
4! = 24Expected output
fib(25) = 75025 using 242785 calls
fibMemo(25) = 75025 using 49 calls
fibMemo(90) = 2880067194370816120Expected output
sumDigits(4096) = 19
reverse("recursion") = noisrucer
power(2, 40) = 1099511627776
power(3, 13) = 1594323A 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.
public class Main {
static int countDown(int n) {
return countDown(n - 1);
}
public static void main(String[] args) {
countDown(5);
}
}Break #2
Correct recursion, wrong answer: overflow
Call the recursive factorial with 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
StackOverflowErrorisn't a fixed number: it depends on-Xss(or theThreadconstructor'sstackSize), 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. - ▸
StackOverflowErroris anError, not anException. 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):
powerabove recurses ~40 times for exponent 2^40. Recursing onn - 1gives 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.sortfor primitives uses a dual-pivot quicksort with bounded depth, falling back to heap sort if partitions get unbalanced, andTreeMapuses iterative loops for lookups.
Remember this
- 1
A recursive method calls itself. It always has two parts: a base case, a small input it answers directly without recursing (
factorial(1)is1), and a recursive case, which calls itself on a smaller input and builds the answer from that (factorial(n) = n * factorial(n - 1)). - 2
Every call gets its own stack frame with its own copy of the parameters, so
factorial(4)andfactorial(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
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
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
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
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
What are the two parts every recursive method needs, and what goes wrong without each?
Walk through what happens on the call stack when factorial(3) runs.
Why is naive recursive Fibonacci so slow, and how does memoisation fix it?
When would you choose recursion over a loop in Java, and when not?
Does Java optimise tail recursion? What does that mean for your code?
Practice
Write a recursive static int sumTo(int n) that returns 1 + 2 + ... + n. Print sumTo(10) and sumTo(100).
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".
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.
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
-Xsslets 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
StackOverflowErrorand 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.