Recursion
The base case and the recursive step, call stack traces, recursive methods on linked lists, and an introduction to backtracking. The exam has “what is printed”, call count, and recursive method writing questions.
Recursion is a method calling itself with a smaller input. At first it looks like magic, but the logic behind it is simple: write the problem as one piece + a smaller version of the same problem, and solve the smallest version directly. A linked list is already a recursive structure (a node + the rest of the list), so list methods are very short when written recursively. Exam questions: what the code prints, how many calls are made, and writing a short recursive method. Next week we will compute the complexity of these methods.
Golden rules:
- Don’t write recursion without a base case. Write it first.
- Every call must approach the base. n − 1, n / 2, p.next.
- Trust the result. Assume
size(p.next)works correctly, and just add your own step. - The position of the print decides the order. Before the call or after?
The base case and the recursive step
static int factorial(int n) {
if (n <= 1) return 1; // base case: a direct answer
return n * factorial(n - 1); // recursive step: shrink and call
}
The same as the definition in mathematics: and . factorial(5) returns 120, factorial(0) returns 1.
The call stack
Every method call opens a stack frame holding its own parameters and local variables. When the call ends, the frame is popped and the return value is passed to the caller. The very same stack as in week 5.
The program output of the trace of factorial(4) (indentation = depth):
factorial(4) called
factorial(3) called
factorial(2) called
factorial(1) called
factorial(1) returns 1
factorial(2) returns 2
factorial(3) returns 6
factorial(4) returns 24
No multiplication is done on the way down; they all wait. When the base case is reached, the return begins and the multiplications are done from the inside out: 1, 2 · 1, 3 · 2, 4 · 6.
the call stack at the deepest moment (top is up):
| factorial(1) |
| factorial(2) | waiting for 2 * ?
| factorial(3) | waiting for 3 * ?
| factorial(4) | waiting for 4 * ?
| main |
Recursion that has no base case or doesn’t approach it fills this stack and you get a StackOverflowError. With default JVM settings this happens at a call depth of a few thousand to a few tens of thousands.
Where is the print?
static void down(int n) { if (n == 0) return; System.out.print(n + " "); down(n - 1); }
static void up(int n) { if (n == 0) return; up(n - 1); System.out.print(n + " "); }
3 2 1
1 2 3
down prints while descending, up prints while returning. In an output question, the first thing to ask is: is the print before or after the recursive call?
More than one call: tree recursion
If a method calls itself twice, the calls form a tree and their number grows quickly.
static int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(5)=5 calls=15
fib(10)=55 calls=177
fib(20)=6765 calls=21891
fib(25)=75025 calls=242785
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2)
│ │ │ ├── fib(1)
│ │ │ └── fib(0)
│ │ └── fib(1)
│ └── fib(2)
│ ├── fib(1)
│ └── fib(0)
└── fib(3)
├── fib(2)
│ ├── fib(1)
│ └── fib(0)
└── fib(1)
fib(3) is computed twice, and fib(2) three times. That is why naive Fibonacci is exponential; with a loop or by storing what has been computed (memoization) it becomes O(n).
If there is a print between the two calls, an interesting pattern appears:
static void g(int n) { if (n == 0) return; g(n - 1); System.out.print(n + " "); g(n - 1); }
1 2 1 3 1 2 1
The speed difference: n − 1 or n / 2?
static long pow(long b, int n) { // n+1 calls
if (n == 0) return 1;
return b * pow(b, n - 1);
}
static long fastPow(long b, int n) { // ~log n calls
if (n == 0) return 1;
long half = fastPow(b, n / 2);
return (n % 2 == 0) ? half * half : half * half * b;
}
1024 calls=11
1024 calls=5
, . Halving the input means logarithmic depth. In week 11 we will show this with a formula.
Recursion on a linked list
List = empty (null) or a node + a list. This definition turns directly into code:
static int size(Node p) { if (p == null) return 0; return 1 + size(p.next); }
static int sum(Node p) { if (p == null) return 0; return p.data + sum(p.next); }
static boolean contains(Node p, int x) {
if (p == null) return false;
if (p.data == x) return true;
return contains(p.next, x);
}
static void printReverse(Node p) {
if (p == null) return;
printReverse(p.next); // the rest first
System.out.print(p.data + " "); // then itself
}
For the list 4 → 7 → 1 → 9, size, sum, contains(1), contains(5) and printReverse:
4 21 true false
9 1 7 4
printReverse prints a singly linked list backward without damaging it; to do this with a loop you would have to either reverse the list or use a stack. In recursion that stack is the call stack itself.
PITFALL · Making the base case
p.next == null.if (p.next == null) return 1;gives a NullPointerException on an empty list (p is null). Write the base case for the smallest input, the empty list:p == null.
An introduction to backtracking
Some problems ask you to “try all possible choices”: all subsets, all permutations, placing queens on a chessboard. The backtracking template:
solve(state):
if the state is complete: record / print, return
for each option:
apply the option
solve(new state)
UNDO the option
All subsets
static void subsets(int[] a, int i, List<Integer> cur) {
if (i == a.length) { System.out.print(cur + " "); return; }
cur.add(a[i]); // take
subsets(a, i + 1, cur);
cur.remove(cur.size() - 1); // undo
subsets(a, i + 1, cur); // don't take
}
[1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []
Permutations
static void perms(String prefix, String rest) {
if (rest.isEmpty()) { System.out.print(prefix + " "); return; }
for (int i = 0; i < rest.length(); i++)
perms(prefix + rest.charAt(i), rest.substring(0, i) + rest.substring(i + 1));
}
ABC ACB BAC BCA CAB CBA
N-Queens
n queens on an n × n board, none threatening another. Put one queen in each row; if a column or diagonal conflicts, skip that column, and if no column works, go back to the previous row and move its queen. The solution counts the program counted:
4-queens: 2
5-queens: 10
6-queens: 4
7-queens: 40
8-queens: 92
Backtracking is better than brute force because it cuts a branch as soon as it sees a conflict; on 8 × 8 it tries very few of the million placements.
Common mistakes
| Wrong | Correct |
|---|---|
| Forgetting the base case | First line: if (base) return ...; |
Not writing return: factorial(n - 1); | Use the result: return n * factorial(n - 1); |
Base case p.next == null on a list | p == null (empty list) |
n doesn’t shrink: f(n) inside f(n) | f(n - 1) or f(n / 2) |
| Calling the half twice in fastPow | Put it in a half variable |
| Ignoring the position of the print | Before → descent order, after → return order |
| Not undoing the choice in backtracking | Without cur.remove(...) the later branches get polluted |
| Recursion is always slower than a loop | On lists and trees it is often simpler, with the same O(n) |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What does factorial(5) return?
static int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}Show answer
5 · factorial(4) = 5 · 4 · factorial(3) = ... = 5 · 4 · 3 · 2 · 1 = 120. Since the base case is n ≤ 1, factorial(0) also returns 1.
What do down(3) and up(3) print?
static void down(int n) {
if (n == 0) return;
System.out.print(n + " ");
down(n - 1);
}
static void up(int n) {
if (n == 0) return;
up(n - 1);
System.out.print(n + " ");
}Show answer
down prints before the call: it prints 3 then goes down to 2. up first goes all the way down (up(0)) and prints on the way back: 1, 2, 3. The position of the print relative to the recursive call decides the order.
What do mystery(1234) and mystery(907) return?
static int mystery(int n) {
if (n == 0) return 0;
return n % 10 + mystery(n / 10);
}Show answer
It takes the last digit and passes the rest to the recursion: the digit sum. 4 + 3 + 2 + 1 = 10. 7 + 0 + 9 = 16.
How many times is pow called (including the first call) to compute pow(2, 10)? How many for fastPow(2, 10)?
static long pow(long b, int n) {
if (n == 0) return 1;
return b * pow(b, n - 1);
}
static long fastPow(long b, int n) {
if (n == 0) return 1;
long half = fastPow(b, n / 2);
return (n % 2 == 0) ? half * half : half * half * b;
}Show answer
pow is called once each for n = 10, 9, ..., 0: 11 calls, O(n). fastPow halves n each time: 10, 5, 2, 1, 0: 5 calls, O(log n). Both return 1024. Keeping half in a variable is important; if you write fastPow(b, n/2) * fastPow(b, n/2) the number of calls becomes O(n) again.
With the naive recursive Fibonacci below, what is fib(5) and how many calls are made in total?
static int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}Show answer
The call tree: fib(5) → fib(4), fib(3); fib(4) → fib(3), fib(2); ... The same values are computed over and over. The number of calls is C(n) = C(n−1) + C(n−2) + 1, C(5) = 15. 21 891 calls for fib(20), 242 785 calls for fib(25): exponential growth.
What does g(3) print?
static void g(int n) {
if (n == 0) return;
g(n - 1);
System.out.print(n + " ");
g(n - 1);
}Show answer
g(1) = 1. g(2) = g(1), 2, g(1) = 1 2 1. g(3) = g(2), 3, g(2) = 1 2 1 3 1 2 1. Two calls at each level: in total numbers are printed (the Towers of Hanoi order).
Write a recursive method static int size(Node p) that returns the number of nodes in the list starting at p. No loops.
Show answer
The empty list is the base case. A non-empty list = one node + the rest of the list.
static int size(Node p) {
if (p == null) return 0;
return 1 + size(p.next);
}The sum with the same pattern: return p.data + sum(p.next);. For the list 4 7 1 9, size is 4, sum is 21.Write a recursive method static void printReverse(Node p) that prints a singly linked list from the last node to the first.
Show answer
First print the rest of the list (reversed), then yourself. The call stack reverses the order for you.
static void printReverse(Node p) {
if (p == null) return;
printReverse(p.next);
System.out.print(p.data + " ");
}4 7 1 9 → 9 1 7 4. The list is not changed. The cost is O(n) stack frames; on a very long list there is a risk of StackOverflowError.What happens when bad(5) is called?
static int bad(int n) {
return n + bad(n - 1);
}Show answer
There is no base case. Every call opens a new stack frame, and none of them returns. When the call stack is full, the JVM throws a StackOverflowError. Every recursive method must (1) have a base case and (2) each call must approach the base.
Write a recursive method static boolean isPal(String s, int lo, int hi) that checks whether s is a palindrome between indices lo and hi.
Show answer
static boolean isPal(String s, int lo, int hi) {
if (lo >= hi) return true; // 0 or 1 characters left
if (s.charAt(lo) != s.charAt(hi)) return false;
return isPal(s, lo + 1, hi - 1);
}isPal("level", 0, 4) → true, isPal("lever", 0, 4) → false: in the first step index 0 (l) and index 4 (r) differ. The call starts with isPal(s, 0, s.length() - 1).What does this backtracking method print for a = {1, 2, 3}?
static void subsets(int[] a, int i, List<Integer> cur) {
if (i == a.length) { System.out.print(cur + " "); return; }
cur.add(a[i]);
subsets(a, i + 1, cur);
cur.remove(cur.size() - 1);
subsets(a, i + 1, cur);
}Show answer
For each element there are two choices: take it, don't take it. The 'take' branch is explored completely first. Take 1, take 2, take 3 → [1,2,3]; undo 3 → [1,2]; undo 2, take 3 → [1,3]; → [1]; then the same without taking 1: [2,3] [2] [3] []. In total subsets. The cur.remove line is backtracking itself.
In what order does this method print the permutations of "ABC"?
static void perms(String prefix, String rest) {
if (rest.isEmpty()) { System.out.print(prefix + " "); return; }
for (int i = 0; i < rest.length(); i++)
perms(prefix + rest.charAt(i),
rest.substring(0, i) + rest.substring(i + 1));
}Show answer
At each level one of the remaining letters is chosen in turn and appended to the front. When A is chosen the remainder is BC: ABC, ACB. When B is chosen the remainder is AC: BAC, BCA. When C is chosen the remainder is AB: CAB, CBA. 3! = 6 permutations, in dictionary order.
Verification: All methods were run on the JDK in verify/SEN2211/W10.java (java W10.java). The call stack trace comes from the program's version that prints indentation, the call counts (pow, fastPow, fib) from counters inside the methods, and the subset/permutation order and the N-Queens solution counts from the program's output.