Chapter 4: Recursion

Data Structures
Base case and recursive stepThe call stackLinear recursion Fast exponentiationFibonacci and binary searchTowers of Hanoi BacktrackingRecurrence relationsTail recursionRecursion on linked lists
Sercan KÜLCÜ · Giresun University · Computer Engineering

Introduction

Learning Outcomes

Recursive design

Define a problem by a base case and a recursive step, and write it in Java.

The call stack

Trace a recursive method frame by frame to find its output and return value.

Kinds of recursion

Tell linear, binary and multiple recursion and backtracking apart, with examples.

Cost

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.

54321“What’s your place?”at the front: “1”on the way back, each person adds 1me
Same question, shorter lineEveryone asks the person ahead the same question, each time about a line one person shorter.
Base caseThe person at the front answers without asking anyone: “1”. The questions stop here.
On the way backAs the answer travels back, each person adds their share: 1, 2, 3, 4, 5.
A recursive method works the same way: it calls itself on a smaller input and answers the smallest input directly.

What Is Recursion

Recursion

n! = 1 (n ≤ 1) · n! = n · (n − 1)! (n > 1)
  • 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.
factorial(4) = 4 · factorial(3)= 24factorial(3) = 3 · factorial(2)= 6factorial(2) = 2 · factorial(1)= 2factorial(1) = 1 (base case)= 1↓ calls go down · ↑ values come back

What Is Recursion

Thinking Recursively

  1. Base case: what is the smallest input, and what is its answer? Recursion stops here.
  2. Reduce: how does the problem shrink to a smaller one of the same kind? (n − 1, n / 2, i + 1 …)
  3. 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”.

Every recursive call must move the input toward the base case; in countdown, n drops by 1 per call and reaches 0.

What Is Recursion · Step through the code

Code Trace: countdown(3)

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

main calls countdown(3). A new frame is about to be pushed.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

n = 3 ≠ 0: not the base case. Print 3, then call countdown(2); this frame waits.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

n = 2 ≠ 0: not the base case. Print 2, then call countdown(1); this frame waits.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

n = 1 ≠ 0: not the base case. Print 1, then call countdown(0); this frame waits.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

n = 0: base case. Print “Done”. The stack is at its deepest: main + 4 frames.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Done→ call · ← return

return: countdown(0) ends and its frame is popped.

countdown(3)print 3countdown(2)print 2countdown(1)print 1countdown(0)print Donereturned→ call · ← return

countdown(1) returns from the call on line 7. Nothing is left to do, so it ends too.

countdown(3)print 3countdown(2)print 2countdown(1)print 1returnedcountdown(0)print Donereturned→ call · ← return

countdown(2) returns from the call on line 7. Nothing is left to do, so it ends too.

countdown(3)print 3countdown(2)print 2returnedcountdown(1)print 1returnedcountdown(0)print Donereturned→ call · ← return

countdown(3) returns from the call on line 7. Nothing is left to do, so it ends too.

countdown(3)print 3returnedcountdown(2)print 2returnedcountdown(1)print 1returnedcountdown(0)print Donereturned→ call · ← return

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 for countUp(2) to finish before printing 3; that call in turn waits for countUp(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.
Work left after the call is kept in the frame while it waits on the stack, and runs as the stack unwinds.

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.
factorial(1)n = 1return 1factorial(2)n = 2waiting: 2 · ?factorial(3)n = 3waiting: 3 · ?main()waiting: println(?)← topeach frame has its own n
The memory cost of a recursive method is the number of frames on the stack at once — the deepest chain of calls.

The Call Stack · Step through the code

Code Trace: factorial(4) and the call stack

→ call · ← returned value

main calls factorial(4).

factorial(4)n = 4→ call · ← returned value

New frame: n = 4. Is 4 ≤ 1? No.

factorial(4)n = 4→ call · ← returned value

4 · factorial(3): the product needs factorial(3) first, so this frame waits.

factorial(4)4 · ? (waiting)factorial(3)n = 3→ call · ← returned value

New frame: n = 3. Is 3 ≤ 1? No.

factorial(4)4 · ? (waiting)factorial(3)n = 3→ call · ← returned value

3 · factorial(2): the product needs factorial(2) first, so this frame waits.

factorial(4)4 · ? (waiting)factorial(3)3 · ? (waiting)factorial(2)n = 2→ call · ← returned value

New frame: n = 2. Is 2 ≤ 1? No.

factorial(4)4 · ? (waiting)factorial(3)3 · ? (waiting)factorial(2)n = 2→ call · ← returned value

2 · factorial(1): the product needs factorial(1) first, so this frame waits.

factorial(4)4 · ? (waiting)factorial(3)3 · ? (waiting)factorial(2)2 · ? (waiting)factorial(1)base case→ call · ← returned value

New frame: n = 1. Is 1 ≤ 1? Yes.

factorial(4)4 · ? (waiting)factorial(3)3 · ? (waiting)factorial(2)2 · 1factorial(1)base case= 1→ call · ← returned value

Base case: return 1. No more calls; values start coming back.

factorial(4)4 · ? (waiting)factorial(3)3 · 2factorial(2)2 · 1= 2factorial(1)base case= 1→ call · ← returned value

factorial(1) returned 1 → 2 · 1 = 2, return 2.

factorial(4)4 · 6factorial(3)3 · 2= 6factorial(2)2 · 1= 2factorial(1)base case= 1→ call · ← returned value

factorial(2) returned 2 → 3 · 2 = 6, return 6.

factorial(4)4 · 6= 24factorial(3)3 · 2= 6factorial(2)2 · 1= 2factorial(1)base case= 1→ call · ← returned value

factorial(3) returned 6 → 4 · 6 = 24, return 24.

factorial(4)4 · 6= 24factorial(3)3 · 2= 6factorial(2)2 · 1= 2factorial(1)base case= 1→ call · ← returned value

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 -Xss option.
  • 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

No base case
int f(int n) {
  return n * f(n - 1);
}
n = 3, 2, 1, 0, −1, −2 … the calls never stop.
Base case unreachable
if (n == 1) return 1;
return n * f(n - 1);
f(0) → −1 → −2 …: n never becomes 1. n <= 1 catches this input too.
Problem doesn't shrink
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.
All three end the same way: the stack fills up and a 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.
40316223sum(a, 3) = 13a[3] = 2sum(a, 4) = 13 + 2 = 15
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)

40316223sum(a, 4) = ?returned: —

main calls sum(a, 4): the sum of the array's first 4 items.

40316223a[3]sum(a, 4) = sum(a, 3) + a[3]returned: —

n = 4 ≠ 0 → sum(a, 3) must be computed first; a[3] = 2 waits to be added.

40316223a[2]sum(a, 3) = sum(a, 2) + a[2]returned: —

n = 3 ≠ 0 → sum(a, 2) must be computed first; a[2] = 6 waits to be added.

40316223a[1]sum(a, 2) = sum(a, 1) + a[1]returned: —

n = 2 ≠ 0 → sum(a, 1) must be computed first; a[1] = 3 waits to be added.

40316223a[0]sum(a, 1) = sum(a, 0) + a[0]returned: —

n = 1 ≠ 0 → sum(a, 0) must be computed first; a[0] = 4 waits to be added.

40316223sum(a, 0) = 0 (empty prefix)returned: 0

n = 0: base case → return 0. The stack is at its deepest: main + 5 frames.

40316223a[0]sum(a, 1) = 0 + 4 = 4returned: 0 → 4

sum(a, 0) returned 0 → 0 + a[0] = 0 + 4; return 4.

40316223a[1]sum(a, 2) = 4 + 3 = 7returned: 0 → 4 → 7

sum(a, 1) returned 4 → 4 + a[1] = 4 + 3; return 7.

40316223a[2]sum(a, 3) = 7 + 6 = 13returned: 0 → 4 → 7 → 13

sum(a, 2) returned 7 → 7 + a[2] = 7 + 6; return 13.

40316223a[3]sum(a, 4) = 13 + 2 = 15returned: 0 → 4 → 7 → 13 → 15

sum(a, 3) returned 13 → 13 + a[3] = 13 + 2; return 15.

40316223a[3]sum(a, 4) = 13 + 2 = 15returned: 0 → 4 → 7 → 13 → 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 is reverse(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).
2071429314651.2.3.result: 6 1 9 4 7 2

Linear Recursion · Step through the code

Code Trace: reverse(a, 0, 5)

207142931465a = {2, 7, 4, 9, 1, 6}

main calls reverse(a, 0, 5).

607142931425ija[0] ↔ a[5] swapped

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

601142937425ija[1] ↔ a[4] swapped

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

601192437425ija[2] ↔ a[3] swapped

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

601192437425iji = 3 ≥ j = 2 → base case

i = 3 ≥ j = 2: base case; nothing is left in the middle. The stack holds main + 4 frames.

601192437425reversed array

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ⁿ = (xn/2)²  (n even)
xⁿ = x · (x(n−1)/2)²  (n odd)

2¹⁰ = (2⁵)²,  2⁵ = 2 · (2²)²,
2² = (2¹)²,  2¹ = 2 · (2⁰)²

npower: callsfastPower: calls
10115
1001018
1,0001,00111
10⁶1,000,00121

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)

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1→ call · ← return

main calls fastPower(2, 10): 2¹⁰.

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1→ call · ← return

n = 10 ≠ 0 → first compute y = fastPower(2, 5) (10 / 2 = 5).

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1→ call · ← return

n = 5 ≠ 0 → first compute y = fastPower(2, 2) (5 / 2 = 2).

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1→ call · ← return

n = 2 ≠ 0 → first compute y = fastPower(2, 1) (2 / 2 = 1).

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1→ call · ← return

n = 1 ≠ 0 → first compute y = fastPower(2, 0) (1 / 2 = 0).

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · yfastPower(2,0)1= 1→ call · ← return

n = 0: base case → return 1 (2⁰ = 1).

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · yfastPower(2,1)2 · y · y= 2fastPower(2,0)1= 1→ call · ← return

y = 1 is back. n = 1 is odd → return 2 · y · y = 2 · 1 · 1 = 2.

fastPower(2,10)y · yfastPower(2,5)2 · y · yfastPower(2,2)y · y= 4fastPower(2,1)2 · y · y= 2fastPower(2,0)1= 1→ call · ← return

y = 2 is back. n = 2 is even → return y · y = 2 · 2 = 4.

fastPower(2,10)y · yfastPower(2,5)2 · y · y= 32fastPower(2,2)y · y= 4fastPower(2,1)2 · y · y= 2fastPower(2,0)1= 1→ call · ← return

y = 4 is back. n = 5 is odd → return 2 · y · y = 2 · 4 · 4 = 32.

fastPower(2,10)y · y= 1024fastPower(2,5)2 · y · y= 32fastPower(2,2)y · y= 4fastPower(2,1)2 · y · y= 2fastPower(2,0)1= 1→ call · ← return

y = 32 is back. n = 10 is even → return y · y = 32 · 32 = 1024.

fastPower(2,10)y · y= 1024fastPower(2,5)2 · y · y= 32fastPower(2,2)y · y= 4fastPower(2,1)2 · y · y= 2fastPower(2,0)1= 1→ call · ← return

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

0F(0)1F(1)1F(2)2F(3)3F(4)5F(5)8F(6)13F(7)21F(8)34F(9)
int fib(int n) {
  if (n <= 1) return n;    // base case
  return fib(n - 1) + fib(n - 2);
}
F(0) = 0, F(1) = 1
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

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

main calls fib(4).

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(4): n > 1 → call fib(3) first; fib(2) waits its turn.

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(3): n > 1 → call fib(2) first; fib(1) waits its turn.

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(2): n > 1 → call fib(1) first; fib(0) waits its turn.

f(4)f(3)f(2)f(1)=1f(0)f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(1): base case → return 1.

f(4)f(3)f(2)f(1)=1f(0)=0f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

Second term: fib(0): base case → return 0.

f(4)f(3)f(2)=1f(1)=1f(0)=0f(1)f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(2) = fib(1) + fib(0) = 1 + 0; return 1.

f(4)f(3)f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

Second term: fib(1): base case → return 1.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(3) = fib(2) + fib(1) = 1 + 1; return 2.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(4) calls fib(2) for its second term: fib(2) is computed a second time. First fib(1).

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)=1f(0)running nowon the stack, waitingreturned a valuenot called yet

fib(1): base case → return 1.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)=1f(0)=0running nowon the stack, waitingreturned a valuenot called yet

Second term: fib(0): base case → return 0.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0running nowon the stack, waitingreturned a valuenot called yet

fib(2) = fib(1) + fib(0) = 1 + 0; return 1.

f(4)=3f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0running nowon the stack, waitingreturned a valuenot called yet

fib(4) = fib(3) + fib(2) = 2 + 1; return 3.

f(4)=3f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0running nowon the stack, waitingreturned a valuenot called yet

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).
f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)

Binary Recursion

Avoiding Repeated Work

nfib(n)calls to fib
5515
1055177
206,76521,891
30832,0402,692,537
40102,334,155331,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;
}
If computed values are stored in an array, each one is computed once: 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⁶.
fib: two calls → treebinarySearch: one is taken → chain

Binary Recursion · Step through the code

Code Trace: binarySearch(a, 23, 0, 9)

205182123164235386567728919x = 23

main searches a sorted 10-item array for 23: binarySearch(a, 23, 0, 9).

205182123164235386567728919lomidhix = 23range: 10 items

lo = 0 ≤ hi = 9: the range is not empty. mid = (0 + 9) / 2 = 4, a[4] = 16.

205182123164235386567728919lohix = 23range: 5 items

23 > 16 → right half: binarySearch(a, 23, 5, 9). The left half is never examined.

205182123164235386567728919lomidhix = 23range: 5 items

lo = 5 ≤ hi = 9: the range is not empty. mid = (5 + 9) / 2 = 7, a[7] = 56.

205182123164235386567728919lohix = 23range: 2 items

23 < 56 → left half: binarySearch(a, 23, 5, 6). The right half is never examined.

205182123164235386567728919lo, midhix = 23range: 2 items

lo = 5 ≤ hi = 6: the range is not empty. mid = (5 + 6) / 2 = 5, a[5] = 23.

205182123164235386567728919lo, midhix = 23range: 2 items

a[5] = 23 = x: base case (found) → return 5.

205182123164235386567728919lo, midhix = 23range: 2 items

binarySearch(a, 23, 5, 9) passes the 5 it received straight on with return.

205182123164235386567728919lo, midhix = 23range: 2 items

binarySearch(a, 23, 0, 9) passes the 5 on too. No new computation happens on the way back.

205182123164235386567728919lo, midhix = 23range: 2 items

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).
ABC321
The recursive idea: move n−1 disks from A to B → move the largest disk from A to C → move the n−1 disks from B to C. Number of moves: T(n) = 2T(n−1) + 1 = 2n − 1.

Hanoi and Backtracking

Towers of Hanoi: 7 Moves with 3 Disks

ABC321

Start: 3 disks on peg A (largest at the bottom). Goal: move them all to C.

ABC321

Move 1: disk 1, A → C.

ABC321

Move 2: disk 2, A → B.

ABC321

Move 3: disk 1, C → B.

ABC213

Move 4: disk 3, A → C.

ABC123

Move 5: disk 1, B → A.

ABC132

Move 6: disk 2, B → C.

ABC321

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)

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

main calls hanoi(3, 'A', 'B', 'C'). Each box in the tree is a call: n:from→to.

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

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.

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

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.

A32BC13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Line 6: hanoi(1,A,B,C): a new frame is pushed. n == 1 → base case: disk 1, A → C.

A32BC13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: the frame is popped and control goes back to the caller.

A3B2C13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

The left subcall is done; only disk 2 is left on peg A → Disk 2: A → B.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Line 8: hanoi(1,C,A,B): a new frame is pushed. n == 1 → base case: disk 1, C → B.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: the frame is popped and control goes back to the caller.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(2,A,C,B) ends; its frame is popped.

AB21C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

The left subcall is done; only disk 3 is left on peg A → Disk 3: A → C.

AB21C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1: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.

A1B2C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Line 6: hanoi(1,B,C,A): a new frame is pushed. n == 1 → base case: disk 1, B → A.

A1B2C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: the frame is popped and control goes back to the caller.

A1BC323:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

The left subcall is done; only disk 2 is left on peg B → Disk 2: B → C.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Line 8: hanoi(1,A,B,C): a new frame is pushed. n == 1 → base case: disk 1, A → C.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: the frame is popped and control goes back to the caller.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(2,B,A,C) ends; its frame is popped.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(3,A,B,C) ends; its frame is popped. 7 calls in the tree = 7 moves = 2³ − 1.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

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.
chooseadvance (call)undonext optionsolution completeor no optionsleft → return
As in Hanoi, each call makes several recursive calls; the call tree is the tree of all choices tried (the decision tree).

Hanoi and Backtracking

Listing Subsets

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]
  • 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}

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

main calls subsets(a, 0, s) with a = {1, 2, 3} and an empty s.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 0: take a[0] = 1 (s = [1]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 1: take a[1] = 2 (s = [1, 2]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 2: take a[2] = 3 (s = [1, 2, 3]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [1, 2, 3].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 3 (s = [1, 2]); with the same i, go down the right branch to the subsets without 3.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [1, 2].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 2 (s = [1]); with the same i, go down the right branch to the subsets without 2.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 2: take a[2] = 3 (s = [1, 3]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [1, 3].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 3 (s = [1]); with the same i, go down the right branch to the subsets without 3.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [1].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 1 (s = []); with the same i, go down the right branch to the subsets without 1.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 1: take a[1] = 2 (s = [2]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 2: take a[2] = 3 (s = [2, 3]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [2, 3].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 3 (s = [2]); with the same i, go down the right branch to the subsets without 3.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [2].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 2 (s = []); with the same i, go down the right branch to the subsets without 2.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 2: take a[2] = 3 (s = [3]) and go down the left branch.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [3].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

Back from the left branch. Undo 3 (s = []); with the same i, go down the right branch to the subsets without 3.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

i = 3 = a.length: base case. All decisions are made → print [].

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]running nowon the stack, waitingreturned a valuenot called yet

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);
  }
}
emptyAABABCACACBBBABACBCBCACCACABCBCBA
  • 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.
A call makes as many recursive calls as there are letters left: multiple recursion. 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?
MethodCallsRecurrenceSolution
factorial, sum, countdownone call, n − 1T(n) = T(n − 1) + cO(n)
reverseone call, n − 2T(n) = T(n − 2) + cO(n)
binarySearch, fastPowerone call, n / 2T(n) = T(n/2) + cO(log n)
hanoitwo calls, n − 1T(n) = 2T(n − 1) + cO(2ⁿ)
fibtwo calls, n − 1 and n − 2T(n) = T(n − 1) + T(n − 2) + cexponential
To solve a recurrence, draw the call tree: if every node does constant work, the total work ≈ c · (number of calls).

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)
ncn − 1cn − 2c…1c0cn + 1 calls × c work
  • 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)

16c8c4c2c1clog₂ 16 + 1 = 5 calls
n161,0241,000,000
halving chain (calls)51120

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ⁿ.
n = 4: 1 calln = 3: 2 callsn = 2: 4 callsn = 1: 8 callstotal 1 + 2 + 4 + 8 = 15 = 2⁴ − 1
The tree grows exponentially, but its depth is n: at most n frames are on the stack at once.

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).
f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)
CallTotal callsDeepest chainTimeSpace
factorial(4)44O(n)O(n)
fib(40)331,160,28140exponentialO(n)
hanoi(3, …)73O(2ⁿ)O(n)
subsets, n = 3154O(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 recursive: countdown, reverse, binarySearch, factTail. Not tail recursive: factorial, sum, fib, countUp (work remains after the call).

Tail Recursion · Step through the code

Code Trace: factTail(4, 1)

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)factTail(1, 24)return acc→ call · ← return

main calls factTail(4, 1); acc starts at 1.

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)factTail(1, 24)return acc→ call · ← return

n = 4 > 1 → multiply first: 4 · 1 = 4. Then call factTail(3, 4).

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)factTail(1, 24)return acc→ call · ← return

n = 3 > 1 → multiply first: 3 · 4 = 12. Then call factTail(2, 12).

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)factTail(1, 24)return acc→ call · ← return

n = 2 > 1 → multiply first: 2 · 12 = 24. Then call factTail(1, 24).

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)factTail(1, 24)return acc= 24→ call · ← return

n = 1: base case → return acc = 24. The result is ready right here.

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)factTail(2, 12)call: (1, 24)= 24factTail(1, 24)return acc= 24→ call · ← return

factTail(2, 12) passes 24 on unchanged: there is nothing to do on the way back.

factTail(4, 1)call: (3, 4)factTail(3, 4)call: (2, 12)= 24factTail(2, 12)call: (1, 24)= 24factTail(1, 24)return acc= 24→ call · ← return

factTail(3, 4) passes 24 on unchanged: there is nothing to do on the way back.

factTail(4, 1)call: (3, 4)= 24factTail(3, 4)call: (2, 12)= 24factTail(2, 12)call: (1, 24)= 24factTail(1, 24)return acc= 24→ call · ← return

factTail(4, 1) passes 24 on unchanged: there is nothing to do on the way back.

factTail(4, 1)call: (3, 4)= 24factTail(3, 4)call: (2, 12)= 24factTail(2, 12)call: (1, 24)= 24factTail(1, 24)return acc= 24→ call · ← return

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 binarySearch converted this way.
  • The loop runs in constant memory, O(1), with no frames pushed.
Java does not optimize tail calls: the compiler and the JVM push a new frame for every call to 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?

CriterionRecursionLoop
Readabilityshort, close to the definitionstate variables managed by hand
MemoryO(depth) call stackusually O(1)
Speedevery call has overheadusually faster
Depth limitrisk of StackOverflowErrornone
Recursion is the natural choiceNested structures such as trees, backtracking (subsets, permutations), divide-in-half methods, Hanoi. The depth is small (log n, or a small n).
A loop is betterSimple counting and summing; linear recursion whose depth reaches n (long lists, large n); binary recursion with repeated subproblems (fib).
Any recursive method can be turned into a loop by doing the call stack's job with an explicit stack.

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.
head739nullrest of list: node.next
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)

head739null

main calls length(head); node points to the first node (7).

head739null

node ≠ null → 1 + length(node.next): a call for the rest of the list (starting at 3); this frame waits.

head739null

node ≠ null → 1 + length(node.next): a call for the rest of the list (starting at 9); this frame waits.

head739null

node ≠ null → 1 + length(node.next): node.next = null, so the call is for the empty list.

head739null↑ 0

node = null: base case; the empty list has length 0.

head739↑ 1null↑ 0

The rest of the list gave 0 → return 1 + 0 = 1.

head73↑ 29↑ 1null↑ 0

The rest of the list gave 1 → return 1 + 1 = 2.

head7↑ 33↑ 29↑ 1null↑ 0

The rest of the list gave 2 → return 1 + 2 = 3.

head7↑ 33↑ 29↑ 1null↑ 0

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

head739null
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

Missing or unreachable base caseIf 0 or a negative input skips the base case, the calls never end: StackOverflowError. n <= 1 is safer than n == 1.
Problem doesn't shrinkIf f(n) calls f(n) or f(n + 1) again, it never gets closer to the base case.
Losing the return valueForget the 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.
Forgetting to undoWithout s.remove(...) in backtracking, one branch's choices leak into the next, and the subsets come out wrong.
Repeated workIn binary recursion like fib, the same subproblem is solved an exponential number of times. Memoization or a loop gives O(n).
Deep linear recursionOn a list of a million nodes, 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

ExampleKind of recursionTimeSpace (stack)
factorial, sum, reverselinearO(n)O(n)
fastPower, binarySearchlinear, halvingO(log n)O(log n)
fibbinaryexponential (≈ 1.6ⁿ)O(n)
hanoibinaryO(2ⁿ)O(n)
subsetsbacktrackingO(n · 2ⁿ)O(n)
permmultiple, backtrackingO(n · n!)O(n)
factTailtail recursionO(n)O(n) (in Java)
length, printReverselinear (linked list)O(n)O(n)
Recursion = a base case + a recursive step that moves toward it. Time comes from the number of calls, space from the deepest chain of calls.

Summary

Interactive Simulators

Towers of Hanoi SimulatorSolve the puzzle by hand with 1–6 disks, step forward and back through the shortest solution, or let it solve itself. Your move count is compared with the minimum (2ⁿ − 1): 63 for n = 6.
Each peg behaves like a stack: only the top disk can be taken, and it can only go onto a larger disk.

Summary

Sample Problems

  1. For int f(int n) { if (n <= 0) return 0; return n % 10 + f(n / 10); }, what is f(2026), and how many calls are made?
  2. What does countUp(4) print? At most how many frames (including main) are on the stack?
  3. How many calls does fastPower(3, 13) make, and what is the result?
  4. How many calls does fib(6) make?
  5. For {1, 2, 3, 4}, how many subsets does subsets print, and how many calls does it make?
  6. If T(n) = T(n/2) + c and T(1) = c, how many calls are in the chain for n = 64?
Show answers
  1. Digit sum: 6 + 2 + 0 + 2 = 10; calls 2026, 202, 20, 2, 0 → 5.
  2. 1 2 3 4; countUp(4 … 0) = 5 frames + main = 6.
  3. n: 13, 6, 3, 1, 0 → 5 calls; 3¹³ = 1,594,323.
  4. 2 · F(7) − 1 = 2 · 13 − 1 = 25 calls (fib(6) = 8).
  5. 2⁴ = 16 subsets; 2⁵ − 1 = 31 calls.
  6. 64, 32, 16, 8, 4, 2, 1 → 7 = log₂ 64 + 1.

Summary

Exercises

  1. While factorial(5) runs, what is the largest number of frames (including main) on the call stack?
  2. How many moves does Hanoi take with 4 disks?
  3. What are the base case and the recursive step of a recursive largest(a, n) that returns the largest item of an array?
  4. isPalindrome(s, i, j) compares the ends and moves inward. How many calls does it make for "kayak"?
  5. 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)?
  6. In the loop version of factTail, how do n and acc change on each iteration?
Answers / hints
  1. factorial(5), (4), (3), (2), (1) + main = 6.
  2. 2⁴ − 1 = 15 moves.
  3. Base: n = 1 → a[0]. Step: max(largest(a, n − 1), a[n − 1]). O(n).
  4. (0, 4) k = k → (1, 3) a = a → (2, 2) base: 3 calls, true.
  5. g(5) = 2⁴ = 16; 2⁵ − 1 = 31 calls. One call is enough: return 2 * g(n - 1); → O(n).
  6. 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.

The End

Chapter 4: Recursion
Next deck: Chapter 5: Linked Lists · Study questions · Simulator: Towers of Hanoi Simulator · Glossary
1 / 1 Sercan KÜLCÜ. All rights reserved.