SEN1002 · Week 12 · Recursion

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.

recursionrecursive methodbase caserecursive stepcall stackstack frameStackOverflowErrorfactorialFibonaccibinary searchpalindromerecursion vs iteration

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:

  1. No recursion without a base case. In the smallest case, the method returns without calling itself.
  2. Each call must get closer to the base case. Like n - 1, n / 10, substring(1).
  3. Each call has its own variables. The n in one call does not affect the n in another.
  4. 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: n!=n⋅(n−1)!n! = n \cdot (n-1)!, and 0!=1!=10! = 1! = 1.

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

F(0)=0F(0) = 0, F(1)=1F(1) = 1, F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2).

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:

ComputationTotal fib calls
fib(5)15
fib(10)177
fib(30)2692537

The number of calls grows roughly as O(2n)O(2^n). A loop does the same job in O(n)O(n) 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 2302^{30}; the version that halves finished in 6 calls (30, 15, 7, 3, 1, 0). This O(log⁡n)O(\log n) 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

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?

RecursionLoop (iteration)
ReadabilityVery clean for naturally recursive problems (trees, divide and conquer)Clearer for simple repetition
MemoryEach call is a stack frameConstant
SpeedHas call cost; a disaster with repeated computation like naive fibUsually faster
RiskBase case mistake → StackOverflowErrorCondition mistake → infinite loop
ConversionEvery recursion can be turned into a loopEvery loop can be turned into recursion

PITFALL · skipping the base case. If the method m2(n) returns n + m2(n - 2) and the base case is n == 0, odd numbers never see 0 (7, 5, 3, 1, −1, …) and the stack overflows. That is why writing n <= 0 is 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 ideaThe truth
It stops somewhere even without a base caseStackOverflowError
Lines after the call also run firstOn the way back, in reverse order
All calls share the same nEach call has its own n
fib(5) makes 5 calls15 calls; the tree grows exponentially
factorial works with int for every n13! 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 returnsmissing return statement
Binary search works on an unsorted array tooThe array must be sorted

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

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
3 2 1 and 1 2 3

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.

Question 2medium

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
<3<2<11>2>3>

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).

Question 3easy

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
24; 4 calls on the stack at the same time

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.

Question 4medium

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
15

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.

Question 5medium

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
6 1

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.

Question 6medium

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
kcats

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".

Question 7medium

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
16 12

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.

Question 8medium

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
missing return statement

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.

Question 9easy

What happens when bad(5) is called?

static int bad(int n) {
    return n + bad(n - 1);
}
Show answer
StackOverflowError

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.

Question 10medium

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
(0, 8, 4) then (5, 8, 6): found, index 6

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.

Question 11hard

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
1213121

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.

Question 12medium

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
true if length ≤ 1; false if the ends differ; otherwise check the middle recursively

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).

Review card

Recursive method = base case (stop) + recursive step (shrink and call itself). Each call must MOVE the problem CLOSER to the base case. Otherwise StackOverflowError (an Error, not an Exception). Each call has its own parameters and local variables (stack frame). Lines BEFORE the call run on the way down, lines AFTER it run on the way back in REVERSE order. factorial: n<=1 → 1; n * f(n-1). 4! = 24, 0! = 1. fib: n<2 → n; f(n-1)+f(n-2). 0 1 1 2 3 5 8 13 21 34 55. The call count is exponential: fib(5) 15 calls. power: e==0 → 1; b * p(b, e-1) (e calls). Fast: half = p(b, e/2); if even half*half (log e). palindrome: len<=1 → true; ends differ → false; substring(1, len-1). binary search: low>high → -1; mid=(low+high)/2; key<a[mid] → left half, otherwise right. GCD (Euclid): b==0 → a; gcd(b, a%b). Every path must return, otherwise "missing return statement". Recursion can always be turned into a loop; a loop is usually faster and uses less memory.