Recursion
Methods that call themselves: base case, recursive step, tracing the call stack, and factorial, Fibonacci, powers, palindromes, binary search. The second big source of output-tracing questions.
Recursion is a method calling itself with a smaller input to solve a problem. In the exam it comes in two forms: “What is printed?” (tracing the call stack to find the output order) and “write a recursive method” (without loops). The key to both is the same: seeing that each call works like a separate copy with its own variables.
Golden rules:
- No recursion without a base case. In the smallest case, the method returns without calling itself.
- Each call must get closer to the base case. Like
n - 1,n / 10,substring(1). - Each call has its own variables. The n in one call does not affect the n in another.
- Lines after the call run in reverse order. The last call made finishes first.
Base case and recursive step
The definition of factorial is already recursive: , and .
static long factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive step
}
System.out.println(factorial(5) + " " + factorial(0) + " " + factorial(20));
120 1 2432902008176640000
We used long because factorial grows very fast; with int, 13! overflows. 20! is the largest factorial that fits in a long.
The call stack
Each method call adds a frame (stack frame) to the stack: the parameters, the local variables and the information “where do I continue when I return”. When the method returns, its frame is removed from the stack. In recursion, many frames of the same method sit on the stack at the same time.
If we add indentation to factorial and print every call and return:
factorial(4)
factorial(3)
factorial(2)
factorial(1)
return 1
return 2
return 6
return 24
stack (top = currently running)
┌────────────┐
│ f(1) → 1 │ base case, returns
┌────┴────────────┤
│ f(2) → 2 * 1 │
┌────┴─────────────────┤
│ f(3) → 3 * 2 │
┌────┴──────────────────────┤
│ f(4) → 4 * 6 = 24 │
└───────────────────────────┘
Output order: before or after?
Most tracing questions rest on this one idea: lines written before the recursive call run on the way down, lines written after it run on the way back (in reverse order).
static void printBefore(int n) {
if (n == 0) return;
System.out.print(n + " "); // print first
printBefore(n - 1);
}
static void printAfter(int n) {
if (n == 0) return;
printAfter(n - 1);
System.out.print(n + " "); // print on the way back
}
static void both(int n) {
if (n == 0) return;
System.out.print("<" + n);
both(n - 1);
System.out.print(n + ">");
}
printBefore(3); System.out.println();
printAfter(3); System.out.println();
both(3); System.out.println();
3 2 1
1 2 3
<3<2<11>2>3>
For methods that make two calls, draw a tree. tree(n) first calls tree(n-1), then prints n, then calls tree(n-1) again:
tree(3)
├─ tree(2)
│ ├─ tree(1) → "1"
│ ├─ "2"
│ └─ tree(1) → "1"
├─ "3"
└─ tree(2) → "121"
result: 1213121
Classic examples
Fibonacci
, , .
static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
for (int i = 0; i <= 10; i++) System.out.print(fib(i) + " ");
0 1 1 2 3 5 8 13 21 34 55
Short and readable, but very slow: it computes the same values again and again. We measured the total number of calls with a counter:
| Computation | Total fib calls |
|---|---|
fib(5) | 15 |
fib(10) | 177 |
fib(30) | 2692537 |
The number of calls grows roughly as . A loop does the same job in steps. The answer to the exam question “why is it slow”: repeated subproblems.
Powers
static double power(double base, int exp) {
if (exp == 0) return 1;
if (exp < 0) return 1 / power(base, -exp);
return base * power(base, exp - 1); // exp calls
}
static long fastPower(long b, int e) {
if (e == 0) return 1;
long half = fastPower(b, e / 2); // halve the problem
return (e % 2 == 0) ? half * half : half * half * b;
}
System.out.println(power(2, 10) + " " + power(3, 0) + " " + power(2, -2));
System.out.println(fastPower(2, 30) + " calls=" + pcalls);
1024.0 1.0 0.25
1073741824 calls=6
The plain version would make 31 calls for ; the version that halves finished in 6 calls (30, 15, 7, 3, 1, 0). This idea is also in binary search.
Palindrome
static boolean isPalindrome(String s) {
if (s.length() <= 1) return true;
if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
return isPalindrome(s.substring(1, s.length() - 1));
}
System.out.println(isPalindrome("racecar") + " " + isPalindrome("abca") + " "
+ isPalindrome("") + " " + isPalindrome("noon"));
true false true true
Binary search
Compare the searched value with the middle element of a sorted array; if it is smaller, repeat the same thing in the left half, if larger, in the right half. Each call halves the search range. (In week 13 we will compare it with the loop version.)
static int binarySearch(int[] a, int key, int low, int high) {
if (low > high) return -1; // range is empty: not found
int mid = (low + high) / 2;
System.out.println(" low=" + low + " high=" + high + " mid=" + mid + " a[mid]=" + a[mid]);
if (a[mid] == key) return mid;
if (key < a[mid]) return binarySearch(a, key, low, mid - 1);
return binarySearch(a, key, mid + 1, high);
}
int[] a = {3, 8, 15, 21, 30, 42, 57, 66, 80};
System.out.println("found at " + binarySearch(a, 57, 0, a.length - 1));
System.out.println("found at " + binarySearch(a, 10, 0, a.length - 1));
low=0 high=8 mid=4 a[mid]=30
low=5 high=8 mid=6 a[mid]=57
found at 6
low=0 high=8 mid=4 a[mid]=30
low=0 high=3 mid=1 a[mid]=8
low=2 high=3 mid=2 a[mid]=15
found at -1
The trace lines are printed before the “found at” line: the concatenation in println’s argument can only be done when binarySearch has completely finished.
Other patterns
static int sumDigits(int n) { // 9045 → 9+0+4+5
if (n < 10) return n;
return n % 10 + sumDigits(n / 10);
}
static int sumArray(int[] a, int i) { // array + index parameter
if (i == a.length) return 0;
return a[i] + sumArray(a, i + 1);
}
static int countX(String s) { // shrink the String letter by letter
if (s.isEmpty()) return 0;
return (s.charAt(0) == 'x' ? 1 : 0) + countX(s.substring(1));
}
System.out.println(sumDigits(9045));
System.out.println(sumArray(new int[]{4, 7, 1, 8}, 0));
System.out.println(countX("xaxbxx"));
18
20
4
Recursion or loop?
| Recursion | Loop (iteration) | |
|---|---|---|
| Readability | Very clean for naturally recursive problems (trees, divide and conquer) | Clearer for simple repetition |
| Memory | Each call is a stack frame | Constant |
| Speed | Has call cost; a disaster with repeated computation like naive fib | Usually faster |
| Risk | Base case mistake → StackOverflowError | Condition mistake → infinite loop |
| Conversion | Every recursion can be turned into a loop | Every loop can be turned into recursion |
PITFALL · skipping the base case. If the method
m2(n)returnsn + m2(n - 2)and the base case isn == 0, odd numbers never see 0 (7, 5, 3, 1, −1, …) and the stack overflows. That is why writingn <= 0is safer. When you write a base case, ask “does every input reach this?”
A method without a base case (return n + bad(n - 1);) compiles but throws StackOverflowError when it runs; that is exactly what happened in the test run. A method where not every path returns does not compile at all: missing return statement.
Common mistakes
| Wrong idea | The truth |
|---|---|
| It stops somewhere even without a base case | StackOverflowError |
| Lines after the call also run first | On the way back, in reverse order |
All calls share the same n | Each call has its own n |
fib(5) makes 5 calls | 15 calls; the tree grows exponentially |
factorial works with int for every n | 13! does not fit in int; use long |
sum(n-1) + n; instead of return n + sum(n-1) | A value must be returned; as a statement on its own it is not a statement |
| It is fine if not every path returns | missing return statement |
| Binary search works on an unsorted array too | The array must be sorted |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed?
static void printBefore(int n) {
if (n == 0) return;
System.out.print(n + " ");
printBefore(n - 1);
}
static void printAfter(int n) {
if (n == 0) return;
printAfter(n - 1);
System.out.print(n + " ");
}
// in main:
printBefore(3); System.out.println();
printAfter(3);Show answer
printBefore prints first and then descends: 3, 2, 1. printAfter first descends all the way down (3 → 2 → 1 → 0), and the printing is done on the way back: the last call (1) prints first. Lines that come AFTER the recursive call run in reverse order.
What is printed by both(3);?
static void both(int n) {
if (n == 0) return;
System.out.print("<" + n);
both(n - 1);
System.out.print(n + ">");
}Show answer
On the way down, "<n" is printed: <3 <2 <1. both(0) returns immediately. On the way back, "n>" is printed, from the innermost to the outermost: 1> 2> 3>. Joined, it is <3<2<11>2>3> (the "11" in the middle is "<1" and "1>" side by side).
What does factorial(4) return, and how deep does the call stack get?
static long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}Show answer
factorial(4) → 4 · factorial(3) → 3 · factorial(2) → 2 · factorial(1) = 1. At the deepest point there are 4 frames on the stack at the same time. On the way back: 1, 2 · 1 = 2, 3 · 2 = 6, 4 · 6 = 24.
How many times is fib called in total when computing fib(5)?
static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}Show answer
Each call (if n ≥ 2) makes two more calls. The count is C(n) = 1 + C(n−1) + C(n−2), C(0) = C(1) = 1: C(2) = 3, C(3) = 5, C(4) = 9, C(5) = 15. The result is fib(5) = 5. In the test run, 177 calls were counted for fib(10) and 2,692,537 for fib(30): exponential growth.
What is printed?
static int mystery(int a, int b) {
if (b == 0) return a;
return mystery(b, a % b);
}
// in main:
System.out.println(mystery(48, 18) + " " + mystery(17, 5));Show answer
This is Euclid's algorithm: the greatest common divisor (GCD). (48, 18) → (18, 12) → (12, 6) → (6, 0) → 6. (17, 5) → (5, 2) → (2, 1) → (1, 0) → 1.
What is printed?
static String reverse(String s) {
if (s.isEmpty()) return s;
return reverse(s.substring(1)) + s.charAt(0);
}
// in main:
System.out.println(reverse("stack"));Show answer
Each step separates the first letter, reverses the rest, and adds the first letter at the end: reverse("stack") = reverse("tack") + 's' = (reverse("ack") + 't') + 's' = ... = "kcats".
What is printed?
static int m2(int n) {
if (n <= 0) return 0;
return n + m2(n - 2);
}
// in main:
System.out.println(m2(7) + " " + m2(6));Show answer
m2(7) = 7 + 5 + 3 + 1 + m2(−1) = 16. m2(6) = 6 + 4 + 2 + m2(0) = 12. If the base case had been written n == 0 instead of n <= 0, odd numbers would skip 0 and descend forever, giving a StackOverflowError.
Find the compile error.
static int sum(int n) {
if (n == 0) return 0;
else if (n > 0) return n + sum(n - 1);
}Show answer
The compiler wants every path to return a value. When n < 0, no return is reached. javac: missing return statement. Fix: make the last else if an else, or add a return at the end.
What happens when bad(5) is called?
static int bad(int n) {
return n + bad(n - 1);
}Show answer
There is no base case: the method calls itself forever, each call adds a new frame to the stack, the stack fills up, and the JVM throws StackOverflowError. It is not a compile error. Error is a subclass of Throwable, not of Exception.
A recursive binary search looks for 57 in {3, 8, 15, 21, 30, 42, 57, 66, 80} with mid = (low + high) / 2. Give the (low, high, mid) of each call.
Show answer
The first call is low = 0, high = 8, mid = 4, a[4] = 30. 57 > 30, go to the right half: low = 5. mid = (5 + 8) / 2 = 6, a[6] = 57: found. 2 calls. If 10 were searched: (0,8,4) → (0,3,1) → (2,3,2) → low = 2 > high = 1: −1, 4 calls in total.
What is printed by tree(3);?
static void tree(int n) {
if (n == 0) return;
tree(n - 1);
System.out.print(n);
tree(n - 1);
}Show answer
tree(1) = "1". tree(2) = tree(1) + "2" + tree(1) = "121". tree(3) = tree(2) + "3" + tree(2) = "1213121". Since each level makes two calls, the length is 2 to the power n minus 1: 7 characters.
Write a recursive method boolean isPalindrome(String s) (no loops). isPalindrome("racecar") is true, isPalindrome("abca") is false, and the empty string is a palindrome.
Show answer
There are two base cases: empty or a single character (true) and the ends differ (false). The recursive step shrinks the problem by two characters each time:
public static boolean isPalindrome(String s) {
if (s.length() <= 1) return true; // base case
if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
return isPalindrome(s.substring(1, s.length() - 1)); // the middle
}In the test run "racecar" gave true, "abca" false, "" true, "noon" true.Verification: All code examples, traces and call counts were run with JDK 26 (java verify/SEN1002/W12.java); the compile errors were tried with javac (verify/SEN1002/err/E12a-b.java).