CMPUT-204 · Lesson 3 of 12 · 30 min

Analysing iterative and recursive algorithms

With asymptotic notation in hand, we can analyse real code. Iterative algorithms are analysed by counting how many times each loop body runs, which turns code into a summation. Recursive algorithms are analysed by writing a recurrence, an equation that expresses the running time on input size \(n\) in terms of the running time on smaller inputs, and then solving it. This lesson develops the summations you will need most often, three methods for solving recurrences, and the master theorem, which settles the recurrences that arise from most divide-and-conquer algorithms in one step.

Loops become sums

A loop that runs \(n\) times with a constant amount of work per iteration costs \(\Theta(n)\). When the work per iteration varies, add up the work iteration by iteration. The resulting sum may be one of a small family of standard forms. You proved several of these by induction in CMPUT-272 lesson 10; here they become tools.

Summations that appear again and again in algorithm analysis
SumClosed formAsymptotic value
1 + 2 + ... + nn(n + 1)/2Θ(n²)
1² + 2² + ... + n²n(n + 1)(2n + 1)/6Θ(n³)
1 + 2 + 4 + ... + 2ᵏ2ᵏ⁺¹ − 1Θ(2ᵏ), dominated by the last term
1 + r + r² + ... (r < 1)at most 1/(1 − r)Θ(1), dominated by the first term
1 + 1/2 + 1/3 + ... + 1/nHₙ ≈ ln n + 0.577Θ(log n)
log 1 + log 2 + ... + log nlog(n!)Θ(n log n)

Two patterns are worth memorizing. In an arithmetic series the terms grow by a constant, and the sum is the number of terms times the average term, so it is one degree higher than the largest term divided by \(n\). In a geometric series the terms grow by a constant factor, and the sum is within a constant factor of its largest term. That second fact is behind many surprising results, such as building a heap in linear time (lesson 5) and the amortized cost of a growing array (lesson 12).

Worked problem

A nested loop with a dependent bound

How many times does the inner statement run? for i in range(n): for j in range(i, n): work()

Show solution
  1. For a given i, the inner loop runs n − i times.
  2. Total = Σ over i = 0 to n − 1 of (n − i) = n + (n − 1) + ... + 1.
  3. That arithmetic series equals n(n + 1)/2.
  4. So the statement runs n(n + 1)/2 times, which is Θ(n²).

Answer: n(n + 1)/2 = Θ(n²). Starting the inner loop at i rather than 0 halves the work but does not change the growth rate.

Counting loop iterations and comparing with closed forms
def count_nested(n):
    c = 0
    for i in range(n):
        for j in range(i, n):
            c += 1
    return c

def count_doubling(n):
    c = 0
    i = 1
    while i < n:
        c += 1
        i *= 2
    return c

def count_doubling_inner(n):
    c = 0
    i = 1
    while i < n:
        for j in range(i):       # inner work grows with i
            c += 1
        i *= 2
    return c

for n in (10, 100, 1000, 1024):
    print(n, count_nested(n), n * (n + 1) // 2,
          count_doubling(n), count_doubling_inner(n))
Output
10 55 55 4 15
100 5050 5050 7 127
1000 500500 500500 10 1023
1024 524800 524800 10 1023

The second function shows the logarithmic pattern: a counter that doubles from 1 reaches \(n\) after \(\lceil \log_2 n \rceil\) steps, so 1000 takes 10 iterations. The third function is a trap for careless analysis. Its outer loop runs \(\Theta(\log n)\) times and its inner loop runs up to \(n\) times, so a quick product suggests \(\Theta(n \log n)\). But the inner loop runs \(1 + 2 + 4 + \cdots\) times, a geometric series dominated by its last term, which is less than \(n\). The total is less than \(2n\), so the function is \(\Theta(n)\), as the output confirms with 1,023 for both 1000 and 1024.

Recurrences from recursive code

For a recursive function, let \(T(n)\) be its worst-case running time on input size \(n\). Each call does some non-recursive work and makes recursive calls on smaller inputs, so \(T(n)\) equals the non-recursive work plus the sum of the recursive costs. The base case gives a constant. For merge sort, which lesson 4 studies in detail, the function splits a list of size \(n\) into two halves, sorts each recursively, and merges them in linear time:

\[T(n) = \begin{cases} c & n \le 1 \\ 2\,T(n/2) + c\,n & n > 1 \end{cases}\]

We usually ignore floors and ceilings, writing \(n/2\) for \(\lfloor n/2 \rfloor\) and \(\lceil n/2 \rceil\), and we usually replace the constant \(c\) with 1, because neither changes the asymptotic answer for the recurrences in this course. Binary search, which looks at the middle element and recurses on one half, gives \(T(n) = T(n/2) + c\). A function that recurses on \(n - 1\) after a linear scan, such as a naive recursive selection sort, gives \(T(n) = T(n-1) + c\,n\).

Method 1: unrolling and the recursion tree

The most intuitive method is to expand the recurrence repeatedly and look for a pattern. For merge sort, the top call does \(cn\) work and makes two calls on size \(n/2\). Each of those does \(cn/2\) work and makes two calls on size \(n/4\), and so on. Drawing this as a tree, level \(i\) has \(2^i\) nodes, each doing \(cn/2^i\) work, so each level costs \(cn\) in total. The sizes halve until they reach 1, which takes \(\log_2 n\) levels below the root, so there are \(\log_2 n + 1\) levels.

Recursion tree for T(n) = 2T(n/2) + n. The root costs n, the next level has two nodes costing n/2 each, then four nodes of n/4, down to n leaves of cost 1. Each level sums to n, and there are log n plus one levels.
The recursion tree for merge sort: every level costs n
\[T(n) = \sum_{i=0}^{\log_2 n} 2^i \cdot c\,\frac{n}{2^i} = c\,n\,(\log_2 n + 1) \in \Theta(n \log n)\]

The recursion tree tells you not only the answer but why. If the cost per level is the same at every level, the total is the per-level cost times the number of levels. If the cost per level shrinks geometrically as you go down, the root dominates and the total is within a constant of the root's cost. If the cost grows geometrically, the leaves dominate and the total is within a constant of the leaf level. These three shapes are exactly the three cases of the master theorem below.

Method 2: substitution

The substitution method guesses a bound and proves it by induction. It is the most powerful method, because it works for recurrences no formula covers, but it needs a good guess, often from a recursion tree. To prove \(T(n) \le c\, n \log_2 n\) for merge sort's recurrence \(T(n) = 2T(n/2) + n\) with \(T(1) = 1\), assume the bound holds for all smaller inputs, in the strong-induction style of CMPUT-272 lesson 10.

Worked problem

Proving the merge sort bound by substitution

Show T(n) = 2T(n/2) + n, T(1) = 1, satisfies T(n) ≤ n log₂ n + n for every power of two n ≥ 1.

Show solution
  1. Base case n = 1: T(1) = 1 and 1 · log₂ 1 + 1 = 1. True.
  2. Inductive step: assume T(n/2) ≤ (n/2) log₂(n/2) + n/2.
  3. Then T(n) = 2T(n/2) + n ≤ n log₂(n/2) + n + n = n(log₂ n − 1) + 2n.
  4. Simplify: n log₂ n − n + 2n = n log₂ n + n. The bound holds for n.

Answer: T(n) ≤ n log₂ n + n, so T(n) ∈ O(n log n). A matching lower bound follows the same way, giving Θ(n log n).

A classic pitfall in substitution is to prove a bound that is too weak to carry the induction. If you try to prove \(T(n) \le c\,n\) for the same recurrence, you get \(T(n) \le 2c(n/2) + n = cn + n\), which is not at most \(cn\). Writing \(cn + n = O(n)\) and declaring victory is wrong: the inductive hypothesis must be reproduced exactly, with the same constant, or the argument proves nothing.

Method 3: the master theorem

Divide-and-conquer algorithms usually produce recurrences of the form \(T(n) = a\,T(n/b) + f(n)\), where \(a \ge 1\) subproblems each have size \(n/b\) with \(b > 1\), and \(f(n)\) is the cost of dividing and combining. The master theorem compares \(f(n)\) with \(n^{\log_b a}\), the number of leaves in the recursion tree, and gives the answer directly in three cases. We state the common simplified version for \(f(n) = \Theta(n^d)\) with \(d \ge 0\).

\[T(n) = a\,T(n/b) + \Theta(n^d) \;\Rightarrow\; T(n) \in \begin{cases} \Theta(n^d) & d > \log_b a \\ \Theta(n^d \log n) & d = \log_b a \\ \Theta(n^{\log_b a}) & d < \log_b a \end{cases}\]

In the first case the root dominates: dividing and combining at the top costs more than everything below it. In the second case every level costs about the same, giving a logarithmic factor. In the third case the leaves dominate, because the number of subproblems multiplies faster than their sizes shrink. The general theorem allows any \(f(n)\) and adds a regularity condition in the first case, but the simplified version covers every recurrence in this course.

The master theorem applied to common recurrences
Recurrencea, b, dlog_b a vs dResultAlgorithm
T(n) = 2T(n/2) + n2, 2, 11 = 1Θ(n log n)Merge sort
T(n) = T(n/2) + 11, 2, 00 = 0Θ(log n)Binary search
T(n) = 3T(n/2) + n3, 2, 11.585 > 1Θ(n^1.585)Karatsuba multiplication (lesson 6)
T(n) = 7T(n/2) + n²7, 2, 22.807 > 2Θ(n^2.807)Strassen matrix multiplication
T(n) = 8T(n/2) + n²8, 2, 23 > 2Θ(n³)Naive recursive matrix multiplication
T(n) = 2T(n/2) + n²2, 2, 21 < 2Θ(n²)Root-dominated example
T(n) = T(n/2) + n1, 2, 10 < 1Θ(n)Selection by halving (lesson 6)

Worked problem

Strassen-style recurrence

A matrix algorithm splits each n × n matrix into four n/2 × n/2 blocks, makes 7 recursive multiplications and combines with Θ(n²) additions. Solve T(n) = 7T(n/2) + n².

Show solution
  1. Identify a = 7, b = 2, d = 2.
  2. Compute log₂ 7 ≈ 2.807.
  3. Compare: d = 2 < 2.807, so the leaves dominate (case 3).
  4. T(n) ∈ Θ(n^log₂ 7) ≈ Θ(n^2.807).

Answer: Θ(n^2.807). Saving one multiplication out of eight lowers the exponent from 3 to about 2.807, which beats the cubic method for large enough matrices.

Evaluating recurrences directly and comparing with the master theorem
import math
from functools import lru_cache

def master(a, b, d):
    e = math.log(a, b)
    if abs(d - e) < 1e-12:
        return f'Theta(n^{d} log n)'
    return f'Theta(n^{max(d, e):.3f})'

@lru_cache(maxsize=None)
def T(n, a, b, d):
    if n <= 1:
        return 1
    return a * T(n // b, a, b, d) + n ** d

for a, b, d in [(2, 2, 1), (1, 2, 0), (3, 2, 1), (7, 2, 2), (2, 2, 2)]:
    n1, n2 = 2 ** 10, 2 ** 11
    growth = T(n2, a, b, d) / T(n1, a, b, d)      # ratio when n doubles
    print(f'a={a} b={b} d={d}: {master(a, b, d):>18}, doubling ratio {growth:.3f}')
Output
a=2 b=2 d=1:   Theta(n^1 log n), doubling ratio 2.182
a=1 b=2 d=0:   Theta(n^0 log n), doubling ratio 1.091
a=3 b=2 d=1:     Theta(n^1.585), doubling ratio 3.012
a=7 b=2 d=2:     Theta(n^2.807), doubling ratio 7.006
a=2 b=2 d=2:     Theta(n^2.000), doubling ratio 4.001

The doubling ratios match the predictions. For \(\Theta(n^k)\) the ratio when \(n\) doubles approaches \(2^k\): about 3 for \(n^{1.585}\), about 7 for \(n^{2.807}\) and about 4 for \(n^2\). For merge sort the ratio is a little above 2, as expected for \(n \log n\), and for binary search the value grows by only 1 each time \(n\) doubles, the signature of a logarithm.

When the master theorem does not apply

The theorem needs subproblems of equal size \(n/b\) with a constant \(b > 1\). It says nothing about \(T(n) = T(n-1) + n\), which shrinks by subtraction, not division. Unrolling that recurrence gives \(n + (n-1) + \cdots + 1 = \Theta(n^2)\), the cost of the naive recursive selection sort. Similarly \(T(n) = 2T(n-1) + 1\), the Towers of Hanoi recurrence, unrolls to \(2^n - 1\). The simplified theorem also misses \(f(n)\) that are not plain powers, such as \(T(n) = 2T(n/2) + n \log n\), whose solution \(\Theta(n \log^2 n)\) follows from a recursion tree in which level \(i\) costs \(n \log(n/2^i)\). Recurrences with unequal parts, such as \(T(n) = T(n/5) + T(7n/10) + n\) from lesson 6, are solved by substitution.

Common mistakes

  • MistakeMultiplying the outer and inner loop bounds when the inner bound depends on the outer variable.

    InsteadWrite the exact sum; a doubling outer loop with an inner loop of length i is a geometric series and costs Θ(n), not Θ(n log n).

  • MistakeApplying the master theorem to T(n) = T(n − 1) + n.

    InsteadThe theorem needs division of n by a constant b > 1; unroll subtractive recurrences instead.

  • MistakeProving T(n) ≤ cn and concluding with "cn + n = O(n)".

    InsteadThe induction must reproduce the exact hypothesis T(n) ≤ cn; changing the constant breaks the proof.

  • MistakeComparing f(n) with a instead of with n^(log_b a).

    InsteadThe deciding quantity is the leaf count n^(log_b a); compare the exponent d with log_b a.

  • MistakeIgnoring the base case entirely.

    InsteadThe base case anchors the induction and fixes constants; check it explicitly in substitution proofs.

Summary and key terms

  • Loops turn into sums: arithmetic series are one degree higher than their terms, and geometric series are within a constant of their largest term.
  • A counter that doubles or halves reaches n in about log₂ n steps.
  • Recursive algorithms are described by recurrences that relate T(n) to the cost on smaller inputs.
  • A recursion tree shows where the work happens: equal levels give a log factor, a shrinking series is dominated by the root, a growing series by the leaves.
  • The substitution method guesses a bound and proves it by induction, reproducing the hypothesis exactly.
  • For T(n) = aT(n/b) + Θ(nᵈ), compare d with log_b a to get Θ(nᵈ), Θ(nᵈ log n) or Θ(n^(log_b a)).
  • Subtractive recurrences and uneven splits need unrolling or substitution instead of the master theorem.
Recurrence
An equation that defines a function in terms of its values on smaller inputs.
Arithmetic series
A sum whose terms increase by a constant; 1 + ... + n = n(n + 1)/2.
Geometric series
A sum whose terms grow by a constant ratio; it is within a constant of its largest term.
Harmonic number
Hₙ = 1 + 1/2 + ... + 1/n, which is Θ(log n).
Recursion tree
A diagram of all calls made by a recursive algorithm, labelled with their non-recursive costs.
Unrolling
Expanding a recurrence repeatedly to discover its pattern.
Substitution method
Guessing a bound and proving it by induction.
Master theorem
A formula solving T(n) = aT(n/b) + f(n) by comparing f(n) with n^(log_b a).
Divide and conquer
Solving a problem by splitting it into smaller subproblems, solving them recursively and combining the results.
Leaf count
The number of base-case calls, n^(log_b a) for the recurrence aT(n/b) + f(n).

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. How many times does work() run in: for i in range(n): for j in range(i): work()?

    Show answer

    n(n − 1)/2 = Θ(n²).

  2. Solve T(n) = 4T(n/2) + n with the master theorem.

    Show answer

    a = 4, b = 2, d = 1; log₂ 4 = 2 > 1, so Θ(n²).

  3. Solve T(n) = 4T(n/2) + n².

    Show answer

    log₂ 4 = 2 = d, so Θ(n² log n).

  4. Solve T(n) = 2T(n/4) + √n.

    Show answer

    log₄ 2 = 1/2 = d, so Θ(√n log n).

  5. Solve T(n) = T(n − 1) + 1 with T(1) = 1.

    Show answer

    T(n) = n, so Θ(n).

  6. Solve T(n) = 2T(n − 1) + 1 with T(1) = 1.

    Show answer

    T(n) = 2ⁿ − 1, so Θ(2ⁿ).

  7. How many levels does the recursion tree of T(n) = 3T(n/3) + n have for n = 81?

    Show answer

    log₃ 81 + 1 = 5 levels.

  8. What is wrong with proving T(n) = 2T(n/2) + n ∈ O(n) by "T(n) ≤ cn + n = O(n)"?

    Show answer

    The inductive hypothesis requires T(n) ≤ cn with the same c, and cn + n is larger than cn.

  9. A loop runs while i < n with i = i * 3 starting at 1. How many iterations?

    Show answer

    About log₃ n, specifically ⌈log₃ n⌉.

  10. Sum 1 + 1/2 + 1/4 + ... (infinitely many terms).

    Show answer

    2: a geometric series with ratio 1/2 sums to 1/(1 − 1/2).

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz