Skip to content

Recurrences and the Master Theorem

Merge sort, binary search, and most “divide and conquer” algorithms all share one shape: split the problem, solve the pieces recursively, combine the results. A recurrence relation captures that shape as an equation, and solving it tells you the Big-O without simulating a single run.

What you’ll learn

  • What a recurrence relation is and where it comes from.
  • The recursion tree method — solving a recurrence by hand, level by level.
  • The Master Theorem and its three cases.
  • How merge sort and binary search reduce to closed-form Big-O.
  • A small simulator that computes recurrence cost numerically.

Where a recurrence comes from

A divide-and-conquer algorithm that splits an input of size nn into aa subproblems of size n/bn/b, then spends f(n)f(n) extra work combining the results, has total cost:

T(n)=aT(n/b)+f(n)T(n) = a \, T(n/b) + f(n)
  • aa — how many subproblems you recurse into.
  • bb — how much smaller each subproblem is (nn shrinks to n/bn/b).
  • f(n)f(n) — the work done outside the recursive calls (splitting + combining).

Merge sort, for example, splits into 2 halves (a=2a = 2, b=2b = 2) and spends O(n)O(n) merging them back together — giving T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n).

The recursion tree method

Draw the recursion as a tree: the root does f(n)f(n) work, its aa children each do f(n/b)f(n/b) work, their children do f(n/b2)f(n/b^2), and so on until subproblems hit the base case. Summing every level’s cost gives the total.

diagram Recursion tree for merge sort: T(n) = 2T(n/2) + O(n) mermaid

Every level’s costs add up to roughly nn (the halves sum back to the whole), and there are log2n\log_2 n levels before subproblems reach size 1. So total cost is nlog2n=O(nlogn)n \cdot \log_2 n = O(n \log n) — no need to draw the whole tree once you see the pattern.

The Master Theorem

Drawing a tree every time is tedious. The Master Theorem gives a direct answer for T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) (with a1a \ge 1, b>1b > 1) by comparing f(n)f(n) to nlogban^{\log_b a} — the cost if there were no extra work at all:

Case 1: f(n)=O ⁣(nlogbaϵ) for some ϵ>0    T(n)=Θ ⁣(nlogba)Case 2: f(n)=Θ ⁣(nlogba)    T(n)=Θ ⁣(nlogbalogn)Case 3: f(n)=Ω ⁣(nlogba+ϵ) and af(n/b)cf(n) for some c<1    T(n)=Θ ⁣(f(n))\begin{aligned} \textbf{Case 1: } & f(n) = O\!\left(n^{\log_b a - \epsilon}\right) \text{ for some } \epsilon > 0 \;\Rightarrow\; T(n) = \Theta\!\left(n^{\log_b a}\right) \\[4pt] \textbf{Case 2: } & f(n) = \Theta\!\left(n^{\log_b a}\right) \;\Rightarrow\; T(n) = \Theta\!\left(n^{\log_b a} \log n\right) \\[4pt] \textbf{Case 3: } & f(n) = \Omega\!\left(n^{\log_b a + \epsilon}\right) \text{ and } a f(n/b) \le c f(n) \text{ for some } c < 1 \;\Rightarrow\; T(n) = \Theta\!\left(f(n)\right) \end{aligned}

In plain language: if the combine step is smaller than the recursive work, the recursion dominates (Case 1). If they’re equal, add a logn\log n factor (Case 2). If the combine step dominates, the top level alone sets the cost (Case 3).

diagram Master Theorem — which case applies? mermaid

Worked examples

Merge sort: T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n). Here a=2a = 2, b=2b = 2, so logba=log22=1\log_b a = \log_2 2 = 1, and f(n)=O(n1)f(n) = O(n^1) — the same order. That’s Case 2, so:

T(n)=Θ(nlogn)T(n) = \Theta(n \log n)

Binary search: T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1). Here a=1a = 1, b=2b = 2, so logba=log21=0\log_b a = \log_2 1 = 0, and f(n)=O(n0)=O(1)f(n) = O(n^0) = O(1) — again the same order. Also Case 2, giving:

T(n)=Θ(logn)T(n) = \Theta(\log n)

A recurrence-cost simulator

Instead of drawing trees by hand, you can compute T(n)T(n) numerically by literally implementing the recurrence, and compare it to the closed-form prediction:

recurrence_simulator.py
def recurrence_cost(n, a, b, f, base=1):
    """Numerically evaluate T(n) = a * T(n // b) + f(n)."""
    if n <= base:
        return 1
    return a * recurrence_cost(n // b, a, b, f, base) + f(n)
 
n = 64
 
# Merge sort: T(n) = 2T(n/2) + n  ->  should track n * log2(n)
merge_cost = recurrence_cost(n, a=2, b=2, f=lambda n: n)
print("merge sort cost:  ", merge_cost, " ~ n log n =", n * n.bit_length())
 
# Binary search: T(n) = T(n/2) + 1  ->  should track log2(n)
search_cost = recurrence_cost(n, a=1, b=2, f=lambda n: 1)
print("binary search cost:", search_cost, " ~ log n =", n.bit_length() - 1)
recurrence_simulator.py
def recurrence_cost(n, a, b, f, base=1):
    """Numerically evaluate T(n) = a * T(n // b) + f(n)."""
    if n <= base:
        return 1
    return a * recurrence_cost(n // b, a, b, f, base) + f(n)
 
n = 64
 
# Merge sort: T(n) = 2T(n/2) + n  ->  should track n * log2(n)
merge_cost = recurrence_cost(n, a=2, b=2, f=lambda n: n)
print("merge sort cost:  ", merge_cost, " ~ n log n =", n * n.bit_length())
 
# Binary search: T(n) = T(n/2) + 1  ->  should track log2(n)
search_cost = recurrence_cost(n, a=1, b=2, f=lambda n: 1)
print("binary search cost:", search_cost, " ~ log n =", n.bit_length() - 1)

Practice

Drill 1 — the critical exponent. logba\log_b a is the exponent every Master Theorem case compares against. Compute it with math.log(a, b)math.log(a, b).

Drill 2 — simulate the recurrence. Finish the recursive call so the simulator actually recurses on the smaller subproblem.

Drill 3 — pick the Master Theorem case. Given the critical exponent and f(n)f(n)’s exponent, decide which case applies.

Recap

  • Divide-and-conquer cost is captured by T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n).
  • The recursion tree method sums cost per level; number of levels is logbn\log_b n.
  • The Master Theorem gives the closed form directly by comparing f(n)f(n) to nlogban^{\log_b a} — no tree required.
  • Merge sort: Θ(nlogn)\Theta(n \log n). Binary search: Θ(logn)\Theta(\log n). Both land in Case 2.

Next: Space Complexity and the Call Stack — what recursion actually costs in memory, not just time.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did