Amortised analysis: what operations really cost

Best, average and worst case per operation, why a single push onto a dynamic array is sometimes O(n) but still O(1) on average, and why constant factors and cache locality decide real performance.

Three different questions

When someone says an operation is O(1), ask which case they mean. A lookup in a hash table is O(1) expected and O(n) worst case, and both answers are correct about the same code.

StructureOperationWorst caseAmortised / expected
Dynamic arrayappendO(n) when it must growO(1) amortised
Dynamic arrayindexO(1)O(1)
Dynamic arrayinsert at frontO(n)O(n) — no averaging helps
Hash tablegetO(n) with all keys collidingO(1) expected
Balanced treeinsertO(log n)O(log n) guaranteed
Binary heappushO(log n)O(log n)

Worst case is the guarantee you need for latency-sensitive paths. Amortised cost is the total divided by the number of operations, and it is the right measure for throughput-oriented work.

Why doubling makes append constant

# geometric growth: capacity doubles
# appends   1..8, cost of the copy
# push 1  -> allocate 1, copy 0
# push 2  -> allocate 2, copy 1
# push 3  -> allocate 4, copy 2
# push 5  -> allocate 8, copy 4
# push 9  -> allocate 16, copy 8

total copy cost after n pushes = n-1 (a geometric series)
amortised cost per push = O(1)

Doubling is what makes the series converge. Growing by a constant amount instead gives O(n) amortised append, because the number of copies is proportional to n for every n.

Growth policyCopies after n appendsAmortised appendMemory slack
+1 each timeabout n squared / 2O(n)None
+k constantabout n squared / (2k)O(n)Constant
x2about nO(1)Up to 2x
x1.5about 2nO(1)Up to 1.5x, reuses freed memory sooner

Big-O hides the constant, and the constant is often the answer

// Two ways to sum an array.
// A: sequential — one cache line per 16 ints
for (int i = 0; i < n; i++) sum += a[i];

// B: strided — a new cache line for almost every element
for (int i = 0; i < n; i += 16) sum += a[i];

// Same O(n). B can be several times slower in wall-clock time.
  • A pointer-chasing structure (linked list, tree of nodes) costs a memory access per step; contiguous data does not.
  • Branch mispredictions on unpredictable comparisons can cost more than the comparison itself.
  • A linked list of 10,000 small ints is usually slower to traverse than a contiguous array of 100,000 ints.
  • Allocation and garbage collection are part of the real cost of node-based structures.
💡
Big-O tells you how a structure scales, not how fast it is at your size. For n below a few thousand, a linear scan of a contiguous array frequently beats a balanced tree, and that is why standard libraries switch strategies at small sizes.

FAQ

Is amortised O(1) good enough for a latency SLA?
Not by itself. A resize can cause a single spike proportional to the current size. If you have a hard per-request limit, preallocate capacity or use a structure with a guaranteed bound.
Why do libraries use 1.5x rather than 2x growth?
A 1.5x policy can reuse previously freed blocks more often in a fragmenting allocator, and it wastes less slack. The asymptotic result is identical.

Heaps and priority queues Benchmarking and testing your data structure

Last refreshed 2026-09-18.