Chapter 4: Recursion
Introduction
Learning Outcomes
Define a problem by a base case and a recursive step, and write it in Java.
Trace a recursive method frame by frame to find its output and return value.
Tell linear, binary and multiple recursion and backtracking apart, with examples.
Write a recurrence, use the call tree to find time and space cost, and convert to a loop when needed.
What Is Recursion
Recursion in Everyday Life
You are in a long line and want to know your place, but you can't step out and count from the front.
What Is Recursion
Recursion
- Recursion: a method calling itself on a smaller input.
- Base case: solved directly, without recursion. Without one the calls never end →
StackOverflowError. - Recursive step: shrinks the problem toward the base case and builds the answer from the smaller problem's answer.
- Each call pushes a new frame on the call stack, with its own n.
What Is Recursion
Thinking Recursively
- Base case: what is the smallest input, and what is its answer? Recursion stops here.
- Reduce: how does the problem shrink to a smaller one of the same kind? (n − 1, n / 2, i + 1 …)
- Combine: how do you build the big answer from the small one?
Assume the smaller problem is solved correctly and think about one step only. If the base case is right and every step is right, the whole chain is right — the logic of induction.
void countdown(int n) { if (n == 0) { // base case System.out.println("Done"); return; } System.out.println(n); countdown(n - 1); // recursive step } countdown(3); // in main
countdown(n): print n down to 1, then “Done”.
What Is Recursion · Step through the code
Code Trace: countdown(3)
main calls countdown(3). A new frame is about to be pushed.
n = 3 ≠ 0: not the base case. Print 3, then call countdown(2); this frame waits.
n = 2 ≠ 0: not the base case. Print 2, then call countdown(1); this frame waits.
n = 1 ≠ 0: not the base case. Print 1, then call countdown(0); this frame waits.
n = 0: base case. Print “Done”. The stack is at its deepest: main + 4 frames.
return: countdown(0) ends and its frame is popped.
countdown(1) returns from the call on line 7. Nothing is left to do, so it ends too.
countdown(2) returns from the call on line 7. Nothing is left to do, so it ends too.
countdown(3) returns from the call on line 7. Nothing is left to do, so it ends too.
All frames have returned. 4 calls (n = 3, 2, 1, 0); output: 3 2 1 Done.
void countdown(int n) { if (n == 0) { // base case System.out.println("Done"); return; } System.out.println(n); countdown(n - 1); // recursive step } countdown(3); // in main
What Is Recursion
Work Before or After the Call?
void countdown2(int n) { if (n == 0) return; System.out.print(n + " "); countdown2(n - 1); // call after }
countdown2(3) prints 3 2 1
Printing happens as the calls go down.
void countUp(int n) { if (n == 0) return; countUp(n - 1); // call first System.out.print(n + " "); }
countUp(3) prints 1 2 3
Printing happens as the calls return.
countUp(3)waits forcountUp(2)to finish before printing 3; that call in turn waits forcountUp(1)before printing 2.- The first call to finish is the last one started (n = 0), so work done after the call happens in reverse order: 1, 2, 3.
The Call Stack
The Call Stack and Frames
- Every method call gets a stack frame (or just frame) in memory: its parameters, local variables, and the place to resume on return.
- Frames live on the call stack: a call pushes one on top, and returning pops it. Last started, first finished: LIFO.
- Only the top frame runs; the ones below wait for the result of the call above them.
- In recursion, several frames of the same method are on the stack at once, each with its own n.
The Call Stack · Step through the code
Code Trace: factorial(4) and the call stack
main calls factorial(4).
New frame: n = 4. Is 4 ≤ 1? No.
4 · factorial(3): the product needs factorial(3) first, so this frame waits.
New frame: n = 3. Is 3 ≤ 1? No.
3 · factorial(2): the product needs factorial(2) first, so this frame waits.
New frame: n = 2. Is 2 ≤ 1? No.
2 · factorial(1): the product needs factorial(1) first, so this frame waits.
New frame: n = 1. Is 1 ≤ 1? Yes.
Base case: return 1. No more calls; values start coming back.
factorial(1) returned 1 → 2 · 1 = 2, return 2.
factorial(2) returned 2 → 3 · 2 = 6, return 6.
factorial(3) returned 6 → 4 · 6 = 24, return 24.
Prints 24. 4 calls, stack depth at most 4 + main → time and memory O(n).
int factorial(int n) { if (n <= 1) return 1; // base case return n * factorial(n - 1); } // in main: System.out.println(factorial(4));
The Call Stack
Stack Overflow: StackOverflowError
long sumTo(int n) { // 1 + 2 + … + n if (n == 0) return 0; return n + sumTo(n - 1); } // in main: System.out.println(sumTo(10_000)); // 50005000 System.out.println(sumTo(1_000_000)); // error
Exception in thread "main"
java.lang.StackOverflowError
at Example.sumTo(Example.java:3)
at Example.sumTo(Example.java:3)
…
- The call stack is a limited region of memory. Its size depends on the JVM and the operating system and can be changed with the
-Xssoption. - Every frame takes space. With default settings, depth is usually limited to somewhere between a few thousand and a few tens of thousands of calls.
sumTo(10_000)fits;sumTo(1_000_000)fills the stack before reaching the base case.- A loop, or the formula n(n + 1)/2, computes the same sum without trouble.
The Call Stack
Forgetting the Base Case
int f(int n) { return n * f(n - 1); }n = 3, 2, 1, 0, −1, −2 … the calls never stop.
if (n == 1) return 1; return n * f(n - 1);
f(0) → −1 → −2 …: n never becomes 1. n <= 1 catches this input too.if (n <= 1) return 1; return n * f(n);The same input is passed again: there is a base case, but the calls never get closer to it.
StackOverflowError is thrown. Every possible input, including 0 and negative values, must reach a base case in a finite number of steps.Linear Recursion
Linear Recursion
- Each call makes at most one recursive call, so the calls form a chain with no branches.
- A problem of size n reduces to a single problem of size n − 1 (or n / 2).
- Examples: factorial, countdown, summing an array, reversing an array, exponentiation, the length of a linked list.
- Array sum: sum of the first n items = sum of the first n − 1 items + a[n − 1]. Base case: n = 0 (empty prefix) → 0.
int sum(int[] a, int n) { // sum of the first n items if (n == 0) return 0; // base case return sum(a, n - 1) + a[n - 1]; }
Linear Recursion · Step through the code
Code Trace: sum(a, 4)
main calls sum(a, 4): the sum of the array's first 4 items.
n = 4 ≠ 0 → sum(a, 3) must be computed first; a[3] = 2 waits to be added.
n = 3 ≠ 0 → sum(a, 2) must be computed first; a[2] = 6 waits to be added.
n = 2 ≠ 0 → sum(a, 1) must be computed first; a[1] = 3 waits to be added.
n = 1 ≠ 0 → sum(a, 0) must be computed first; a[0] = 4 waits to be added.
n = 0: base case → return 0. The stack is at its deepest: main + 5 frames.
sum(a, 0) returned 0 → 0 + a[0] = 0 + 4; return 4.
sum(a, 1) returned 4 → 4 + a[1] = 4 + 3; return 7.
sum(a, 2) returned 7 → 7 + a[2] = 7 + 6; return 13.
sum(a, 3) returned 13 → 13 + a[3] = 13 + 2; return 15.
Result 15. 5 calls (n = 4, 3, 2, 1, 0): time O(n); at most 5 frames (+ main): memory O(n).
int sum(int[] a, int n) { // sum of the first n items if (n == 0) return 0; // base case return sum(a, n - 1) + a[n - 1]; } // in main: int[] a = {4, 3, 6, 2}; System.out.println(sum(a, 4));
Linear Recursion
Reversing an Array
- Idea: swap the two ends, then reverse the part in between the same way.
- The part is given by the indices of
a[i..j]; the first call isreverse(a, 0, n − 1). - Base case:
i >= j, so 0 or 1 items remain and there is nothing to do. - Each call shortens the part by 2 items: for n items, ⌊n/2⌋ swaps and ⌊n/2⌋ + 1 calls → O(n).
- The array changes in place; the method returns nothing (
void).
Linear Recursion · Step through the code
Code Trace: reverse(a, 0, 5)
main calls reverse(a, 0, 5).
i = 0 < j = 5 → swap a[0] and a[5]: the array is 6 7 4 9 1 2. Then call reverse(a, 1, 4).
i = 1 < j = 4 → swap a[1] and a[4]: the array is 6 1 4 9 7 2. Then call reverse(a, 2, 3).
i = 2 < j = 3 → swap a[2] and a[3]: the array is 6 1 9 4 7 2. Then call reverse(a, 3, 2).
i = 3 ≥ j = 2: base case; nothing is left in the middle. The stack holds main + 4 frames.
The frames return one by one with no work left to do. Result: 6 1 9 4 7 2. 3 swaps, 4 calls.
void reverse(int[] a, int i, int j) { if (i >= j) return; // base: 0 or 1 items int t = a[i]; a[i] = a[j]; a[j] = t; // the ends reverse(a, i + 1, j - 1); // the inner part } // in main: int[] a = {2, 7, 4, 9, 1, 6}; reverse(a, 0, a.length - 1);
Linear Recursion
Exponentiation: Slow and Fast
Slow: xⁿ = x · xⁿ⁻¹, x⁰ = 1
long power(long x, int n) { if (n == 0) return 1; return x * power(x, n - 1); }
Fast: halve the exponent at every step
xⁿ = x · (x(n−1)/2)² (n odd)
2¹⁰ = (2⁵)², 2⁵ = 2 · (2²)²,
2² = (2¹)², 2¹ = 2 · (2⁰)²
| n | power: calls | fastPower: calls |
|---|---|---|
| 10 | 11 | 5 |
| 100 | 101 | 8 |
| 1,000 | 1,001 | 11 |
| 10⁶ | 1,000,001 | 21 |
The slow version makes n + 1 calls: O(n). In the fast version n halves with every call: ⌊log₂ n⌋ + 2 calls, O(log n).
Linear Recursion · Step through the code
Code Trace: fastPower(2, 10)
main calls fastPower(2, 10): 2¹⁰.
n = 10 ≠ 0 → first compute y = fastPower(2, 5) (10 / 2 = 5).
n = 5 ≠ 0 → first compute y = fastPower(2, 2) (5 / 2 = 2).
n = 2 ≠ 0 → first compute y = fastPower(2, 1) (2 / 2 = 1).
n = 1 ≠ 0 → first compute y = fastPower(2, 0) (1 / 2 = 0).
n = 0: base case → return 1 (2⁰ = 1).
y = 1 is back. n = 1 is odd → return 2 · y · y = 2 · 1 · 1 = 2.
y = 2 is back. n = 2 is even → return y · y = 2 · 2 = 4.
y = 4 is back. n = 5 is odd → return 2 · y · y = 2 · 4 · 4 = 32.
y = 32 is back. n = 10 is even → return y · y = 32 · 32 = 1024.
Result 1024. Only 5 calls; the slow power(2, 10) would make 11.
long fastPower(long x, int n) { if (n == 0) return 1; // base case long y = fastPower(x, n / 2); // x^(n/2) if (n % 2 == 0) return y * y; // n even return x * y * y; // n odd } // in main: System.out.println(fastPower(2, 10));
Binary Recursion
Fibonacci Numbers
int fib(int n) { if (n <= 1) return n; // base case return fib(n - 1) + fib(n - 2); }
F(n) = F(n − 1) + F(n − 2) (n ≥ 2)
- Binary recursion: a call makes two recursive calls.
- The calls no longer form a chain but a call tree: each node is a call, and its children are the calls it makes.
- The definition turns straight into code, with two base cases: n = 0 and n = 1.
- The tree computes the same values more than once, and that is what sets the cost.
Binary Recursion · Step through the code
Code Trace: the fib(4) call tree
main calls fib(4).
fib(4): n > 1 → call fib(3) first; fib(2) waits its turn.
fib(3): n > 1 → call fib(2) first; fib(1) waits its turn.
fib(2): n > 1 → call fib(1) first; fib(0) waits its turn.
fib(1): base case → return 1.
Second term: fib(0): base case → return 0.
fib(2) = fib(1) + fib(0) = 1 + 0; return 1.
Second term: fib(1): base case → return 1.
fib(3) = fib(2) + fib(1) = 1 + 1; return 2.
fib(4) calls fib(2) for its second term: fib(2) is computed a second time. First fib(1).
fib(1): base case → return 1.
Second term: fib(0): base case → return 0.
fib(2) = fib(1) + fib(0) = 1 + 0; return 1.
fib(4) = fib(3) + fib(2) = 2 + 1; return 3.
fib(4) = 3. 9 calls: fib(2) was computed twice, fib(1) three times, fib(0) twice.
int fib(int n) { if (n <= 1) return n; // base case return fib(n - 1) + fib(n - 2); } // in main: System.out.println(fib(4));
Binary Recursion
The Cost of Recursion
int fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); }
- factorial: T(n) = T(n − 1) + c → n calls → O(n) time, O(n) stack memory.
- fib: every call makes two calls, so the same subproblems are solved over and over.
- fib(4) → 9 calls; the orange f(2) is computed twice. The number of calls grows exponentially (≈ 1.6ⁿ).
- The fix: memoization or a loop → O(n).
Binary Recursion
Avoiding Repeated Work
| n | fib(n) | calls to fib |
|---|---|---|
| 5 | 5 | 15 |
| 10 | 55 | 177 |
| 20 | 6,765 | 21,891 |
| 30 | 832,040 | 2,692,537 |
| 40 | 102,334,155 | 331,160,281 |
Number of calls = 2 · F(n + 1) − 1: each time n grows by 1, it grows about 1.6 times.
long[] memo = new long[91]; // 0: not computed long fibMemo(int n) { if (n <= 1) return n; if (memo[n] != 0) return memo[n]; memo[n] = fibMemo(n - 1) + fibMemo(n - 2); return memo[n]; }
long fibLoop(int n) { long a = 0, b = 1; // F(0), F(1) for (int i = 0; i < n; i++) { long t = a + b; a = b; b = t; } return a; }
fibMemo(40) makes 79 calls. The loop version runs in O(n) time and O(1) memory.Binary Recursion
Recursive Binary Search
- To find x in a sorted array, look at the middle item; if x is smaller, run the same search on the left half, otherwise on the right half.
- Two base cases: the range is empty (
lo > hi) → −1; the middle item is x → its index. - The code contains two recursive calls, but each call makes only one of them. Unlike Fibonacci there is no tree; the calls form a chain.
- The range halves with every call: T(n) = T(n/2) + c → O(log n) time and O(log n) stack memory.
- At most 5 calls for n = 10, at most 21 for n = 10⁶.
Binary Recursion · Step through the code
Code Trace: binarySearch(a, 23, 0, 9)
main searches a sorted 10-item array for 23: binarySearch(a, 23, 0, 9).
lo = 0 ≤ hi = 9: the range is not empty. mid = (0 + 9) / 2 = 4, a[4] = 16.
23 > 16 → right half: binarySearch(a, 23, 5, 9). The left half is never examined.
lo = 5 ≤ hi = 9: the range is not empty. mid = (5 + 9) / 2 = 7, a[7] = 56.
23 < 56 → left half: binarySearch(a, 23, 5, 6). The right half is never examined.
lo = 5 ≤ hi = 6: the range is not empty. mid = (5 + 6) / 2 = 5, a[5] = 23.
a[5] = 23 = x: base case (found) → return 5.
binarySearch(a, 23, 5, 9) passes the 5 it received straight on with return.
binarySearch(a, 23, 0, 9) passes the 5 on too. No new computation happens on the way back.
Result 5. 3 calls; the range shrank from 10 to 5 to 2 items.
int binarySearch(int[] a, int x, int lo, int hi) { if (lo > hi) return -1; // base: empty range int mid = (lo + hi) / 2; if (a[mid] == x) return mid; // base: found if (x < a[mid]) return binarySearch(a, x, lo, mid - 1); // left half return binarySearch(a, x, mid + 1, hi); // right half } System.out.println(binarySearch(a, 23, 0, 9));
Hanoi and Backtracking
Towers of Hanoi
A classic problem in computer science and mathematics: move a stack of disks from the source peg to the target peg while obeying the rules.
- Each move carries one disk — the top disk of a peg.
- A disk may only go onto a larger disk or an empty peg.
- Goal: using the spare peg, move all disks from the source (A) to the target (C).
Hanoi and Backtracking
Towers of Hanoi: 7 Moves with 3 Disks
Start: 3 disks on peg A (largest at the bottom). Goal: move them all to C.
Move 1: disk 1, A → C.
Move 2: disk 2, A → B.
Move 3: disk 1, C → B.
Move 4: disk 3, A → C.
Move 5: disk 1, B → A.
Move 6: disk 2, B → C.
Move 7: disk 1, A → C. Done: 2³ − 1 = 7 moves.
Hanoi and Backtracking · Step through the code
Code Trace: Recursive hanoi(3, A, B, C)
main calls hanoi(3, 'A', 'B', 'C'). Each box in the tree is a call: n:from→to.
hanoi(3,A,B,C): a new frame is pushed. n = 3 ≠ 1 → first the top 2 disks must move from peg A to peg B.
Line 6: hanoi(2,A,C,B): a new frame is pushed. n = 2 ≠ 1 → first the top disk must move from peg A to peg C.
Line 6: hanoi(1,A,B,C): a new frame is pushed. n == 1 → base case: disk 1, A → C.
return: the frame is popped and control goes back to the caller.
The left subcall is done; only disk 2 is left on peg A → Disk 2: A → B.
Line 8: hanoi(1,C,A,B): a new frame is pushed. n == 1 → base case: disk 1, C → B.
return: the frame is popped and control goes back to the caller.
hanoi(2,A,C,B) ends; its frame is popped.
The left subcall is done; only disk 3 is left on peg A → Disk 3: A → C.
Line 8: hanoi(2,B,A,C): a new frame is pushed. n = 2 ≠ 1 → first the top disk must move from peg B to peg A.
Line 6: hanoi(1,B,C,A): a new frame is pushed. n == 1 → base case: disk 1, B → A.
return: the frame is popped and control goes back to the caller.
The left subcall is done; only disk 2 is left on peg B → Disk 2: B → C.
Line 8: hanoi(1,A,B,C): a new frame is pushed. n == 1 → base case: disk 1, A → C.
return: the frame is popped and control goes back to the caller.
hanoi(2,B,A,C) ends; its frame is popped.
hanoi(3,A,B,C) ends; its frame is popped. 7 calls in the tree = 7 moves = 2³ − 1.
Back in main: all disks are on C. The call stack is at most n = 3 deep (+ main) → memory O(n), time O(2ⁿ).
void hanoi(int n, char from, char via, char to) { if (n == 1) { System.out.println("1: " + from + "→" + to); return; } hanoi(n - 1, from, to, via); System.out.println(n + ": " + from + "→" + to); hanoi(n - 1, via, from, to); } // in main: hanoi(3, 'A', 'B', 'C');
Hanoi and Backtracking
Backtracking
- The solution is built step by step; at each step one of the possible choices is tried.
- Making a choice moves on to the next step: a recursive call.
- When the call returns, the choice is undone and the next option is tried, so every branch starts from the same state.
- If a choice cannot lead to a solution, that branch is abandoned without going deeper.
- Examples: listing subsets or permutations, finding a path through a maze, sudoku, n-queens.
Hanoi and Backtracking
Listing Subsets
- Two decisions per item: take it or leave it. The left branch takes the item (+1, +2, +3); the right branch doesn't.
- Level i decides a[i]; at a leaf (i = n) all decisions are made → s is a subset.
- n items → 2ⁿ leaves = 2ⁿ subsets; 2ⁿ⁺¹ − 1 calls (n = 3: 8 subsets, 15 calls).
- Output order:
[1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []
Hanoi and Backtracking · Step through the code
Code Trace: subsets of {1, 2, 3}
main calls subsets(a, 0, s) with a = {1, 2, 3} and an empty s.
i = 0: take a[0] = 1 (s = [1]) and go down the left branch.
i = 1: take a[1] = 2 (s = [1, 2]) and go down the left branch.
i = 2: take a[2] = 3 (s = [1, 2, 3]) and go down the left branch.
i = 3 = a.length: base case. All decisions are made → print [1, 2, 3].
Back from the left branch. Undo 3 (s = [1, 2]); with the same i, go down the right branch to the subsets without 3.
i = 3 = a.length: base case. All decisions are made → print [1, 2].
Back from the left branch. Undo 2 (s = [1]); with the same i, go down the right branch to the subsets without 2.
i = 2: take a[2] = 3 (s = [1, 3]) and go down the left branch.
i = 3 = a.length: base case. All decisions are made → print [1, 3].
Back from the left branch. Undo 3 (s = [1]); with the same i, go down the right branch to the subsets without 3.
i = 3 = a.length: base case. All decisions are made → print [1].
Back from the left branch. Undo 1 (s = []); with the same i, go down the right branch to the subsets without 1.
i = 1: take a[1] = 2 (s = [2]) and go down the left branch.
i = 2: take a[2] = 3 (s = [2, 3]) and go down the left branch.
i = 3 = a.length: base case. All decisions are made → print [2, 3].
Back from the left branch. Undo 3 (s = [2]); with the same i, go down the right branch to the subsets without 3.
i = 3 = a.length: base case. All decisions are made → print [2].
Back from the left branch. Undo 2 (s = []); with the same i, go down the right branch to the subsets without 2.
i = 2: take a[2] = 3 (s = [3]) and go down the left branch.
i = 3 = a.length: base case. All decisions are made → print [3].
Back from the left branch. Undo 3 (s = []); with the same i, go down the right branch to the subsets without 3.
i = 3 = a.length: base case. All decisions are made → print [].
Done: 8 subsets, 15 calls. The stack never held more than main + 4 frames.
void subsets(int[] a, int i, List<Integer> s) { if (i == a.length) { // base case System.out.print(s + " "); return; } s.add(a[i]); // take a[i] subsets(a, i + 1, s); s.remove(s.size() - 1); // undo subsets(a, i + 1, s); // try without a[i] } // in main: subsets(new int[]{1, 2, 3}, 0, new ArrayList<>());
Hanoi and Backtracking
Listing Permutations
void perm(char[] c, boolean[] used, StringBuilder s) { if (s.length() == c.length) { // base case System.out.print(s + " "); return; } for (int k = 0; k < c.length; k++) { if (used[k]) continue; // letter already used used[k] = true; s.append(c[k]); // choose perm(c, used, s); used[k] = false; // undo s.deleteCharAt(s.length() - 1); } }
- Output:
ABC ACB BAC BCA CAB CBA - Each level chooses one of the remaining letters: 3 · 2 · 1 = 3! = 6 leaves.
- Number of calls: 1 + 3 + 6 + 6 = 16.
used[] and s are shared by all calls, which is why each choice is undone after it has been tried.Recurrence Relations
Recurrence Relations
- T(n): the total work a method does on an input of size n. The cost of a recursive method is written in terms of its own cost on smaller inputs: a recurrence relation.
- Three questions to write one: What does the base case cost (usually a constant c)? How many recursive calls are made, and how much smaller are their inputs? How much work is done outside the calls?
| Method | Calls | Recurrence | Solution |
|---|---|---|---|
| factorial, sum, countdown | one call, n − 1 | T(n) = T(n − 1) + c | O(n) |
| reverse | one call, n − 2 | T(n) = T(n − 2) + c | O(n) |
| binarySearch, fastPower | one call, n / 2 | T(n) = T(n/2) + c | O(log n) |
| hanoi | two calls, n − 1 | T(n) = 2T(n − 1) + c | O(2ⁿ) |
| fib | two calls, n − 1 and n − 2 | T(n) = T(n − 1) + T(n − 2) + c | exponential |
Recurrence Relations
T(n) = T(n − 1) + c
T(n) = T(n − 1) + c
= T(n − 2) + 2c
= T(n − 3) + 3c
…
= T(0) + n · c
= c + n · c → O(n)
- Solve it by unrolling: each step adds another c and lowers n. After n steps we reach the base case.
- The call tree is a chain with no branches: n + 1 calls. The whole chain is on the stack at once → memory is O(n) too.
- Examples: factorial, sum, countdown, slow exponentiation, the length of a linked list. For
reverse, T(n) = T(n − 2) + c: half as many calls, still O(n).
Recurrence Relations
T(n) = T(n/2) + c
T(n) = T(n/2) + c
= T(n/4) + 2c
= T(n/8) + 3c
…
= T(n/2ᵏ) + k · c
We stop when n/2ᵏ = 1, that is, k = log₂ n.
T(n) = T(1) + c · log₂ n → O(log n)
| n | 16 | 1,024 | 1,000,000 |
|---|---|---|---|
| halving chain (calls) | 5 | 11 | 20 |
Examples: binary search, fast exponentiation. A thousand times more input adds only about 10 calls, and since the chain is log n deep, memory is O(log n).
Recurrence Relations
T(n) = 2T(n − 1) + c
- Each call makes two calls of size n − 1, so the number of calls doubles at every level of the call tree.
- Level k has 2ᵏ calls, for levels 0 … n − 1:
1 + 2 + 4 + … + 2ⁿ⁻¹ = 2ⁿ − 1 calls → O(2ⁿ). - Hanoi: exactly 2ⁿ − 1 moves. That is 1,048,575 for n = 20 and about 1.8 · 10¹⁹ for n = 64.
- fib: since T(n − 2) ≤ T(n − 1), T(n) ≤ 2T(n − 1) + c → upper bound O(2ⁿ); the true growth is about 1.6ⁿ.
Recurrence Relations
Space Cost: The Deepest Chain of Calls
- A call finishes before its sibling starts, so the stack only ever holds one path from the root to some node.
- Memory = deepest chain of calls × frame size. What matters is the depth, not the total number of calls.
fib(4): 9 calls, but never more than 4 frames at once (+ main).
| Call | Total calls | Deepest chain | Time | Space |
|---|---|---|---|---|
| factorial(4) | 4 | 4 | O(n) | O(n) |
| fib(40) | 331,160,281 | 40 | exponential | O(n) |
| hanoi(3, …) | 7 | 3 | O(2ⁿ) | O(n) |
| subsets, n = 3 | 15 | 4 | O(n · 2ⁿ) | O(n) |
Tail Recursion
Tail Recursion
- A method is tail recursive if the recursive call is its last action — nothing is left to do after the call returns.
- The partial result travels down in an accumulator parameter (acc); the multiplication happens before the call. First call:
factTail(n, 1).
Not tail recursive
int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); // multiply after the call returns }
Tail recursive
int factTail(int n, int acc) { if (n <= 1) return acc; return factTail(n - 1, n * acc); // last action: the call (multiply first) }
Tail Recursion · Step through the code
Code Trace: factTail(4, 1)
main calls factTail(4, 1); acc starts at 1.
n = 4 > 1 → multiply first: 4 · 1 = 4. Then call factTail(3, 4).
n = 3 > 1 → multiply first: 3 · 4 = 12. Then call factTail(2, 12).
n = 2 > 1 → multiply first: 2 · 12 = 24. Then call factTail(1, 24).
n = 1: base case → return acc = 24. The result is ready right here.
factTail(2, 12) passes 24 on unchanged: there is nothing to do on the way back.
factTail(3, 4) passes 24 on unchanged: there is nothing to do on the way back.
factTail(4, 1) passes 24 on unchanged: there is nothing to do on the way back.
Result 24. Java still pushed 4 frames, even though none of them was needed on the way back.
int factTail(int n, int acc) { if (n <= 1) return acc; // base case return factTail(n - 1, n * acc); // last action } // in main: System.out.println(factTail(4, 1));
Tail Recursion
Converting Tail Recursion to a Loop
int factLoop(int n) { int acc = 1; while (n > 1) { // while not the base case acc = n * acc; // the call's new n = n - 1; // parameters } return acc; // base: return acc }
- A tail call means updating the parameters and jumping back to the start of the method: the parameters become variables and the base case becomes the loop condition.
- The conversion is mechanical; iterative binary search is just
binarySearchconverted this way. - The loop runs in constant memory, O(1), with no frames pushed.
factTail too, so very deep recursion still throws StackOverflowError. Some languages, such as Scheme, guarantee this optimization; the Scala and Kotlin compilers can turn self-recursive tail calls into loops.Tail Recursion
Recursion or Loop?
| Criterion | Recursion | Loop |
|---|---|---|
| Readability | short, close to the definition | state variables managed by hand |
| Memory | O(depth) call stack | usually O(1) |
| Speed | every call has overhead | usually faster |
| Depth limit | risk of StackOverflowError | none |
Recursion on Linked Lists
Recursion on Linked Lists
- A linked list is either empty (
null) or a node followed by a shorter list. - The definition is recursive: process the node, then pass the rest of the list (
node.next) to the same method. - Base case:
node == null.
class Node { int data; Node next; } int length(Node node) { if (node == null) return 0; // empty list return 1 + length(node.next); // 1 + the rest }
void print(Node node) { if (node == null) return; System.out.print(node.data + " "); print(node.next); }
Recursion on Linked Lists · Step through the code
Code Trace: length(head)
main calls length(head); node points to the first node (7).
node ≠ null → 1 + length(node.next): a call for the rest of the list (starting at 3); this frame waits.
node ≠ null → 1 + length(node.next): a call for the rest of the list (starting at 9); this frame waits.
node ≠ null → 1 + length(node.next): node.next = null, so the call is for the empty list.
node = null: base case; the empty list has length 0.
The rest of the list gave 0 → return 1 + 0 = 1.
The rest of the list gave 1 → return 1 + 1 = 2.
The rest of the list gave 2 → return 1 + 2 = 3.
Result 3. 4 calls (3 nodes + null): time O(n), stack O(n).
int length(Node node) { if (node == null) return 0; // empty list return 1 + length(node.next); // 1 + the rest } // in main (head → 7 → 3 → 9 → null): System.out.println(length(head));
Recursion on Linked Lists
Printing a List in Reverse
void print(Node node) { if (node == null) return; System.out.print(node.data + " "); print(node.next); }
Output: 7 3 9 (print before the call)
void printReverse(Node node) { if (node == null) return; printReverse(node.next); // the rest first System.out.print(node.data + " "); }
Output: 9 3 7 (print after the call)
- A singly linked list has no backward links; the call stack provides the way back, printing the nodes from last to first as it unwinds.
- The price is O(n) stack memory. For very long lists, a loop with an explicit stack is safer.
Summary
Common Mistakes
StackOverflowError. n <= 1 is safer than n == 1.f(n) calls f(n) or f(n + 1) again, it never gets closer to the base case.return before the left-half call in binarySearch and the result is thrown away; control falls through to the right-half call. Wrong answer, and no compiler warning.s.remove(...) in backtracking, one branch's choices leak into the next, and the subsets come out wrong.fib, the same subproblem is solved an exponential number of times. Memoization or a loop gives O(n).length overflows the stack. Java doesn't optimize tail calls → use a loop.Lab · Java code, step by step
Lab: The Call Stack
Summary
Summary
| Example | Kind of recursion | Time | Space (stack) |
|---|---|---|---|
| factorial, sum, reverse | linear | O(n) | O(n) |
| fastPower, binarySearch | linear, halving | O(log n) | O(log n) |
| fib | binary | exponential (≈ 1.6ⁿ) | O(n) |
| hanoi | binary | O(2ⁿ) | O(n) |
| subsets | backtracking | O(n · 2ⁿ) | O(n) |
| perm | multiple, backtracking | O(n · n!) | O(n) |
| factTail | tail recursion | O(n) | O(n) (in Java) |
| length, printReverse | linear (linked list) | O(n) | O(n) |
Summary
Interactive Simulators
Summary
Sample Problems
- For
int f(int n) { if (n <= 0) return 0; return n % 10 + f(n / 10); }, what isf(2026), and how many calls are made? - What does
countUp(4)print? At most how many frames (including main) are on the stack? - How many calls does
fastPower(3, 13)make, and what is the result? - How many calls does
fib(6)make? - For {1, 2, 3, 4}, how many subsets does
subsetsprint, and how many calls does it make? - If T(n) = T(n/2) + c and T(1) = c, how many calls are in the chain for n = 64?
Show answers
- Digit sum: 6 + 2 + 0 + 2 = 10; calls 2026, 202, 20, 2, 0 → 5.
1 2 3 4; countUp(4 … 0) = 5 frames + main = 6.- n: 13, 6, 3, 1, 0 → 5 calls; 3¹³ = 1,594,323.
- 2 · F(7) − 1 = 2 · 13 − 1 = 25 calls (fib(6) = 8).
- 2⁴ = 16 subsets; 2⁵ − 1 = 31 calls.
- 64, 32, 16, 8, 4, 2, 1 → 7 = log₂ 64 + 1.
Summary
Exercises
- While
factorial(5)runs, what is the largest number of frames (including main) on the call stack? - How many moves does Hanoi take with 4 disks?
- What are the base case and the recursive step of a recursive
largest(a, n)that returns the largest item of an array? isPalindrome(s, i, j)compares the ends and moves inward. How many calls does it make for "kayak"?int g(int n) { if (n <= 1) return 1; return g(n - 1) + g(n - 1); }: what are g(5) and the number of calls? How can it be made O(n)?- In the loop version of
factTail, how do n and acc change on each iteration?
Answers / hints
- factorial(5), (4), (3), (2), (1) + main = 6.
- 2⁴ − 1 = 15 moves.
- Base: n = 1 → a[0]. Step: max(largest(a, n − 1), a[n − 1]). O(n).
- (0, 4) k = k → (1, 3) a = a → (2, 2) base: 3 calls, true.
- g(5) = 2⁴ = 16; 2⁵ − 1 = 31 calls. One call is enough:
return 2 * g(n - 1);→ O(n). acc = n * acc; n = n - 1;the loop runs while n > 1; at the end acc is returned.
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Sedgewick and Wayne, Computer Science: An Interdisciplinary Approach, 2016.
- Roberts, Thinking Recursively with Java, 2006.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.