Lesson 05 / 25
The Method for Computing Complexity
Counting cost with loop and conditional rules, recurrence relations, the master theorem, and amortized analysis.
Contents
The classes have been recognized; next comes determining which class a piece of code belongs to by looking at it. This lesson gives the rules for the calculation. The rules are few and are applied in combination.
Basic Rules
Sequential parts are added. If two parts run one after the other, their costs are added, and the larger one dominates: .
A loop is body cost times iteration count. A loop with a constant body that runs times is .
In nested loops, costs multiply. Two loops, each running times, are .
In conditional structures, the worst branch is taken. The guarantee is given over the worst case.
A constant number of repetitions is constant. A loop whose iteration count does not depend on is .
def counting_examples(n: int) -> dict[str, int]: counters = {"single": 0, "nested": 0, "triangular": 0, "halving": 0} for i in range(n): # O(n) counters["single"] += 1 for i in range(n): # O(n²) for j in range(n): counters["nested"] += 1 for i in range(n): # O(n²) — triangular, but still quadratic for j in range(i): counters["triangular"] += 1 i = 1 while i < n: # O(log n) counters["halving"] += 1 i *= 2 return counters print(counting_examples(8)) # {'single': 8, 'nested': 64, 'triangular': 28, 'halving': 3} print(counting_examples(16)) # {'single': 16, 'nested': 256, 'triangular': 120, 'halving': 4}
When the input doubles, the counts change as the classes predict: single doubles, nested quadruples; the halving loop only increases by one.
The triangular loop needs attention. The total iteration count is ; once the constant factor is eliminated, remains. The observation “it runs about half as many times” does not change the class — it only changes the constant.
How the Loop Bound Changes
The iteration count depends on how the loop variable advances:
| Advancement | Iteration count | Class |
|---|---|---|
i += 1 |
||
i += k |
||
i *= 2 |
||
i = i * i |
||
i -= 1 (from n) |
The second row matters: advancing by a constant step does not change the class. Multiplicative advancement does; this is the source of binary search’s logarithmic behavior.
Recurrence Relations
A recursive algorithm’s cost is written in terms of itself. This expression is called a recurrence relation.
Three common patterns:
At every step the problem shrinks by one element and constant work is done; a total of steps. Factorial computation is of this pattern.
At every step the problem is halved and constant work is done; the number of halvings is . Binary search is of this pattern.
The problem is split into two halves, and all elements are processed at every level. Merge sort is of this pattern, and it is the main example of the next topic.
Recursion Tree
The intuitive way to solve a relation is to draw the calls as a tree and sum the cost level by level.
For the relation :
| Level | Call count | Work per call | Level total |
|---|---|---|---|
| 0 | 1 | ||
| 1 | 2 | ||
| 2 | 4 | ||
Every level’s total is , and the number of levels is ; the total comes out to .
The same method gives a different result for : the level totals grow as , and the total comes out to . The leaf count becomes dominant.
The Master Theorem
Most divide-and-conquer relations fit a single pattern:
Here is the number of subproblems, is the shrink factor, and is the cost of splitting and combining. The result is determined by comparing with :
- If is smaller: — the leaves dominate.
- If the two are of the same order: — all levels are equal.
- If is larger: — the root dominates.
In merge sort, , , ; since , the second case applies, and the result comes out to .
The theorem’s full statement contains additional conditions, and not every relation fits this pattern; for those that do not, the tree method or a direct solution is used.
Amortized Analysis
The cost of some operations is misleading when looked at individually. The dynamic array from the Data Structures course was an example of this: appending is mostly constant, occasionally linear.
The aggregate method computes the total cost of operations and divides by . In a dynamic array, the total copying for appends was less than ; the amortized cost per append is constant.
def total_copies(n: int) -> int: """The total number of copies made over n appends (capacity doubling each time).""" capacity, length, copies = 1, 0, 0 for _ in range(n): if length == capacity: copies += length # every element is moved to the new block capacity *= 2 length += 1 return copies for n in (16, 1000, 100_000): print(n, total_copies(n), f"{total_copies(n)/n:.2f}") # 16 15 0.94 # 1000 1023 1.02 # 100000 131071 1.31
The average number of copies per append stays around a small constant as the input grows; it does not grow linearly. The statement “amortized constant cost” says exactly this.
There are two more methods. The accounting method charges every cheap operation a share for expensive work to come; the potential method expresses the structure’s “stored energy” with a function. All three give the same result; the choice depends on which one makes the proof shorter.
Order of Application
The sequence followed to find the cost of a piece of code:
- Define the input size. What does count?
- Find the innermost operation. Which operation repeats most often?
- Multiply the iteration counts. From outside to inside in nested structures.
- Add the sequential parts, take the largest term.
- If there is recursion, write the relation and solve it.
- If there is amortized behavior, divide total cost by operation count.
When the steps are applied in order, the result is obtained without resorting to intuition.
Summary
- Sequential parts are added, costs multiply in nested loops, and the worst branch is taken in conditionals.
- Multiplicative advancement of the loop variable makes the iteration count logarithmic; advancing by a constant step does not change the class.
- Recursive cost is written as a relation; three common patterns give linear, logarithmic, and linearithmic results.
- The recursion tree solves the relation by summing cost level by level.
- The master theorem classifies divide-and-conquer relations into three cases.
- Amortized analysis divides the total cost of a sequence of operations by the operation count.
Next Step
So far, only time has been considered. Yet algorithms also use memory, and the two frequently substitute for one another: it is possible to run faster by using more memory, or slower by using less. The next lesson takes up this trade-off and its typical patterns.
To keep your progress and take notes, Log in
My notes
Log in to take notes.