08

Recursion

Solve a big problem by trusting a smaller copy of itself — and know exactly where it stops.

beginner~65 min5 simulations6 problems

▶ guided lesson⚡ 1-minute summary↻ 14 recall cards

before this: Big-O · Java Toolkit

01 · why it exists

Why this exists, and the weight it carries

since1960

Recursion

John McCarthy (Lisp); the ALGOL 60 committee, report edited by Peter Naur

Recursion was a mathematical idea first; 1960 is when two landmark languages built it in. In April 1960, McCarthy's Lisp paper built a whole language around recursive functions. One month later, the ALGOL 60 report allowed a procedure to call itself — through one short sentence added late, after a phone call to its editor, Peter Naur. In Naur's memory the caller was van Wijngaarden, "speaking also for" Dijkstra; in Dijkstra's, it was Dijkstra himself. Maarten van Emden later retold both versions. Almost every language since, Java included, lets a method call itself.

the problem it solvedWriting code for nested, self-similar data — expressions, lists of lists, trees — in the same shape as the data.

its jobs today

  • File systems: folder size, copy-folder and delete-folder walk sub-folders recursively (or with an explicit stack).
  • Compilers: javac parses Java with a hand-written recursive-descent parser — one method per grammar rule, calling each other.
  • Trees everywhere: rendering the DOM, walking JSON, and every tree or graph DFS later in this course.

the weight it carries

Illustrative: a project folder 10 levels deep holding 10⁵ files. A recursive walk makes one call per folder and touches each file once — O(files + folders) — while the call stack never holds more than about 10 frames, one per level.

without it

Nested data would need a hand-managed stack everywhere; a 5-line tree method becomes 25 lines of bookkeeping.

when · why · how

when
The input is nested or branching (trees, folders, expressions), or the definition is self-similar (row n is built from row n − 1).
why
The code mirrors the data, so it's short and provable by induction. Cost: time = calls × work per call; space = depth — O(n) for a chain, O(log n) when halving.
how
A method with a base case (if (n <= 1) return n;) and a call on a strictly smaller input; carry extra state (indices, accumulators) in a private helper's parameters.

02 · plain english

The idea in 30 seconds

A recursive method solves a problem by calling itself on a smaller input, then using that answer.

Every recursion needs a base case (small enough to answer directly) and a recursive case that moves towards it.

Each call gets its own frame on the call stack; the frames pile up on the way down and resolve on the way back up.

picture this

File Explorer working out a folder's size: size of Projects = its own files + the size of each sub-folder — and each sub-folder answers the same question the same way. A folder with no sub-folders is the base case; the totals flow back up to the top.

03 · remember

What you must know

  • Two parts, always: a base case that returns without recursing, and a recursive case that makes the input strictly smaller.
  • The leap of faith: assume the call on the smaller input already works. Your only job is to use its answer for the current input.
  • The call stack stores one frame per active call (its parameters and local variables). Depth of recursion = extra space used.
  • Cost = number of calls × work per call. Draw the recursion tree: a chain is O(n), halving is O(log n), two branches per level is O(2ⁿ).
  • Overlapping sub-problems (naive Fibonacci) make recursion exponential. Memoization — caching each answer — makes it linear. That's the doorway to dynamic programming.
  • Java has no tail-call optimization: every call pushes a frame, so very deep recursion throws StackOverflowError. Loops or an explicit ArrayDeque stack avoid it.
  • Anything recursive can be written with a loop + explicit stack, and vice versa. Pick recursion when the data is nested or branching (trees, choices); pick loops for simple linear walks.
  • Backtracking is recursion that makes a choice, recurses, then undoes the choice — the engine behind "generate all" problems.

04 · see it

Play with it in 3D

Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.

sim

The call stack, frame by frame

factorial(n): frames stack up until the base case, then each one finishes with a multiplication on the way back.

loading simulation…
sim

Naive Fibonacci — the exploding recursion tree

Red nodes are sub-problems that were already solved somewhere else in the tree. That waste is why it's O(2ⁿ).

loading simulation…
sim

Memoization collapses the tree

The memo array fills from small to large; every repeat becomes a one-step lookup, so only 2n − 1 calls remain.

loading simulation…
sim

Halving the input — O(log n) recursion

pow(x, n) asks for pow(x, n/2) once and squares it. The call chain is only as long as n has binary digits.

loading simulation…
sim

Recursion as a tree of choices (backtracking teaser)

Each call chooses "(" or ")". Choices that would break the rules are never explored.

loading simulation…

05 · cost

Time & space, explained

Classic recursions and their cost
TimeStack spacewhy
factorial(n)O(n)O(n)One call per value n, n−1, …, 1; all of them wait on the stack at the deepest point.
Naive fib(n)O(2ⁿ)O(n)Two calls per call → the tree roughly doubles per level (tightly ≈ 1.618ⁿ). Depth is only n, so space stays linear.
Memoized fib(n)O(n)O(n)Each n from 0..n is computed once (2n − 1 calls); the memo array and stack are both size n.
Iterative fib(n)O(n)O(1)Same n additions, but just two variables and no call stack.
Fast power pow(x, n)O(log n)O(log n)n halves every call, so there are about log₂ n calls — each waiting on the stack.
Reverse a linked list recursivelyO(n)O(n)One call per node, and every call stays on the stack until the end is reached.
Generate all strings of length m from 2 choicesO(2ᵐ · m)O(m)2ᵐ leaves, each copied in O(m); the stack holds one frame per character.
Read the cost straight from the recurrence
Calls / total workExamplewhy
f(n) calls f(n − 1) once, O(1) workO(n)factorial, reverse listA single chain of n calls.
f(n) calls f(n / 2) once, O(1) workO(log n)fast power, binary searchYou can halve n only log₂ n times before reaching 1.
f(n) calls f(n − 1) twiceO(2ⁿ)Tower of Hanoi; naive fib (≤ this)Each level doubles the number of calls: 1, 2, 4, … 2ⁿ. Naive fib calls f(n − 1) + f(n − 2), so it is bounded by this (tightly ≈ 1.618ⁿ).
f(n) calls f(n / 2) twice + O(n) workO(n log n)merge sortlog n levels, and every level does O(n) work in total.
Any recursion with memoizationO(states × work per state)memo fib: n states × O(1)Each distinct input is solved once; repeats cost O(1).

06 · java

Raw vs Collections Framework

Recursion vs a plain loop

javarecursivejava.util.*
class Factorial {    /** n! = n × (n−1)!  and  0! = 1! = 1. */    static long factorial(int n) {        if (n <= 1) return 1;              // base case: stop here        long smaller = factorial(n - 1);   // trust the smaller call        return n * smaller;                // combine on the way back    }}
  • Both are O(n) time. The recursive one also uses O(n) stack frames; the loop uses O(1).
  • Use long for factorials: 13! already overflows int, and 20! is the largest that fits in a long.
  • If a recursion only passes values down a straight line, a loop is usually simpler and safer in Java.

The hidden call stack vs an explicit ArrayDeque

javarecursive (JVM stack)java.util.*
record Folder(String name, int files, List<Folder> children) {} class CountRecursive {    /** Files in this folder + files in every sub-folder. */    static int countFiles(Folder folder) {        int total = folder.files();                 // this folder's own work        for (Folder child : folder.children()) {            total += countFiles(child);             // same problem, smaller input        }        return total;                               // no children → loop is skipped: that's the base case    }}
  • Recursion uses the JVM's call stack for you. An explicit Deque does the same job on the heap, which is far larger — no StackOverflowError on deep inputs.
  • Use ArrayDeque with push/pop/peek. The old java.util.Stack class is synchronized and extends Vector — slower and discouraged.
  • The explicit-stack version may visit children in a different order. If order matters, push children in reverse.

Memoization: int[] vs HashMap

javaint[] memojava.util.*
class Solution {    public int fib(int n) {        int[] memo = new int[n + 1];        Arrays.fill(memo, -1);                // -1 means "not solved yet"        return solve(n, memo);    }     private int solve(int n, int[] memo) {        if (n <= 1) return n;        if (memo[n] != -1) return memo[n];    // solved before → reuse        memo[n] = solve(n - 1, memo) + solve(n - 2, memo);        return memo[n];    }}
  • When the inputs are small non-negative integers, an int[]/long[] memo is fastest: no boxing, O(1) indexing.
  • Use a HashMap when the state is sparse, negative, or a combination of values (e.g. a String or List key).
  • Don't use computeIfAbsent with a function that recursively updates the same HashMap — since Java 9 it can throw ConcurrentModificationException. Use get + put.
  • Fill the memo with a sentinel (-1) that can never be a real answer, so 0 can be cached too.

Tail recursion — and why Java doesn't save you

javatail call vs loopjava.util.*
class TailRecursion {    /** Tail call: the recursive call is the very last thing — nothing waits for its result. */    static long factorial(int n, long acc) {        if (n <= 1) return acc;        return factorial(n - 1, acc * n);           // Java still pushes a new frame (no TCO)    }     /** What a tail call *could* be turned into — in Java you must do it yourself. */    static long factorialLoop(int n) {        long acc = 1;        while (n > 1) {            acc *= n;            n--;        }        return acc;    }}
  • A tail call is a recursive call that's the very last action — nothing is left to do after it returns.
  • Some languages turn tail calls into loops (Scheme always; Scala for self-calls, with @tailrec to guarantee it; Kotlin with tailrec). The JVM and javac do not, so the stack still grows by one frame per call.
  • In Java, if you write a tail-recursive method, you might as well write the loop — it's a mechanical rewrite.
Java API you will actually use
calldoescost
Deque<T> st = new ArrayDeque<>()Explicit stack to replace deep recursionO(1) per op (amortized)
st.push(x) / st.pop() / st.peek()Push, remove, look at the topO(1)
map.get(k) / map.put(k, v)Read / write a memo entryO(1) average
Arrays.fill(memo, -1)Mark every memo slot as "not solved yet"O(n)
sb.append(c) / sb.deleteCharAt(sb.length() - 1)Make a choice and undo it while backtrackingO(1) amortized
Integer.bitCount(x)Number of 1-bits (used in K-th Symbol)O(1)
new Thread(null, task, "deep", 1L << 26)Run deep recursion on a thread with a bigger stack (a hint to the JVM)—
java -Xss8m MainJVM flag: bigger default thread stack size—

Nested / branching data (trees, folders, JSON)

→ Recursion

The code mirrors the shape of the data — short and easy to prove correct.

Linear walk over n up to 10⁵–10⁶

→ A loop

Same time, O(1) space, no StackOverflowError risk.

Deep tree/graph DFS (depth may reach 10⁵)

→ ArrayDeque explicit stack

Heap memory instead of the limited thread stack.

Recursion that repeats the same sub-problems

→ Memoization (int[] or HashMap)

Turns exponential time into states × work per state.

Generate all combinations / arrangements

→ Recursion + backtracking

Each call is one choice; undo it and try the next.

07 · recognise

Spot the pattern

The answer for n is defined using the answer for n − 1, n / 2, or a sub-part

Write the recurrence first, then the base case

Nested or tree-shaped input (trees, folders, expressions, nested lists)

Recursion on each child, combine the results

"Generate all", "list every", "all combinations / permutations / subsets"

Recursion with choices → backtracking

Same arguments show up again and again in the recursion tree

Add a memo → dynamic programming

Work from the end of a linked list back to the front

Recurse to the end, act while unwinding

Huge exponent / repeated multiplication (xⁿ, matrix powers)

Divide and conquer: halve n each call

08 · traps & tricks

Mistakes to avoid, tricks to use

traps

  • Missing or unreachable base case → infinite recursion → StackOverflowError. Check that every path shrinks the input towards the base case.
  • Forgetting to return the recursive result (fib(n-1) + fib(n-2); without return doesn't even compile; helper(x); instead of return helper(x); silently drops the answer).
  • Calling the same sub-problem twice: pow(x, n/2) * pow(x, n/2) is O(n), not O(log n). Store it in a variable.
  • Exponential blow-up from overlapping sub-problems (naive Fibonacci). Spot repeats in the tree and memoize.
  • Recursing over 10⁵ linked-list nodes or a skewed tree in Java can overflow the stack — prefer the iterative version or an explicit stack.
  • s.substring(1) or new ArrayList<>(list) on every call copies O(n) data per level → O(n²). Pass indices instead.
  • Negating Integer.MIN_VALUE overflows (it stays negative). Widen to long before -n.
  • Sharing a mutable StringBuilder/List across calls without undoing the change after the recursive call (the classic backtracking bug).

tricks

  • Write recursion in three lines of English first: base case, smaller problem, how to combine. Then code it.
  • Chalo, dry-run karte hain — with the smallest inputs first (0, 1, 2). If those work and each call shrinks the input, the rest follows by induction.
  • State the space cost out loud in interviews: "this recursion is O(n) deep, so O(n) stack space" — many candidates forget it.
  • If an interviewer asks "can you do it without recursion?", reach for a loop with variables (linear) or an ArrayDeque (branching).
  • Draw the recursion tree for n = 4 or 5. Count the nodes and the levels — that is the time complexity.
  • Use a private helper with extra parameters (indices, accumulators) and keep the public method signature clean.

09 · why & how

Questions you might be too shy to ask

How do I write a recursive function without getting lost in all the calls?

Don't trace every call in your head. Decide (1) the base case, (2) what smaller input to call yourself on, and (3) how to turn that smaller answer into yours. Then trust the smaller call — that's mathematical induction. Trace only small inputs to double-check.

How do I pick the base case, and how do I know it's enough?

Pick the smallest input you can answer without thinking: an empty range, n = 0, a null node. Then check every recursive call: does it always land on a base case? Fibonacci calls fib(n - 2), which can jump from 1 to −1, so it needs both 0 and 1 covered. if (n <= 1) return n; does exactly that.

What actually happens in memory when a method calls itself?

Each call gets a new stack frame with its own copies of the parameters and local variables. The caller pauses at the call line. When the callee returns, its frame is popped and the caller resumes exactly where it stopped. The simulation above shows this stack in 3D.

Does code written after the recursive call run before or after the deeper calls?

After. The line after the call waits until the whole deeper chain has returned. So print(n); f(n - 1); prints 3, 2, 1 (on the way down). And f(n - 1); print(n); prints 1, 2, 3 (on the way back up). Same calls, different moment.

When the base case says return, does the whole recursion stop?

No. return ends only the current call and hands its value to the caller one level up. That caller then continues from its own call line. factorial(1) returns 1 to factorial(2), which multiplies and returns 2 to factorial(3), and so on up to the top.

Recursion or a loop — which should I use in an interview?

Use whichever is clearer for the shape of the problem. Linear walks (sum an array, reverse a string): a loop. Trees, nested data, "generate all": recursion. Then mention the trade-off: recursion costs O(depth) stack space and can overflow for very deep inputs in Java.

Why do solutions add a private helper instead of calling the LeetCode method itself?

The method LeetCode gives you often has no room for the extra state you need. reverseString(char[] s) has no left and right. So the public method just calls reverse(s, 0, s.length - 1), and that private helper recurses with the indices. Simple hai: the helper's parameters are the "smaller problem".

Why does naive Fibonacci get slow so quickly?

Every call makes two more calls, so the number of calls nearly doubles with each +1 to n. fib(30) makes over 2.6 million calls, and most of them recompute small values — fib(5) alone is computed 121,393 times. Caching each answer once (memoization) brings it down to 59 calls.

What is StackOverflowError and how deep can Java recurse?

The JVM gives each thread a fixed-size stack (the JDK 17 docs list 1024 KB by default on Linux/x64 and macOS; on Windows it depends on virtual memory; -Xss changes it). Each call uses some of it, so depending on how big each frame is you get roughly thousands to tens of thousands of nested calls. Go deeper and the JVM throws StackOverflowError. It's an Error, not something you should catch in normal code — fix the depth instead.

Does Java optimize tail recursion like some other languages?

No. Neither javac nor the JVM performs tail-call elimination, so a tail-recursive method still uses one frame per call. If you need it to run in constant stack space, rewrite it as a loop yourself.

How do I find the time complexity of a recursive function?

Count the calls and multiply by the work done in each one. Drawing the recursion tree helps: a chain of n calls → O(n); halving each time → O(log n); two calls per level for n levels → O(2ⁿ). Divide-and-conquer recurrences like T(n) = 2T(n/2) + O(n) → O(n log n) (the Master theorem formalises this).

Is memoization the same thing as dynamic programming?

Memoization is top-down dynamic programming: keep the natural recursion, cache results. Bottom-up DP fills a table from the smallest sub-problems with loops (like the iterative Fibonacci). Same idea, different direction — you'll go deep on both in the DP chapter.

Pow(x, n): when n is odd, n / 2 drops the remainder. How is the answer still right?

Integer division rounds down here: 7 / 2 = 3. So x⁷ = x³ · x³ · x — the extra × x puts back the piece that was dropped. For even n there's nothing to put back: x⁸ = x⁴ · x⁴. That's why the code checks n % 2 before choosing half * half or half * half * x.

Swap Nodes in Pairs: why call swapPairs(second.next) before setting second.next = head?

Because second.next is your only arrow to the rest of the list (node 3 onwards). If you overwrite it first, the rest is lost. So: save the rest by recursing on it (head.next = swapPairs(second.next)), then point second back at head. Draw 1 → 2 → 3 → 4 and move one arrow at a time.

10 · practice

Interview problems

Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.

  1. #509
    Fibonacci Numbermust-do
    EasyTCSMicrosoftBloombergAmazonGoogle+1
  2. #344
    Reverse String
    EasyTCSBloombergAmazonMicrosoftGoogle+1
  3. #24
    Swap Nodes in Pairs
    MediumBloombergMicrosoftUberAmazonGoogle+1
  4. #50
    Pow(x, n)must-do
    MediumMetaLinkedInBloombergMicrosoftGoogle+1
  5. #779
    K-th Symbol in Grammar
    MediumGoogleGoldman SachsMicrosoftAmazonBloomberg
  6. #427

Also practise this here

Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.

  1. #206
    Reverse Linked List
    Easyin Linked Lists

    Trust the recursion: reverse the rest, then hook the head on — its recursive approach has a call-by-call dry run.

  2. #21
    Merge Two Sorted Lists
    Easyin Linked Lists

    "The smaller head comes first; the rest is a smaller merge" — a six-line recursive splice.

  3. #22
    Generate Parentheses
    Mediumin Backtracking

    Recursion as a decision tree: two choices per call, pruned by the open/close counts.

11 · revise

The whole chapter on one screen

9 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.

  1. what it is

    A method that calls itself on a smaller input. Two parts: a base case that returns directly, and a recursive case that moves towards it.

  2. use it when

    Nested or branching data (trees, folders, JSON), self-similar definitions (row n from row n − 1), and "generate all" problems.

  3. template

    if (base) return answer; → smaller = solve(smaller input); → return combine(smaller); Then trust the smaller call.

  4. cost

    Time = calls × work per call. Space = deepest chain of calls. Chain → O(n) · halving → O(log n) · two calls on n − 1 → O(2ⁿ).

  5. trap

    No reachable base case → frames pile up → StackOverflowError. Java has no tail-call optimization, so depth 10⁴–10⁵ is risky.

  6. trap

    pow(x, n/2) * pow(x, n/2) makes two calls → O(n). Store half once → O(log n). Same sub-problem twice = wasted work.

  7. trap

    Backtracking with a shared StringBuilder or List: undo the change after the recursive call, or later branches see leftovers.

  8. pro tip

    Repeated sub-problems? Add a memo (int[] filled with −1, or a HashMap) so each state is solved once. That's top-down DP.

  9. pro tip

    Too deep for the stack? Rewrite with a loop, or an explicit ArrayDeque stack that lives on the heap.

12 · recall

Flip, remember, repeat

card 1 / 14
recall

Which two parts must every recursive method have?

tap or Space to flip ↻

Question 1: Which two parts must every recursive method have?

13 · check yourself

Quick quiz

  1. What happens if a recursive method has no reachable base case?

  2. factorial(5) is running and has reached factorial(1). How many factorial frames are on the stack?

  3. Time complexity of naive recursive fib(n)?

  4. `return pow(x, n/2) * pow(x, n/2);` (n even). What's its time complexity?

  5. Recursive reverse of a 5000-node linked list. Extra space?

  6. Does Java turn a tail-recursive method into a loop?

  7. Memoized fib(n) makes how many calls (n ≥ 1)?