Amortized Analysis
Amortized analysis measures the average cost of an operation over a long sequence, revealing that some operations are cheap almost every time even though they're occasionally expensive.
Prerequisites: Big-O Complexity
Appending to a Python list is described as , but every so often — when the underlying array is full — it has to allocate a bigger array and copy every existing element into it, which is . So is the claim a lie? No: it is true on average, even though it is false for any single worst-case call. Amortized analysis is the tool that makes that average-case claim rigorous instead of hand-wavy.
The idea: spread the expensive operations over the cheap ones
The trick behind a dynamic array is doubling: when the array of capacity fills up, allocate a new array of capacity and copy everything over. That copy costs , but it only happens once every appends — right after the array has filled up again. Add up the total cost of appends: most are , plus one resize costing , giving a total of for operations, or per operation on average. This is the aggregate method: total cost over a sequence, divided by the number of operations.
A cleaner way to see why doubling (not adding a fixed amount) matters: if the array grew by a fixed 10 slots each time instead of doubling, a resize would still cost to copy, but now it happens every 10 appends regardless of how big the array already is — giving total resize cost across roughly resizes, each , which sums to overall. Doubling keeps resizes exponentially rare as the array grows, which is what keeps the amortized cost constant.
Worked example: appending 8 elements to a dynamic array starting at capacity 1
| append # | capacity before | action | cost |
|---|---|---|---|
| 1 | 1 | fits | 1 |
| 2 | 1 | resize to 2, copy 1, insert | 2 |
| 3 | 2 | resize to 4, copy 2, insert | 3 |
| 4 | 4 | fits | 1 |
| 5 | 4 | resize to 8, copy 4, insert | 5 |
| 6–8 | 8 | fits | 1 each |
Total cost across 8 appends: . Average cost per append: — a small constant, not the a single resize might suggest. As the sequence grows longer, that average converges toward a fixed constant near 2, regardless of how many appends you do.
Amortized means the total cost over a long sequence of operations divides out to a constant per operation — not that every individual call is cheap. Never assume amortized bounds hold for a single worst-case call in isolation; if a system reacts to per-call latency (a real-time trading loop, say), an occasional spike still matters even though the average is fine.
Where it shows up
Interviewers ask you to justify why list.append(), hash table insertion, and union-find's path compression are all amortized rather than worst-case — being able to explain why with the doubling argument (not just cite the result) is what separates a memorized answer from an understood one. In production, this distinction matters directly for latency-sensitive systems: a market-making loop that appends to a growing buffer can hit an occasional resize-induced latency spike at exactly the wrong moment, which is why low-latency code often pre-allocates capacity instead of relying on amortized growth — see C++ for Low-Latency Trading.
Practice in interviews
Further reading
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. 17)