Amortized Analysis
18 min
Python's list.append() is described as O(1) — but that can't be
literally true for EVERY call, since occasionally the underlying array
runs out of room and has to be resized, which means copying every
existing element into a bigger array (an O(n) operation). Amortized
analysis resolves this apparent contradiction: individual operations
can occasionally be expensive, but the AVERAGE cost per operation, over
a long sequence, is still O(1).
def total_resizes(n):
capacity = 1
size = 0
resizes = 0
for _ in range(n):
if size == capacity:
capacity *= 2
resizes += 1
size += 1
return resizes
print(total_resizes(1000)) # far fewer than 1000 -- doubling grows capacity exponentially
Resizes grow logarithmically (roughly doubling n only adds ONE more resize) -- 1,000,000 appends trigger only about 20 resizes total, not 1,000,000.
Here's the key insight: because capacity DOUBLES each time (not just
increases by a fixed amount), each resize copies roughly as many
elements as ALL the previous resizes combined copied in total. Summing
1 + 2 + 4 + 8 + ... + n/2 (the sizes at each resize) totals just under
2n — proportional to n, not to the NUMBER of resizes.
def total_copy_operations(n):
capacity = 1
size = 0
total_copies = 0
for _ in range(n):
if size == capacity:
total_copies += capacity # this resize's copying cost
capacity *= 2
size += 1
return total_copies
Since the total copying work across n appends is O(n), the AVERAGE
(amortized) cost per individual append is O(n)/n = O(1) — even though
any single append might trigger an O(n) resize. This is why doubling
(not adding a fixed amount) is the standard growth strategy for dynamic
arrays — growing by a fixed amount instead would make resizes happen
proportionally to n, making the amortized cost O(n), not O(1).
Write `total_resizes(n)`: simulate a dynamic array starting at capacity 1 that DOUBLES its capacity whenever a new element wouldn't fit, as you append `n` elements one at a time. Return the total number of RESIZE operations (not appends).
Write `total_copy_operations(n)`: same simulation, but return the TOTAL number of element copies performed across every resize (each resize copies every existing element — `capacity` of them — into the new array).
Why is the amortized cost of appending to a dynamic array O(1), even though an individual resize is O(n)?
You understand why doubling a dynamic array's capacity keeps appends amortized O(1), even though individual resizes are O(n).