Why Amortized Analysis?
Worst-case analysis is pessimistic by design. It describes the single most expensive operation. But when that expensive operation happens rarely — and cheap operations happen frequently — worst-case can paint a wildly misleading picture of real performance.
Mental Model: Amortized Analysis
Problem it solves: Some data structure operations are occasionally expensive (O(n)) but almost always cheap (O(1)). Worst-case analysis says O(n); amortized analysis says O(1) — and the amortized view predicts real throughput far better.
When NOT to use it: When a single slow operation is unacceptable regardless of average (real-time systems, latency-sensitive code). In those cases, worst-case is the right metric.
Common misconception: "Amortized O(1) means each operation costs O(1)." No — it means the average cost per operation over a sequence of n operations is O(1). Individual operations can still be O(n).
Interview lens: Interviewers use "why is ArrayList.add() O(1) amortized?" as a test of depth. Knowing the three proof methods signals strong CS fundamentals.
Production lens: Understanding amortized cost helps you predict throughput at scale. A process that appends 10M elements to a list will take ~O(n) total regardless of individual resize events.
Dynamic Array Growth
A dynamic array (Python list, Java ArrayList, C++ std::vector) maintains a contiguous buffer. When the buffer fills up, it allocates a new buffer — typically 2× the current size — and copies all elements over.
timeline
title Resize Events for 16 Appends (doubling strategy)
Append 1 : capacity=1, size=1
Append 2 : RESIZE to 2, copy 1 element
Append 3 : RESIZE to 4, copy 2 elements
Append 4 : size=4 (no resize)
Append 5 : RESIZE to 8, copy 4 elements
Append 9 : RESIZE to 16, copy 8 elements
Append 17 : RESIZE to 32, copy 16 elements
Looking only at the resize operations, it seems expensive. But between each resize, there are many cheap O(1) appends. The key observation: resizes happen at sizes 1, 2, 4, 8, 16, ... so they grow geometrically.
Method 1: Aggregate Method
The aggregate method asks: what is the total cost of all n operations? Divide by n to get the amortized cost per operation.
Doubling Proof — Aggregate Method
For n appends to a dynamic array starting empty, count the total copy operations:
- Resize at capacity 1 → copies 1 element
- Resize at capacity 2 → copies 2 elements
- Resize at capacity 4 → copies 4 elements
- ...
- Resize at capacity n/2 → copies n/2 elements
Total copies = 1 + 2 + 4 + 8 + ... + n/2 = n − 1 (geometric series with ratio 2).
Plus n cheap appends (1 operation each) = n total cheap operations.
Total cost = n (appends) + (n−1) (copies) ≈ 2n = O(n).
Amortized cost per append = 2n / n = O(1).
Visualising the Proof
Cumulative Cost vs Number of Appends
Total operations grow linearly at slope ≈ 2 — confirming O(n) total cost and O(1) amortized cost per append.
Method 2: Accounting Method
The accounting method assigns each operation a amortized cost that may differ from its actual cost. Operations that are "cheap" pay in advance — depositing credits into a bank. Expensive operations draw from that bank.
Key Rule
The bank balance must never go negative. If every operation's amortized cost covers its actual cost plus enough credits for future expensive operations, the sum of amortized costs is an upper bound on the total actual cost.
Accounting Example: Dynamic Array
Assign amortized cost 3 to each append (even though most cost 1):
| When append is cheap (no resize) | Cost | Credits deposited |
|---|---|---|
| Pay actual cost | 1 | +2 credits (for future copy) |
| Cheap append total | 1 | Bank: +2 |
When a resize occurs at capacity k, we must copy k elements. Each of the k elements was inserted since the last resize, so each deposited 2 credits. Total credits available = 2k ≥ k (the cost of copying). The bank never goes negative. ✓
Since every append has amortized cost 3, total amortized cost for n appends = 3n = O(n). Amortized cost per append = 3/n × n / n = O(1).
Method 3: Potential Method
The potential method uses a potential function Φ that maps the data structure's state to a non-negative real number. The amortized cost of an operation is:
âi = ci + Φ(Di) − Φ(Di−1)
Where: ci = actual cost, Φ(Di) = potential after operation i, Φ(Di−1) = potential before.
Potential Proof for Dynamic Array
Define Φ = 2 × (current_size − current_capacity / 2).
This potential is 0 when the array is half-full (right after a resize) and equals the capacity when the array is completely full (just before the next resize).
Case 1: Cheap Append (no resize)
Actual cost ci = 1. Size increases by 1, capacity unchanged.
ΔΦ = 2 × 1 = 2.
Amortized cost = 1 + 2 = 3. ✓
Case 2: Resize (append causes doubling)
Let capacity before = k, so size = k. Actual cost = k+1 (copy k elements + 1 new insert). New capacity = 2k.
Φ before = 2(k − k/2) = k. Φ after = 2(k+1 − k) = 2.
ΔΦ = 2 − k.
Amortized cost = (k+1) + (2−k) = 3. ✓
Both cases give amortized cost 3. Total amortized cost = 3n = O(n). Per operation: O(1) amortized.
Growth Factor Tradeoffs: 2× vs 1.5× vs φ
The doubling factor is a design choice. Different languages make different tradeoffs:
| Language / Implementation | Growth Factor | Rationale |
|---|---|---|
CPython list | ~1.125 (incremental) | Minimises memory waste; uses a formula: new_size = old_size + old_size >> 3 + 6 |
Java ArrayList | 1.5× | Balance between memory overhead and copy frequency |
C++ std::vector | 2× (GCC), 1.5× (MSVC) | Standard doesn't specify; 2× is most common |
| Go slices | 2× (small), ~1.25× (large) | Reduces waste for large slices |
Why Not Factor 1.0 (add one slot at a time)?
If you grow by 1 on each resize, the total copy cost for n appends = 1+2+3+...+(n−1) = n(n−1)/2 = O(n²). Amortized cost per append = O(n). Python's formula avoids this. Always ensure at least geometric growth.
Real-World Growth Implementations
import sys
# Observe CPython list growth
lst = []
prev_size = sys.getsizeof(lst)
print(f"n=0 : {prev_size} bytes")
for i in range(1, 33):
lst.append(i)
new_size = sys.getsizeof(lst)
if new_size != prev_size:
print(f"n={i:<3}: {new_size} bytes (resize at n={i})")
prev_size = new_size
# Manual dynamic array with configurable growth factor
class DynamicArray:
def __init__(self, growth_factor=2.0):
self._data = [None]
self._size = 0
self._capacity = 1
self._growth = growth_factor
self._resize_count = 0
self._total_copies = 0
def append(self, val):
if self._size == self._capacity:
new_cap = max(int(self._capacity * self._growth), self._capacity + 1)
new_data = [None] * new_cap
for i in range(self._size):
new_data[i] = self._data[i]
self._data = new_data
self._total_copies += self._size
self._resize_count += 1
self._capacity = new_cap
self._data[self._size] = val
self._size += 1
def stats(self):
print(f"n={self._size}, capacity={self._capacity}")
print(f"Resizes: {self._resize_count}, Total copies: {self._total_copies}")
print(f"Copies per append: {self._total_copies / max(1, self._size):.2f}")
# Compare growth factors
for gf in [2.0, 1.5, 1.25]:
arr = DynamicArray(growth_factor=gf)
for i in range(1000):
arr.append(i)
print(f"\nGrowth factor {gf}:")
arr.stats()
Other Amortized Structures
Queue via Two Stacks — Amortized O(1) Dequeue
Maintain an inbox stack and an outbox stack. Enqueue to inbox (O(1)). Dequeue from outbox; if outbox is empty, move all inbox elements to outbox (O(n) one time, then O(1) until outbox empties again). Each element is moved at most once inbox→outbox. Amortized cost per operation: O(1).
Union-Find (Path Compression + Union by Rank) — Amortized O(α(n))
The inverse Ackermann function α(n) is practically constant (≤ 4 for any n you'll encounter in computing). Path compression makes future finds cheaper by flattening the tree. Union by rank keeps trees shallow. The amortized proof (by Tarjan) is one of the most elegant in algorithms.
Binary Counter — Amortized O(1) Increment
Incrementing a k-bit binary counter can flip up to k bits in the worst case. But the aggregate analysis shows: in n increments, bit 0 flips n times, bit 1 flips n/2, bit 2 flips n/4, ... Total flips = n(1 + 1/2 + 1/4 + ...) = 2n = O(n). Amortized cost per increment = O(1).
Interview & Practice
Quick Check
- Explain amortized O(1) append to someone who only knows worst-case analysis.
- If a dynamic array grows by factor 1.1 instead of 2, is append still O(1) amortized? Prove it.
- A stack has push(x) = O(1) and multi-pop(k) = O(k). Prove that a sequence of n push/multi-pop operations costs O(n) total using the potential method. Define Φ = number of elements in the stack.
LeetCode Connections
- Implement Queue using Stacks (LeetCode 232) — implement amortized O(1) dequeue
- Min Stack (LeetCode 155) — O(1) getMin with amortized thinking
- Design HashMap (LeetCode 706) — think about when to rehash