Structures

What amortised means

Appending to a dynamic array is O(1) amortised. It is also, on 512 appends, an operation that costs one unit 503 times and 257 units once. The amortised bound is a true statement about the sequence and a false one about any append in it, and the picture that shows why is a sawtooth nobody draws.

A dynamic array — a list, a vector, a slice, whatever a given language calls the thing that grows — supports appending in O(1)O(1) amortised time.

That statement is true, it is proved, and it is routinely repeated in a way that suggests appending costs about the same every time. It does not. Appending 512 elements to an array that doubles when full costs one unit for 503 of them and 257 units for one of them.

The word doing the work is amortised, and what it means is: averaged over the sequence. It is a claim about the total, divided by the number of operations. It says nothing whatsoever about any individual operation, and the individual operations are wildly different.

The cost of each of 512 appendsOne spike per append, on a logarithmic vertical axis. Almost every append costs one unit. 9 of them cost the entire current size, because the array had to be copied, and the largest cost 257 — more than half of all the appends put together would suggest. The mean over the whole sequence is 2.00, which is the amortised cost, and it is a true statement about the sequence and a false one about any append in it.110100257amortised 2.000256512append numbercost of that append (log scale)growth factor 2, cost = 1 write + a copy of the array when it resizes9 resizes in 512 appends
Fig. 1 The cost of each of 512 appends, on a logarithmic vertical axis. Almost every one costs a single unit. Nine of them cost the entire current size of the array plus the write, because the array had to be copied, and the largest costs 257. The green line is the mean over the whole sequence — 2.00 — which is the amortised cost, and no append anywhere in the picture costs it.

The cost model, stated

An amortised cost is only meaningful against a stated cost model, so here is this site’s.

Appending costs one unit for the write, plus, if the array was full, the current size for copying every existing element into the new allocation.

That model gives 2.00 units per append averaged over 512 appends, and 2.02 averaged over 64,000. Both are constant under any reasonable definition.

A different and equally defensible model also charges for the write of each element into the new array, giving a factor of two on the copy, and produces the figure of 3 that textbooks usually quote. Neither is more correct. They differ in whether a copy is one operation or two, which is a question about the machine and not about the algorithm.

The point of stating this is that an amortised cost quoted without its cost model is not checkable, and the difference between 2.02 and 3 is entirely a difference of model rather than of measurement. Anyone comparing this page’s number against a textbook’s should compare the models first.

Why doubling gives a constant

The argument is a geometric series and it is short.

Starting from capacity 1 and doubling, the copies happen at sizes 1, 2, 4, 8, …, up to the largest power of two below nn. The total copying is

1+2+4++2k<2k+12n1 + 2 + 4 + \cdots + 2^k < 2^{k+1} \le 2n

so the total work is at most nn writes plus 2n2n copies, which is 3n3n under one model and about 2n2n under this site’s. Divided by nn appends, that is a constant.

The essential feature is that the copies get exponentially rarer as they get more expensive, and the two effects exactly cancel. That cancellation is what makes the bound constant rather than merely small, and it is what fails if the array grows by a fixed amount instead — growing by a fixed step is quadratic, and the site’s gate asserts that the amortised-constant check refuses it.

The gate requires the spikes

There is a second assertion here that is easy to overlook and is the more interesting one.

The site checks that the amortised cost is constant. It also checks that the individual costs are not: the most expensive single append out of 8,192 must cost more than a quarter of nn. Measured, it costs 4,097 — half the array.

The reason for the second check is that if no individual append were expensive, the word amortised would be distinguishing nothing. A structure whose every operation costs 2 units is O(1)O(1) worst case, which is a strictly stronger property — the same relationship that a proved floor has to a measured average, where the stronger statement is the one that admits no exceptions, and calling it amortised would be underselling it. The word only earns its place when there is a gap between the average and the worst, and the gap here is a factor of 2,000.

That pair of assertions is the same shape as several others on this site: a tolerance is only defensible when bracketed by two measured things, and a distinction is only real when both sides of it have been checked.

Where the distinction bites

Amortised and worst case are the same for most purposes and violently different for a few, and knowing which situation applies is the practical content of this essay.

It does not matter when a million elements are being appended in a loop and the total is what counts. The amortised bound is exactly the right tool: the total is the quantity being measured, and it is linear.

It matters enormously in any system with a latency requirement. A real-time audio callback that must complete in 5 milliseconds cannot afford an append that copies a 100-megabyte array, however rare. The average is irrelevant; the maximum is the specification. The same applies to interactive frame budgets, to network servers with tail-latency targets, and to anything where a percentile rather than a mean is the metric.

For those situations the answer is not a dynamic array, any more than an average-case sorting bound is a latency budget. It is a structure with a genuine worst-case bound — a chunked list, a deque of fixed-size blocks, or an incrementally-resized array that copies a few elements on every operation rather than all of them at once. Each pays something in average throughput to remove the spike.

This is the same tension as an average concealing a tail, one level down, and the resolution is the same: the summary statistic should match the question being asked.

Choosing a growth factor is choosing where to loseEach point is a growth factor, appending 20,000 elements. Rightwards is more memory left unused when the sequence ends; upwards is more copying per append. Growing by ×1.125 wastes almost nothing and costs 9.89 units per append; growing by ×4 costs 2.09 and leaves 69% of the allocation empty. No point is below and left of every other, which is the definition of a trade-off and the reason real implementations disagree about this number.0%25%50%75%12345×1.125×1.25×1.5×2×3×4capacity left unused at the endamortised cost per append20,000 appends, cost = 1 write + a copy on resizeneither end wins
Fig. 2 The amortised cost is not one number either — it depends on the growth factor, and so does the memory wasted. Growing by ×1.125 costs 9.89 units per append and leaves 10% of the allocation unused; growing by ×4 costs 2.09 and leaves 69% empty. No point is best on both axes.

The potential-function proof, and what the picture adds

The standard proof is not the geometric series. It is a potential-function argument, and it is more powerful because it generalises.

Define the potential of the array as Φ=2sizecapacity\Phi = 2 \cdot \text{size} - \text{capacity}. An ordinary append costs 1 and raises the potential by 2, for an amortised cost of 3. An append that triggers a copy costs 1+size1 + \text{size}, and the potential drops from size\text{size} to 2size2size=02\cdot\text{size} - 2\cdot\text{size} = 0… and the arithmetic works out to a constant again.

The technique is genuinely elegant and it is how amortised bounds are proved for structures where no simple summation exists — splay trees, Fibonacci heaps, union-find with path compression.

What it does not do is show the sawtooth. The potential function’s whole purpose is to smooth the spikes away so the algebra is tractable, and having smoothed them it never mentions them again. Somebody who learns the bound from the proof learns a correct statement and does not learn that a single append can cost half the array.

That is the argument for drawing the picture: the proof and the picture say the same thing and leave different impressions, and the impression the proof leaves is the one that gets people into trouble in a latency-sensitive system.

The banker’s view

There is a third way to prove these bounds, and it is the one most people find easiest to hold.

Charge each append three units instead of one. One pays for the write. The other two go into a savings account attached to that element. When the array fills and has to be copied, every element that must be moved has two units saved — enough to pay for its own move and to leave the newly-doubled half of the array with credit for the next round.

The accounting works because between one resize and the next, exactly as many elements are appended as need to be copied at the next resize. The saved-up credit always covers the bill.

This is the accounting method, and it is equivalent to the potential function — the total credit outstanding is the potential — but it makes the mechanism visible in a way the algebra does not. It also makes the answer to “why three?” concrete: three is the charge that happens to balance the books under a cost model where a copy is one unit. Under this site’s model the measured figure is 2.02, and the difference is entirely in whether the copy’s write is charged separately.

Both methods share the property this essay is about: they establish a fact about the total and say nothing about any individual operation, because smoothing the individual operations away is the technique.

The cost of each of 1024 appendsOne spike per append, on a logarithmic vertical axis. Almost every append costs one unit. 10 of them cost the entire current size, because the array had to be copied, and the largest cost 513 — more than half of all the appends put together would suggest. The mean over the whole sequence is 2.00, which is the amortised cost, and it is a true statement about the sequence and a false one about any append in it.110100513amortised 2.0005121024append numbercost of that append (log scale)growth factor 2, cost = 1 write + a copy of the array when it resizes10 resizes in 1024 appends
Fig. 3 The same sawtooth at 1,024 appends. Ten resizes rather than nine, the largest costing 513, and the amortised mean essentially unmoved. The savings account is being emptied ten times, and it is full every time.

The measurement across sizes

An amortised bound claims the cost per operation does not drift as the structure grows, and that is checkable directly.

appends amortised cost resizes
1,000 2.023 10
4,000 2.024 12
16,000 2.024 14
64,000 2.024 16

Four significant figures of agreement across a factor of 64 in nn, and the resize count growing by exactly 2 each time nn quadruples — which is log2\log_2 behaving. The site’s gate requires the ratio between the largest and smallest of these to stay under 1.35, so a drift would fail rather than be quietly reported.

That check would catch a real bug. An implementation that grew by a fixed amount rather than a factor would still work, would still be O(1)O(1) per append for small nn, and would show its true quadratic nature only as nn got large — exactly the failure mode that a single-size measurement misses and a sweep catches.

The cost of each of 2048 appendsOne spike per append, on a logarithmic vertical axis. Almost every append costs one unit. 11 of them cost the entire current size, because the array had to be copied, and the largest cost 1025 — more than half of all the appends put together would suggest. The mean over the whole sequence is 2.00, which is the amortised cost, and it is a true statement about the sequence and a false one about any append in it.1101001,025amortised 2.00010242048append numbercost of that append (log scale)growth factor 2, cost = 1 write + a copy of the array when it resizes11 resizes in 2048 appends
Fig. 4 The same sawtooth at four times the size. The shape is identical, which is the visual form of the constancy in the table: the spikes get taller and rarer at exactly the rate that keeps the mean fixed. Eleven resizes for 2,048 appends, and the largest costs 1,025.

Where else the word appears

Dynamic arrays are the gentlest example. The technique earns its keep on structures where no simple summation would work, and it is worth naming a few so the pattern is recognisable.

Union-find with path compression. A sequence of mm operations on nn elements costs O(mα(n))O(m \cdot \alpha(n)), where α\alpha is the inverse Ackermann function and is at most 4 for any nn that fits in the universe. Individual operations can cost Θ(logn)\Theta(\log n); the amortised cost is effectively constant, and the proof is one of the more remarkable potential-function arguments in the subject.

Splay trees. Every operation is O(logn)O(\log n) amortised and individual operations can be Θ(n)\Theta(n). The structure has no balance invariant at all — it simply rotates the accessed node to the root — and the amortised bound is the only thing keeping it honest.

Fibonacci heaps. Decrease-key is O(1)O(1) amortised, which is what makes Dijkstra’s algorithm O(E+VlogV)O(E + V\log V). Individual decrease-key operations can trigger a cascade of cuts costing Θ(logn)\Theta(\log n).

In every case the shape is the same: cheap operations bank credit, expensive ones spend it, and the accounting works out. And in every case the individual worst case is much worse than the amortised cost, so the latency objection applies to all of them.

That is why Fibonacci heaps are famous and rarely used. The amortised bound is real, the constants are large, and the spikes are genuine — for most workloads a simpler heap with worse asymptotics wins on both counts.

Choosing a growth factor is choosing where to loseEach point is a growth factor, appending 50,000 elements. Rightwards is more memory left unused when the sequence ends; upwards is more copying per append. Growing by ×1.125 wastes almost nothing and costs 9.12 units per append; growing by ×4 costs 1.44 and leaves 24% of the allocation empty. No point is below and left of every other, which is the definition of a trade-off and the reason real implementations disagree about this number.0%25%50%75%12345×1.125×1.25×1.5×2×3×4capacity left unused at the endamortised cost per append50,000 appends, cost = 1 write + a copy on resizeneither end wins
Fig. 5 The trade at two and a half times the default number of appends. The curve is unchanged, which is the constancy the amortised bound claims — but the absolute size of the largest spike has grown with n, and it is the spike rather than the mean that decides whether a structure is usable in a latency budget.

Three kinds of average

It is worth separating three words that all mean “not the worst case” and mean quite different things.

Amortised. Averaged over a sequence of operations, with no randomness anywhere. Which is also why the growth factor is a pure constant-factor question — every factor above 1 gives the same class, so the choice lives entirely in the number this essay measures. A worst-case guarantee about the total. Deterministic: the same sequence always costs the same, and there is no input an adversary can choose to break it.

Expected. Averaged over randomness, either in the input distribution or in the algorithm’s own coin flips. Quicksort with a random pivot is expected Θ(nlogn)\Theta(n \log n), meaning it is that on average over the coins, and a very unlucky run is worse.

Average case. Averaged over an assumed input distribution. Weaker than both, because it depends on an assumption about the data that the user of the algorithm may not satisfy.

A dynamic array’s append is amortised, which is the strongest of the three: no distribution is assumed and no randomness is involved. An adversary who chooses the entire sequence of operations still cannot make the total worse than linear. That is a much better guarantee than “expected”, and the shared word “average” hides the difference.

Probes per insertion against load factor, table of 8,192The line is Knuth's closed form for linear probing, integrated over the fill; the points are a table actually filled and counted. They agree to within 2.3% everywhere. At a load factor of 0.5 an insertion averages 1.49 probes; at 0.9 it averages 5.39. The formula that matters for insertion is the unsuccessful-search one, ½(1 + 1/(1−α)²), and using the successful-search form instead is an error that hides at low load and is glaring at high load.1234560.000.250.500.75formulameasuredload factor αprobes per insertion8,192 slots, mean of 8 fills, seeds stated in lib/structures.jstwo routes agree to 2.3%
Fig. 6 An expected bound rather than an amortised one, for contrast. The probes per insertion into a hash table depend on the load factor and on the hash’s behaviour on the keys — there is randomness in the model, and a table filled with adversarially chosen keys would not match this curve at all. A dynamic array’s amortised bound has no equivalent weakness.