Recursion patterns
Recursive methods fall into a few recognisable shapes. Naming the shape tells you the shape of the call tree, how deep the stack gets, and roughly how much work is done.
Learning goals
- Classify a method as linear, binary or multiple-branching recursion
- Tell head recursion from tail recursion and say where the work happens
- Write mutually recursive methods
- Decide between a void recursion and one that returns a value
Prerequisites
Linear recursion
Linear recursion makes at most one recursive call per invocation. The call tree is a single chain: factorial, sum of digits, reversing an array. For n levels it makes n calls and uses O(n) stack.
Many linear recursions are really loops in disguise, and the iterative version needs no stack.
1static int sum(int[] a, int i) {2if (i == a.length) return 0; // base case3return a[i] + sum(a, i + 1); // exactly one recursive call4}
Binary recursion
Binary recursion makes two recursive calls. The call tree is a binary tree, and the number of calls depends on how the problem shrinks: Fibonacci shrinks by 1 and 2 (exponential calls), merge sort halves (O(n) calls), and Tower of Hanoi shrinks by 1 twice (2^n - 1 moves).
In the animation each disk move is one visit of the middle step: move n - 1 disks away, move the biggest, move n - 1 disks back on top. The call count doubles with each extra disk, which is why 64 disks would take about 1.8 × 10^19 moves.
| Disks | Moves 2^n - 1 | At one move per second |
|---|---|---|
| 5 | 31 | 31 seconds |
| 10 | 1,023 | 17 minutes |
| 20 | 1,048,575 | 12 days |
| 64 | 18,446,744,073,709,551,615 | 585 billion years |
Worst
O(2^n)
Space
O(n) stack
Tower of Hanoi
Interactive · edit the inputThree pegs · 3 disks
A
B
C
Peg A: 3, 2, 1. Peg B: empty. Peg C: empty
Move all 3 disks from A to C. Only one disk moves at a time and a disk may never sit on a smaller one. The minimum is 2^3 - 1 = 7 moves.
1static void hanoi(int n, char from, char to, char via) {2if (n == 0) return;3hanoi(n - 1, from, via, to); // park the n-1 smaller disks on 'via'4System.out.println("Move disk " + n + ": " + from + " -> " + to);5hanoi(n - 1, via, to, from); // bring them onto the big disk6}
Live state
n
3
Call stack (top first)
- empty
Execution trace (dry run table)Show 16 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=3 | Move all 3 disks from A to C. Only one disk moves at a time and a disk may never sit on a smaller one. The minimum is 2^3 - 1 = 7 moves. |
| 2 | L1 | n=3 from=A to=C via=B | To move 3 disks from A to C: first park the 2 above the biggest on B. |
| 3 | L1 | n=2 from=A to=B via=C | To move 2 disks from A to B: first park the 1 above the biggest on C. |
| 4 | L1 | n=1 from=A to=C via=B | Move 1 disk from A to C: nothing sits above it, so it can move directly. |
| 5 | L4 | n=1 disk=1 from=A to=C | Move disk 1 from A to C (move 1). |
| 6 | L4 | n=2 disk=2 from=A to=B | Move disk 2 from A to B (move 2). |
| 7 | L1 | n=1 from=C to=B via=A | Move 1 disk from C to B: nothing sits above it, so it can move directly. |
| 8 | L4 | n=1 disk=1 from=C to=B | Move disk 1 from C to B (move 3). |
| 9 | L4 | n=3 disk=3 from=A to=C | Move disk 3 from A to C (move 4). |
| 10 | L1 | n=2 from=B to=C via=A | To move 2 disks from B to C: first park the 1 above the biggest on A. |
| 11 | L1 | n=1 from=B to=A via=C | Move 1 disk from B to A: nothing sits above it, so it can move directly. |
| 12 | L4 | n=1 disk=1 from=B to=A | Move disk 1 from B to A (move 5). |
| 13 | L4 | n=2 disk=2 from=B to=C | Move disk 2 from B to C (move 6). |
| 14 | L1 | n=1 from=A to=C via=B | Move 1 disk from A to C: nothing sits above it, so it can move directly. |
| 15 | L4 | n=1 disk=1 from=A to=C | Move disk 1 from A to C (move 7). |
| 16 | L6 | moves=7 | Done in 7 moves = 2^3 - 1. The count doubles (plus one) with every extra disk: T(n) = 2·T(n-1) + 1. |
Multiple branching
Multiple branching makes several recursive calls in a loop: generating permutations, subsets, or all paths through a grid. The tree has many children per node, and its size is bounded by the branching factor raised to the depth.
This is the structure of backtracking (Chapter 20). The recursion tree for permutations of n items has n! leaves.
1static void permutations(int[] a, int k, List<int[]> out) {2if (k == a.length) { out.add(a.clone()); return; } // one complete arrangement3for (int i = k; i < a.length; i++) { // up to n - k branches4swap(a, k, i);5permutations(a, k + 1, out);6swap(a, k, i); // undo, so the next branch starts clean7}8}
Head vs tail recursion
In head recursion the recursive call comes first and the work is done afterwards, on the return path. In tail recursion the work comes first and the recursive call is the last action. The two print in opposite orders.
Tail recursion can be turned into a loop mechanically, which is what some compilers do. Head recursion usually needs a stack, explicit or implicit, because work is still pending when the call is made.
1static void countDown(int n) { // work before the call2if (n == 0) return;3System.out.print(n + " ");4countDown(n - 1); // prints 3 2 15}67static void countUp(int n) { // work after the call8if (n == 0) return;9countUp(n - 1);10System.out.print(n + " "); // prints 1 2 311}
Mutual recursion
In mutual recursion two or more methods call each other. Each call moves toward the base case, but through the other method. Typical uses are parsers (an expression contains a term, which contains an expression in parentheses) and state machines.
The same termination rules apply: every cycle of calls must shrink the input.
1static boolean isEven(int n) { return n == 0 ? true : isOdd(n - 1); }2static boolean isOdd(int n) { return n == 0 ? false : isEven(n - 1); }3// isEven(3) -> isOdd(2) -> isEven(1) -> isOdd(0) -> false
Recursion with return value
A recursion can return a value that is combined on the way back (sum, max, count), or return nothing and act through side effects (printing, filling a list, swapping array elements).
Return values keep the state local to each call and are easier to reason about. Side effects need careful handling of shared state: if a recursive call changes a list or array, every later call sees the change, so undo it (backtracking) or copy it.
| Style | Example | Watch for |
|---|---|---|
| Returns a value | max of an array, tree height | Combining the children's results correctly |
| Void with side effects | print a tree, fill a result list | Shared mutable state, undoing changes |
Practice ladder
Now try it yourself
Easy
- Print an array backwards recursively
- Count the nodes of a linked list recursively
- isEven and isOdd with mutual recursion
Medium
- Generate all binary strings of length n
- Print all subsets of a set
- Move n disks in Hanoi and count the moves
Advanced
- Generate all permutations without duplicates when the input has repeats
- Evaluate a nested arithmetic expression with mutually recursive parse methods
- Prove Hanoi needs at least 2^n - 1 moves
Key takeaways
- Linear recursion makes a chain, binary recursion a binary tree, multiple branching a wide tree.
- Head recursion does its work on the way back; tail recursion does it on the way down.
- Prefer returning values over shared mutable state, or undo your changes after each branch.