Amortized methods
Amortised analysis proves that an occasional expensive operation is paid for by many cheap ones. This lesson applies the aggregate, accounting and potential methods to dynamic arrays and a multipop stack, and previews union-find.
Learning goals
- Prove amortised O(1) for array doubling
- Choose between aggregate, accounting and potential methods
- Explain why multipop is still O(1) amortised
Prerequisites
Aggregate analysis
Aggregate analysis bounds the total cost T(n) of any sequence of n operations and divides by n. For a dynamic array that doubles when full, the copies cost 1 + 2 + 4 + … < 2n over n appends, plus n writes, so T(n) < 3n and each append is amortised O(1) (at most 3).
It treats all operations alike, so it is the simplest method.
Accounting method
The accounting method charges each operation an amortised price that may exceed its real cost; the surplus is stored as credits on specific elements and pays for later expensive steps. For a doubling array, charge 3 per append: 1 pays for the write, 2 are saved on the new element. When the array doubles, the elements added since the last doubling carry enough credits to pay for copying all of them and half of the old ones.
The balance of credits must never go negative.
Potential method
The potential method defines a function Φ of the data structure's state, and amortised cost = actual cost + Φ(after) - Φ(before). With Φ ≥ 0 and Φ(start) = 0, the total amortised cost is an upper bound on the total real cost. For a doubling array use Φ = 2 · size - capacity.
The same potential is used in the animation of the multipop stack, with Φ = number of items on the stack.
Stack with multipop: the potential method
Interactive · edit the inputstack (bottom → top)
Each push costs 1 and each pop costs 1, so a single multipop can cost a lot, yet the total over any sequence of operations is small. The potential Φ = stack size measures stored-up work.
1void push(int x) { stack.push(x); } // actual cost 12void multipop(int k) {3while (k-- > 0 && !stack.isEmpty()) stack.pop(); // actual cost = number popped4}5// potential Φ = number of items on the stack (always >= 0)6// amortised cost = actual cost + ΔΦ: push: 1 + 1 = 2; multipop: m + (-m) = 0
Live state
potential
0
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | potential=0 | Each push costs 1 and each pop costs 1, so a single multipop can cost a lot, yet the total over any sequence of operations is small. The potential Φ = stack size measures stored-up work. |
| 2 | L1 | op=push(5) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=1 | push(5): actual cost 1, potential +1, amortised cost 2. |
| 3 | L1 | op=push(3) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=2 | push(3): actual cost 1, potential +1, amortised cost 2. |
| 4 | L1 | op=push(8) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=3 | push(8): actual cost 1, potential +1, amortised cost 2. |
| 5 | L1 | op=push(1) actual=1 ΔΦ=1 amortised=2 potential=4 total actual=4 | push(1): actual cost 1, potential +1, amortised cost 2. |
| 6 | L3 | op=multipop(3) actual=3 ΔΦ=-3 amortised=0 potential=1 total actual=7 | multipop(3) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops. |
| 7 | L1 | op=push(4) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=8 | push(4): actual cost 1, potential +1, amortised cost 2. |
| 8 | L1 | op=push(7) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=9 | push(7): actual cost 1, potential +1, amortised cost 2. |
| 9 | L3 | op=multipop(4) actual=3 ΔΦ=-3 amortised=0 potential=0 total actual=12 | multipop(4) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops. |
| 10 | L1 | op=push(2) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=13 | push(2): actual cost 1, potential +1, amortised cost 2. |
| 11 | L3 | op=multipop(2) actual=1 ΔΦ=-1 amortised=0 potential=0 total actual=14 | multipop(2) removed 1 item: actual cost 1, potential -1, amortised cost 0. The potential drawn down pays for the pops. |
| 12 | L6 | operations=10 total actual cost=14 total amortised=14 return=14 | Total actual cost 14 ≤ total amortised 14 ≤ 2 × 10 = 20. Every operation is O(1) amortised even though one multipop can cost O(n): you cannot pop more than you pushed. |
Dynamic array doubling
A dynamic array doubles its capacity when full: allocate 2× the space and copy every element. A single append can cost O(n), but the next n appends cost O(1), so the average is constant. Growing by a fixed amount (for example +10) instead gives O(n) per append on average, O(n²) in total.
Compare the growth policies and watch the total copy cost in the animation.
1int[] data = new int[1]; int size = 0; long copies = 0;2for (int i = 0; i < 1000; i++) {3if (size == data.length) { data = Arrays.copyOf(data, size * 2); copies += size; }4data[size++] = i;5}6// copies < 2 * 1000: total work < 3n
Dynamic array: amortised cost of append
Interactive · edit the inputbacking array (capacity 1)
cost of each append
Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2).
1void add(int x) {2if (size == data.length) { // full: allocate a bigger array3data = Arrays.copyOf(data, newCapacity(data.length)); // copies `size` elements4}5data[size++] = x; // the cheap part: 1 write6}7// doubling: newCapacity = 2 * capacity; grow-by-1: capacity + 1; Java ArrayList: capacity + capacity / 2
Live state
capacity
1
size
0
Execution trace (dry run table)Show 24 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | capacity=1 size=0 | Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2). |
| 2 | L5 | size=1 capacity=1 cost of this append=1 total cost=1 amortised=1 | Append #1 costs 1. Total so far 1, so the average is 1.00 per append. |
| 3 | L3 | capacity=1 new capacity=2 copies=1 | Full: allocate 2 slots and copy 1 element. This single append costs 2. |
| 4 | L5 | size=2 capacity=2 cost of this append=2 total cost=3 amortised=1.5 | Append #2 costs 2. Total so far 3, so the average is 1.50 per append. |
| 5 | L3 | capacity=2 new capacity=4 copies=2 | Full: allocate 4 slots and copy 2 elements. This single append costs 3. |
| 6 | L5 | size=3 capacity=4 cost of this append=3 total cost=6 amortised=2 | Append #3 costs 3. Total so far 6, so the average is 2.00 per append. |
| 7 | L5 | size=4 capacity=4 cost of this append=1 total cost=7 amortised=1.75 | Append #4 costs 1. Total so far 7, so the average is 1.75 per append. |
| 8 | L3 | capacity=4 new capacity=8 copies=4 | Full: allocate 8 slots and copy 4 elements. This single append costs 5. |
| 9 | L5 | size=5 capacity=8 cost of this append=5 total cost=12 amortised=2.4 | Append #5 costs 5. Total so far 12, so the average is 2.40 per append. |
| 10 | L5 | size=6 capacity=8 cost of this append=1 total cost=13 amortised=2.17 | Append #6 costs 1. Total so far 13, so the average is 2.17 per append. |
| 11 | L5 | size=7 capacity=8 cost of this append=1 total cost=14 amortised=2 | Append #7 costs 1. Total so far 14, so the average is 2.00 per append. |
| 12 | L5 | size=8 capacity=8 cost of this append=1 total cost=15 amortised=1.88 | Append #8 costs 1. Total so far 15, so the average is 1.88 per append. |
| 13 | L3 | capacity=8 new capacity=16 copies=8 | Full: allocate 16 slots and copy 8 elements. This single append costs 9. |
| 14 | L5 | size=9 capacity=16 cost of this append=9 total cost=24 amortised=2.67 | Append #9 costs 9. Total so far 24, so the average is 2.67 per append. |
| 15 | L5 | size=10 capacity=16 cost of this append=1 total cost=25 amortised=2.5 | Append #10 costs 1. Total so far 25, so the average is 2.50 per append. |
| 16 | L5 | size=11 capacity=16 cost of this append=1 total cost=26 amortised=2.36 | Append #11 costs 1. Total so far 26, so the average is 2.36 per append. |
| 17 | L5 | size=12 capacity=16 cost of this append=1 total cost=27 amortised=2.25 | Append #12 costs 1. Total so far 27, so the average is 2.25 per append. |
| 18 | L5 | size=13 capacity=16 cost of this append=1 total cost=28 amortised=2.15 | Append #13 costs 1. Total so far 28, so the average is 2.15 per append. |
| 19 | L5 | size=14 capacity=16 cost of this append=1 total cost=29 amortised=2.07 | Append #14 costs 1. Total so far 29, so the average is 2.07 per append. |
| 20 | L5 | size=15 capacity=16 cost of this append=1 total cost=30 amortised=2 | Append #15 costs 1. Total so far 30, so the average is 2.00 per append. |
| 21 | L5 | size=16 capacity=16 cost of this append=1 total cost=31 amortised=1.94 | Append #16 costs 1. Total so far 31, so the average is 1.94 per append. |
| 22 | L3 | capacity=16 new capacity=32 copies=16 | Full: allocate 32 slots and copy 16 elements. This single append costs 17. |
| 23 | L5 | size=17 capacity=32 cost of this append=17 total cost=48 amortised=2.82 | Append #17 costs 17. Total so far 48, so the average is 2.82 per append. |
| 24 | L7 | appends=17 total cost=48 return=2.824 | Doubling: total cost 48 < 3n = 51, so the amortised cost per append is O(1) (here 2.82). The expensive copies are rare and pay for themselves: aggregate analysis. Accounting method: charge 3 coins per append: 1 for the write, 1 to copy itself later, 1 to copy an older element. |
Stack multipop
A stack with push, pop and multipop(k) (pop up to k items) looks like it could cost O(n) per multipop, so n operations would cost O(n²). But each item can be popped at most once after it was pushed, so the total pops over n operations are at most n. Hence every operation is amortised O(1).
Run the sequence and watch the potential (the stack size) pay for each large multipop.
1static int multipop(Deque<Integer> stack, int k) {2int popped = 0;3while (!stack.isEmpty() && popped < k) { stack.pop(); popped++; }4return popped; // cost = popped; total over all calls <= total pushes5}
Stack with multipop: the potential method
Interactive · edit the inputstack (bottom → top)
Each push costs 1 and each pop costs 1, so a single multipop can cost a lot, yet the total over any sequence of operations is small. The potential Φ = stack size measures stored-up work.
1void push(int x) { stack.push(x); } // actual cost 12void multipop(int k) {3while (k-- > 0 && !stack.isEmpty()) stack.pop(); // actual cost = number popped4}5// potential Φ = number of items on the stack (always >= 0)6// amortised cost = actual cost + ΔΦ: push: 1 + 1 = 2; multipop: m + (-m) = 0
Live state
potential
0
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | potential=0 | Each push costs 1 and each pop costs 1, so a single multipop can cost a lot, yet the total over any sequence of operations is small. The potential Φ = stack size measures stored-up work. |
| 2 | L1 | op=push(5) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=1 | push(5): actual cost 1, potential +1, amortised cost 2. |
| 3 | L1 | op=push(3) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=2 | push(3): actual cost 1, potential +1, amortised cost 2. |
| 4 | L1 | op=push(8) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=3 | push(8): actual cost 1, potential +1, amortised cost 2. |
| 5 | L1 | op=push(1) actual=1 ΔΦ=1 amortised=2 potential=4 total actual=4 | push(1): actual cost 1, potential +1, amortised cost 2. |
| 6 | L3 | op=multipop(3) actual=3 ΔΦ=-3 amortised=0 potential=1 total actual=7 | multipop(3) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops. |
| 7 | L1 | op=push(4) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=8 | push(4): actual cost 1, potential +1, amortised cost 2. |
| 8 | L1 | op=push(7) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=9 | push(7): actual cost 1, potential +1, amortised cost 2. |
| 9 | L3 | op=multipop(4) actual=3 ΔΦ=-3 amortised=0 potential=0 total actual=12 | multipop(4) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops. |
| 10 | L1 | op=push(2) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=13 | push(2): actual cost 1, potential +1, amortised cost 2. |
| 11 | L3 | op=multipop(2) actual=1 ΔΦ=-1 amortised=0 potential=0 total actual=14 | multipop(2) removed 1 item: actual cost 1, potential -1, amortised cost 0. The potential drawn down pays for the pops. |
| 12 | L6 | operations=10 total actual cost=14 total amortised=14 return=14 | Total actual cost 14 ≤ total amortised 14 ≤ 2 × 10 = 20. Every operation is O(1) amortised even though one multipop can cost O(n): you cannot pop more than you pushed. |
Union-find intuition
Union-find keeps disjoint sets as trees. With union by rank and path compression, any sequence of m operations on n elements costs O(m · α(n)), where α is the inverse Ackermann function and is at most 4 for any input that fits in the universe. That is amortised, nearly constant time per operation.
Individual find calls can still be slow; the guarantee covers the whole sequence. The full structure is covered in the graph chapters.
1static int find(int[] parent, int x) {2while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } // path halving3return x;4}
Practice ladder
Now try it yourself
Easy
- Compute the total cost of 16 appends into a doubling array starting at capacity 1
- How much do you charge per append in the accounting method?
Medium
- Show that growing the array by a constant 10 each time makes n appends cost Θ(n²)
- Define a potential for a binary counter increment
Advanced
- Prove that n increments of a k-bit binary counter cost O(n) in total
Key takeaways
- Amortised cost is guaranteed per operation over any sequence, not on average input.
- Doubling gives amortised O(1) appends; fixed-step growth does not.
- Credits and potentials are two bookkeeping views of the same argument.