SEN2211 · Week 11 · Recursive Complexity

Recursive complexity: recurrence relations

Writing the running time of a recursive method as a recurrence T(n), and solving it by unrolling, the recursion tree and the master theorem. The exam has “write the recurrence and give the Big-O” questions.

recurrence relationT(n)unrolling (iteration method)recursion treemaster theoremdivide and conquerlogarithmic depthexponential growthspace complexitycall stack depth

In week 10 we wrote recursive methods. Now the question is: how fast are these methods? In loops we counted the number of rounds. In recursion, however, the method’s time depends on its own time: the time of factorial(n) is the time of factorial(n - 1) plus a little work. We write this as a recurrence relation and then solve it. The exam asks for two steps: extract the recurrence from the code, then find the Big-O from the recurrence. If you recognize the ten recurrences on this page, you can answer most of the questions by heart.

Golden rules:

  1. T(n) = (how many calls) · T(size of each call) + (work done outside the calls).
  2. Recursion that shrinks by n − 1 has linear depth; one that shrinks by n / 2 has logarithmic depth.
  3. Two calls + n − 1 = exponential. Two calls + n / 2 = linear or n log n.
  4. In a recursion tree: number of levels × work per level.

From code to recurrence

Ask three questions: how much work is there in the base case? How many recursive calls are there and with what size each? How much work is there outside the calls?

CodeRecurrence
return n * f(n - 1);T(n)=T(n−1)+cT(n) = T(n-1) + c
for (i < n) ...; f(n - 1);T(n)=T(n−1)+nT(n) = T(n-1) + n
return f(n / 2) + 1;T(n)=T(n/2)+cT(n) = T(n/2) + c
for (i < n) ...; f(n / 2);T(n)=T(n/2)+nT(n) = T(n/2) + n
f(n / 2); f(n / 2);T(n)=2T(n/2)+cT(n) = 2T(n/2) + c
f(left half); f(right half); merge (n);T(n)=2T(n/2)+nT(n) = 2T(n/2) + n
f(n - 1); f(n - 1);T(n)=2T(n−1)+cT(n) = 2T(n-1) + c

Constant work (c) doesn’t affect the Big-O; it is usually written as 1.

Method 1: unrolling

Substitute the recurrence into itself a few times, see the pattern, and carry it down to the base case.

T(n)=T(n−1)+nT(n) = T(n-1) + n, T(1)=1T(1) = 1

T(n) = T(n-1) + n
     = T(n-2) + (n-1) + n
     = T(n-3) + (n-2) + (n-1) + n
     ...
     = T(1) + 2 + 3 + ... + n
     = 1 + 2 + ... + n = n(n+1)/2   → O(n²)

T(n)=T(n/2)+1T(n) = T(n/2) + 1, T(1)=1T(1) = 1

T(n) = T(n/2) + 1
     = T(n/4) + 2
     = T(n/8) + 3
     ...
     = T(n/2^k) + k      when n/2^k = 1, k = log2 n
     = 1 + log2 n        → O(log n)

T(n)=2T(n−1)+1T(n) = 2T(n-1) + 1, T(0)=0T(0) = 0

T(n) = 2T(n-1) + 1
     = 4T(n-2) + 2 + 1
     = 8T(n-3) + 4 + 2 + 1
     ...
     = 2^n T(0) + (2^(n-1) + ... + 2 + 1)
     = 2^n - 1           → O(2^n)

The values the program computed directly (the ratios as n doubles are in parentheses):

T(n-1)+1: T(10)=10 T(1000)=1000 (x100.00) T(2000)=2000 (x2.00) T(4000)=4000 (x2.00)
T(n-1)+n: T(10)=55 T(1000)=500500 (x9100.00) T(2000)=2001000 (x4.00) T(4000)=8002000 (x4.00)
T(n/2)+1: T(16)=5 T(1024)=11 (x2.20) T(2048)=12 (x1.09) T(1048576)=21 (x1.75)
2T(n-1)+1: T(5)=31 T(10)=1023 (x33.00) T(20)=1048575 (x1025.00) T(21)=2097151 (x2.00)

Note the last line: when n increases by only 1 (20 → 21), the value doubles. This is exponential growth.

Method 2: the recursion tree

Each node shows a call’s own work; the total time is the sum of all the nodes.

T(n)=2T(n/2)+nT(n) = 2T(n/2) + n

level 0:                  n                         total n
level 1:           n/2          n/2                 total n
level 2:       n/4    n/4    n/4    n/4             total n
   ...
level log n:   1  1  1  1  ...  1   (n leaves)      total n

The number of levels is log⁡2n+1\log_2 n + 1, with n work at each level: T(n)=nlog⁡2n+nT(n) = n\log_2 n + n. For n = 8 that is 8 · 3 + 8 = 32, and 11 264 for n = 1024. The program found the same:

2T(n/2)+n: T(8)=32 T(1024)=11264 (x352.00) T(2048)=24576 (x2.18) T(4096)=53248 (x2.17)

The tree of T(n)=T(n/2)+nT(n) = T(n/2) + n has a single branch: n, n/2, n/4, … The total is n(1+12+14+… )<2nn(1 + \frac12 + \frac14 + \dots) < 2n. The work at the root dominates: O(n).

In the tree of T(n)=2T(n/2)+1T(n) = 2T(n/2) + 1 the work per level is 1, 2, 4, …, n. The total is 2n−12n - 1. The leaves dominate: O(n).

TIP · Who dominates in the tree? If the work per level decreases going down, the root dominates (the answer is the work at the root). If it is constant, multiply by the number of levels (a log n factor). If it increases, the leaves dominate (the answer is the number of leaves). The master theorem is the formula form of this.

Method 3: the master theorem

For recurrences of the form T(n)=a T(n/b)+ndT(n) = a\,T(n/b) + n^d (a≥1a \ge 1, b>1b > 1). Compute k=log⁡bak = \log_b a and compare it with d:

CaseResultIntuition
d>kd > kΘ(nd)\Theta(n^d)the work at the root dominates
d=kd = kΘ(ndlog⁡n)\Theta(n^d \log n)every level is equal
d<kd < kΘ(nk)\Theta(n^k)the leaves dominate
Recurrencea, b, dkResult
T(n/2)+1T(n/2) + 11, 2, 00d=kd = k → Θ(log⁡n)\Theta(\log n)
T(n/2)+nT(n/2) + n1, 2, 10d>kd > k → Θ(n)\Theta(n)
2T(n/2)+12T(n/2) + 12, 2, 01d<kd < k → Θ(n)\Theta(n)
2T(n/2)+n2T(n/2) + n2, 2, 11d=kd = k → Θ(nlog⁡n)\Theta(n\log n)
2T(n/2)+n22T(n/2) + n^22, 2, 21d>kd > k → Θ(n2)\Theta(n^2)
3T(n/3)+n3T(n/3) + n3, 3, 11d=kd = k → Θ(nlog⁡n)\Theta(n\log n)
4T(n/2)+n4T(n/2) + n4, 2, 12d<kd < k → Θ(n2)\Theta(n^2)
8T(n/2)+n28T(n/2) + n^28, 2, 23d<kd < k → Θ(n3)\Theta(n^3)

Checking with the program (expected ratio when n doubles: x2 for n, x4 for n², x8 for n³):

4T(n/2)+n: T(1024)=2096128 T(2048)=8386560 (x4.00) T(4096)=33550336 (x4.00)
T(n/2)+n: T(1024)=2047 T(2048)=4095 (x2.00) T(4096)=8191 (x2.00)
2T(n/2)+1: T(1024)=2047 T(2048)=4095 (x2.00) T(4096)=8191 (x2.00)
8T(n/2)+n^2: T(256)=33488896 T(512)=268173312 (x8.01) T(1024)=2146435072 (x8.00)
2T(n/2)+n^2: T(1024)=2096128 T(2048)=8386560 (x4.00) T(4096)=33550336 (x4.00)

PITFALL · The master theorem does not apply to n − 1. In forms like T(n)=T(n−1)+nT(n) = T(n-1) + n or 2T(n−1)+12T(n-1) + 1 there is no division, there is subtraction. Solve these by unrolling.

Counting in real code

So that the recurrences don’t stay abstract, we counted operations in real recursive code:

static void halfLoop(int n) {           // T(n) = T(n/2) + n
    if (n <= 1) return;
    for (int i = 0; i < n; i++) ops++;
    halfLoop(n / 2);
}

static void twoCalls(int n) {           // T(n) = 2T(n-1) + 1
    ops++;
    if (n == 0) return;
    twoCalls(n - 1);
    twoCalls(n - 1);
}

static void splitLoop(int lo, int hi) { // T(n) = 2T(n/2) + n
    if (hi - lo < 1) return;
    for (int i = lo; i <= hi; i++) ops++;
    int mid = (lo + hi) / 2;
    splitLoop(lo, mid);
    splitLoop(mid + 1, hi);
}
halfLoop n=1024 ops=2046  halfLoop n=2048 ops=4094  halfLoop n=4096 ops=8190  
bsearch(miss) n=1024 calls=11  bsearch(miss) n=2048 calls=12  bsearch(miss) n=1048576 calls=21  
twoCalls n=3 calls=15  twoCalls n=10 calls=2047  twoCalls n=11 calls=4095  twoCalls n=12 calls=8191  
splitLoop n=1024 ops=10240  splitLoop n=2048 ops=22528  splitLoop n=4096 ops=49152  
loopThenOne n=10 ops=55  loopThenOne n=1000 ops=500500  loopThenOne n=2000 ops=2001000  

For splitLoop, 1024 · 10 = 10 240: exactly n log₂ n. For loopThenOne (a loop of n + f(n − 1)), n(n+1)/2.

Space complexity

Recursion has a hidden cost: every open call holds a stack frame. The extra memory is the length of the deepest call chain.

MethodTimeExtra memory (stack)
Recursive list sum / sizeO(n)O(n)
Recursive binary searchO(log n)O(log n)
Naive fibaround O(2ⁿ)O(n) (depth n, not width)
Merge sortO(n log n)O(log n) stack + O(n) auxiliary array

In naive fib the number of calls is exponential but the number of calls open at the same time is at most n: one branch is not left before it finishes.

Common mistakes

WrongCorrect
Two recursive calls are always O(2ⁿ)Yes with n − 1; with n / 2 it is O(n) or O(n log n)
T(n)=T(n/2)+nT(n) = T(n/2) + n → O(n log n)A geometric series: O(n)
T(n)=2T(n/2)+nT(n) = 2T(n/2) + n → O(n²)n per level, log n levels: O(n log n)
Applying the master theorem to T(n−1)T(n-1)Use the unrolling method
Computing log⁡ba\log_b a wrongly (log⁡28=4\log_2 8 = 4)log⁡28=3\log_2 8 = 3
Recursion uses no extra memoryFrames as deep as the depth: O(n) for a list
If there is a call inside a loop, counting only the loopThe loop work + the work of the calls, both enter the recurrence

Test yourself

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

Question 1easy

Write the recurrence for the running time of factorial(n) (if (n <= 1) return 1; return n * factorial(n - 1);) and give its Big-O.

Show answer
T(n) = T(n-1) + c, O(n)

Each call does constant work (a comparison, a multiplication) and calls itself once with n − 1. Unrolling: T(n)=T(n−1)+c=T(n−2)+2c=⋯=T(1)+(n−1)cT(n) = T(n-1) + c = T(n-2) + 2c = \dots = T(1) + (n-1)c. Linear: O(n).

Question 2medium

Solve T(n)=T(n−1)+nT(n) = T(n-1) + n, T(1)=1T(1) = 1. What is T(10)T(10) and the Big-O?

Show answer
T(n) = n(n+1)/2, T(10) = 55, O(n^2)

Unrolling: T(n)=n+(n−1)+⋯+2+1=n(n+1)2T(n) = n + (n-1) + \dots + 2 + 1 = \frac{n(n+1)}{2}. T(10) = 55. With the program: T(1000) = 500 500, T(2000) = 2 001 000 (x4.00). Example code: doing a loop of n in each call and calling again with n − 1 (the recursive form of selection sort).

Question 3easy

Solve T(n)=T(n/2)+1T(n) = T(n/2) + 1, T(1)=1T(1) = 1 for n=16n = 16. What is the Big-O?

Show answer
5, O(log n)

16 → 8 → 4 → 2 → 1: four halvings, each +1, plus T(1) = 1. Total 5 = log⁡216+1\log_2 16 + 1. Measurement: T(1024) = 11, T(1 048 576) = 21. This is the recurrence of binary search.

Question 4medium

Solve T(n)=2T(n/2)+nT(n) = 2T(n/2) + n, T(1)=1T(1) = 1 for n=8n = 8. What is the Big-O?

Show answer
32, O(n log n)

Closed form nlog⁡2n+nn\log_2 n + n: 8 · 3 + 8 = 32. In the recursion tree the total work at each level is n, and the number of levels is log⁡2n\log_2 n. Measurement: T(1024) = 11 264, T(2048) = 24 576 (x2.18). The recurrence of merge sort.

Question 5medium

Solve T(n)=2T(n−1)+1T(n) = 2T(n-1) + 1, T(0)=0T(0) = 0. What is T(5)T(5) and the Big-O?

Show answer
31, O(2^n)

T(1) = 1, T(2) = 3, T(3) = 7, T(4) = 15, T(5) = 31: T(n)=2n−1T(n) = 2^n - 1. When n increases by one, the time doubles (T(20) = 1 048 575, T(21) = 2 097 151). The Towers of Hanoi and the g(n) method from week 10 fit this recurrence.

Question 6medium

How many calls does f(10) make in total (including the first)? What is the Big-O?

static void f(int n) {
    if (n == 0) return;
    f(n - 1);
    f(n - 1);
}
Show answer
2047, O(2^n)

The number of calls is C(n) = 2C(n−1) + 1, C(0) = 1, that is C(n)=2n+1−1C(n) = 2^{n+1} - 1. C(10) = 2047. With a counter: n = 10 → 2047, 11 → 4095, 12 → 8191. Since both calls are made with n − 1, the tree is full and exponential.

Question 7medium

Use the master theorem to give the Big-O of T(n)=4T(n/2)+nT(n) = 4T(n/2) + n.

Show answer
O(n^2)

a = 4, b = 2, d = 1. log⁡ba=log⁡24=2\log_b a = \log_2 4 = 2. d = 1 < 2: the leaves dominate, T(n)=Θ(nlog⁡ba)=Θ(n2)T(n) = \Theta(n^{\log_b a}) = \Theta(n^2). Measurement: when n doubles T grows x4.00.

Question 8hard

What is the Big-O of this method? Write its recurrence first.

static void h(int n) {
    if (n <= 1) return;
    for (int i = 0; i < n; i++)
        count++;
    h(n / 2);
}
Show answer
T(n) = T(n/2) + n, O(n)

The loop is n work, then a single call with n/2. Unrolling: n+n2+n4+⋯<2nn + \frac{n}{2} + \frac{n}{4} + \dots < 2n. A geometric series, the total is O(n). Counter: n = 1024 → 2046, 2048 → 4094 (x2). Master theorem: a = 1, b = 2, d = 1 > log₂1 = 0 → Θ(n).

Question 9hard

Give the Big-O for (a) T(n)=3T(n/3)+nT(n) = 3T(n/3) + n (b) T(n)=8T(n/2)+n2T(n) = 8T(n/2) + n^2 (c) T(n)=2T(n/2)+1T(n) = 2T(n/2) + 1.

Show answer
(a) O(n log n) (b) O(n^3) (c) O(n)

(a) log⁡33=1=d\log_3 3 = 1 = d: equal, Θ(nlog⁡n)\Theta(n\log n). (b) log⁡28=3>d=2\log_2 8 = 3 > d = 2: Θ(n3)\Theta(n^3); in the measurement n x2 → T x8.00. (c) log⁡22=1>d=0\log_2 2 = 1 > d = 0: Θ(n)\Theta(n); T(1024) = 2047, T(2048) = 4095.

Question 10easy

What is the running time of recursive binary search on a sorted array of size n? About how many calls are made for an unsuccessful search when n=220n = 2^{20}?

Show answer
O(log n), about 21 calls

Each call does constant work and makes a single call with half of the range: T(n) = T(n/2) + c → O(log n). With 2²⁰ elements there are 20 halvings and one more call for the empty range: the counter said 21. About 20 steps for a million elements.

Question 11medium

For T(n)=2T(n/2)+nT(n) = 2T(n/2) + n, draw the recursion tree. How many levels are there, how much work is done on each level, and what is the total?

Show answer
log2 n levels (plus leaves), n work per level, total n log n

level 0:            n                      = n
level 1:       n/2     n/2                 = n
level 2:    n/4 n/4  n/4 n/4               = n
...
level k:  2^k pieces, each n/2^k           = n
last level: n copies of T(1)               = n
We stop when n/2^k = 1: k=log2nk = \\log_2 n. Each level is n work, the total is nlog2n+nn\\log_2 n + n → O(n log n).

Question 12hard

What is the space complexity (extra memory) of the recursive sum(Node p) (return p.data + sum(p.next);) on a list of n nodes, and of recursive binary search?

Show answer
sum: O(n); binary search: O(log n)

The extra memory is the maximum number of stack frames open at the same time. sum opens one frame for each node, and all of them wait until the base case: n + 1 frames, O(n). In binary search the depth is log n: O(log n). Written with a loop, both use O(1) memory.

Verification: Each recurrence was computed directly in verify/SEN2211/W11.java (java -Xss64m W11.java) and compared with closed formulas (e.g. n log n + n for 2T(n/2)+n: T(8) = 32, T(1024) = 11264). The operation/call counts were also counted in real recursive code; when n doubles the ratio (x2, x4, +1, ~x2.2) was seen to match the claimed class.

Review card

Recursive time: T(n) = (number of calls) × T(subproblem) + (work outside the calls) T(n) = T(n-1) + 1 → O(n) factorial, list size/sum T(n) = T(n-1) + n → O(n²) n(n+1)/2; recursive selection sort T(n) = T(n/2) + 1 → O(log n) binary search, log2 n + 1 T(n) = T(n/2) + n → O(n) n + n/2 + n/4 + ... < 2n T(n) = 2T(n/2) + 1 → O(n) 2n - 1 T(n) = 2T(n/2) + n → O(n log n) merge sort; n log n + n T(n) = 2T(n-1) + 1 → O(2ⁿ) 2ⁿ - 1; Hanoi; naive fib ~ O(1.6ⁿ) MASTER: T(n) = aT(n/b) + n^d, k = log_b a d > k → n^d d = k → n^d log n d < k → n^k Recursion tree: number of levels × work per level Memory: the deepest call chain (list O(n), binary search O(log n))