Amortised Analysis

Push onto a dynamic array a million times and almost every push is trivially cheap — write a value, bump a counter. But every so often the array fills up, and that one unlucky push must allocate a bigger buffer and copy everything across: a \Theta(n) jolt. A worst-case-per-operation analysis screams "O(n) per push!" and concludes a sequence of n pushes costs O(n^2). That bound is true and useless — the real total is \Theta(n), because the expensive pushes are rare enough to pay for.

Amortised analysis is the art of charging each operation its fair share of the total cost of a whole sequence, so that a rare expensive operation is subsidised by the many cheap ones around it. Crucially — and this trips everyone up once — it is not average-case analysis: there are no probabilities, no random inputs, no expectation. The amortised bound is a worst-case guarantee over any sequence of operations. We will meet the three classic techniques: the aggregate, accounting, and potential methods.

Method 1 — the aggregate method

The bluntest instrument, and often enough: bound the total cost T(n) of any sequence of n operations, then declare the amortised cost per operation to be T(n)/n. Every operation is assigned the same amortised cost, regardless of whether it was individually cheap or dear.

The geometric series is the whole secret: doubling makes resizes exponentially rarer as they grow more expensive, and a geometric sum is dominated by its last term. The chart plots the true cumulative cost of n pushes — a staircase with a jump at each resize — hugging the straight line 3n. It never escapes the line: that is the amortised bound, made visible.

Method 2 — the accounting (banker's) method

The aggregate method gives one flat amortised cost; the accounting method is subtler and more flexible. You invent an amortised charge for each operation — usually more than it really costs — and bank the surplus as credit stored on the data structure. Later, an expensive operation pays its true cost by spending the credit that earlier cheap operations left lying around. The only rule you must never break:

For the dynamic array, charge 3 per push: 1 pays to write the new element, and 2 is saved as credit on that element. When the array later doubles, every element in the old half carries exactly the credit needed to pay for copying itself (and it has been sitting there since the last resize). The bank never runs dry, so amortised O(1) per push — the same 3 the aggregate method found, but now with a story for where the money goes.

Two more classics: the multipop stack and the binary counter

Amortisation shines whenever cheap operations quietly build up a resource that a rare expensive operation then consumes all at once.

class MultiStack { private s: number[] = []; push(x: number): void { this.s.push(x); } // actual cost 1 multipop(k: number): void { // actual cost min(k, size) let n = Math.min(k, this.s.length); while (n-- > 0) this.s.pop(); } }

Notice the recurring shape: a geometric series (1+2+4+\dots or n+n/2+n/4+\dots) collapses to a constant times n, and the accounting story is always "prepay for the cleanup when you do the cheap thing."

Suppose that instead of doubling you grew the buffer by a fixed 100 slots each time it filled. Now a resize happens every 100 pushes and copies \Theta(n) elements, so the total copy work over n pushes is 100 + 200 + \dots + n \approx n^2/200 = \Theta(n^2) — amortised \Theta(n) per push, catastrophic. The magic of multiplicative growth is that it makes resizes geometrically rare relative to their cost; any growth factor >1 (1.5×, as many real implementations use, works fine) gives amortised O(1). Additive growth cannot, no matter how large the chunk. The factor is a space–time knob: a smaller factor wastes less memory but resizes more often.

The single most common error is to hear "amortised O(1)" as "usually O(1), on average". It is stronger and completely different. There is no randomness anywhere: the amortised bound holds for the worst possible sequence of operations an adversary can devise. It is an accounting identity, not an expectation. This also means a single operation can still be genuinely slow — an individual push really can cost \Theta(n) — which matters for real-time systems where a latency spike is unacceptable even if the throughput is fine. "Amortised O(1)" promises the total over any sequence is O(n); it promises nothing about the cost of the one operation you happen to be waiting on.