Chapter 4: Complexity, Memory and Performance Analysis
4.06Beginner6 topics16 min3 interactive animations

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

Complexity scenariosSummations and sequences
4.06.1

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.

4.06.2

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.

4.06.3

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 input

Your input

Try

stack (bottom → top)

empty
Setup

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.

Step 1 / 12
Java
1void push(int x) { stack.push(x); } // actual cost 1
2void multipop(int k) {
3 while (k-- > 0 && !stack.isEmpty()) stack.pop(); // actual cost = number popped
4}
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

0 actual cost

potential

0

No condition evaluated on this step
Execution trace (dry run table)Show 12 steps
StepLineStateWhat happens
1L5potential=0Each 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.
2L1op=push(5) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=1push(5): actual cost 1, potential +1, amortised cost 2.
3L1op=push(3) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=2push(3): actual cost 1, potential +1, amortised cost 2.
4L1op=push(8) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=3push(8): actual cost 1, potential +1, amortised cost 2.
5L1op=push(1) actual=1 ΔΦ=1 amortised=2 potential=4 total actual=4push(1): actual cost 1, potential +1, amortised cost 2.
6L3op=multipop(3) actual=3 ΔΦ=-3 amortised=0 potential=1 total actual=7multipop(3) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops.
7L1op=push(4) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=8push(4): actual cost 1, potential +1, amortised cost 2.
8L1op=push(7) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=9push(7): actual cost 1, potential +1, amortised cost 2.
9L3op=multipop(4) actual=3 ΔΦ=-3 amortised=0 potential=0 total actual=12multipop(4) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops.
10L1op=push(2) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=13push(2): actual cost 1, potential +1, amortised cost 2.
11L3op=multipop(2) actual=1 ΔΦ=-1 amortised=0 potential=0 total actual=14multipop(2) removed 1 item: actual cost 1, potential -1, amortised cost 0. The potential drawn down pays for the pops.
12L6operations=10 total actual cost=14 total amortised=14 return=14Total 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.
4.06.4

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.

Java
1int[] data = new int[1]; int size = 0; long copies = 0;
2for (int i = 0; i < 1000; i++) {
3 if (size == data.length) { data = Arrays.copyOf(data, size * 2); copies += size; }
4 data[size++] = i;
5}
6// copies < 2 * 1000: total work < 3n

Dynamic array: amortised cost of append

Interactive · edit the input

Your input

Try

backing array (capacity 1)

·

cost of each append

empty
Setup

Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2).

Step 1 / 24
Java
1void add(int x) {
2 if (size == data.length) { // full: allocate a bigger array
3 data = Arrays.copyOf(data, newCapacity(data.length)); // copies `size` elements
4 }
5 data[size++] = x; // the cheap part: 1 write
6}
7// doubling: newCapacity = 2 * capacity; grow-by-1: capacity + 1; Java ArrayList: capacity + capacity / 2

Live state

0 copies0 writes

capacity

1

size

0

No condition evaluated on this step
Execution trace (dry run table)Show 24 steps
StepLineStateWhat happens
1L1capacity=1 size=0Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2).
2L5size=1 capacity=1 cost of this append=1 total cost=1 amortised=1Append #1 costs 1. Total so far 1, so the average is 1.00 per append.
3L3capacity=1 new capacity=2 copies=1Full: allocate 2 slots and copy 1 element. This single append costs 2.
4L5size=2 capacity=2 cost of this append=2 total cost=3 amortised=1.5Append #2 costs 2. Total so far 3, so the average is 1.50 per append.
5L3capacity=2 new capacity=4 copies=2Full: allocate 4 slots and copy 2 elements. This single append costs 3.
6L5size=3 capacity=4 cost of this append=3 total cost=6 amortised=2Append #3 costs 3. Total so far 6, so the average is 2.00 per append.
7L5size=4 capacity=4 cost of this append=1 total cost=7 amortised=1.75Append #4 costs 1. Total so far 7, so the average is 1.75 per append.
8L3capacity=4 new capacity=8 copies=4Full: allocate 8 slots and copy 4 elements. This single append costs 5.
9L5size=5 capacity=8 cost of this append=5 total cost=12 amortised=2.4Append #5 costs 5. Total so far 12, so the average is 2.40 per append.
10L5size=6 capacity=8 cost of this append=1 total cost=13 amortised=2.17Append #6 costs 1. Total so far 13, so the average is 2.17 per append.
11L5size=7 capacity=8 cost of this append=1 total cost=14 amortised=2Append #7 costs 1. Total so far 14, so the average is 2.00 per append.
12L5size=8 capacity=8 cost of this append=1 total cost=15 amortised=1.88Append #8 costs 1. Total so far 15, so the average is 1.88 per append.
13L3capacity=8 new capacity=16 copies=8Full: allocate 16 slots and copy 8 elements. This single append costs 9.
14L5size=9 capacity=16 cost of this append=9 total cost=24 amortised=2.67Append #9 costs 9. Total so far 24, so the average is 2.67 per append.
15L5size=10 capacity=16 cost of this append=1 total cost=25 amortised=2.5Append #10 costs 1. Total so far 25, so the average is 2.50 per append.
16L5size=11 capacity=16 cost of this append=1 total cost=26 amortised=2.36Append #11 costs 1. Total so far 26, so the average is 2.36 per append.
17L5size=12 capacity=16 cost of this append=1 total cost=27 amortised=2.25Append #12 costs 1. Total so far 27, so the average is 2.25 per append.
18L5size=13 capacity=16 cost of this append=1 total cost=28 amortised=2.15Append #13 costs 1. Total so far 28, so the average is 2.15 per append.
19L5size=14 capacity=16 cost of this append=1 total cost=29 amortised=2.07Append #14 costs 1. Total so far 29, so the average is 2.07 per append.
20L5size=15 capacity=16 cost of this append=1 total cost=30 amortised=2Append #15 costs 1. Total so far 30, so the average is 2.00 per append.
21L5size=16 capacity=16 cost of this append=1 total cost=31 amortised=1.94Append #16 costs 1. Total so far 31, so the average is 1.94 per append.
22L3capacity=16 new capacity=32 copies=16Full: allocate 32 slots and copy 16 elements. This single append costs 17.
23L5size=17 capacity=32 cost of this append=17 total cost=48 amortised=2.82Append #17 costs 17. Total so far 48, so the average is 2.82 per append.
24L7appends=17 total cost=48 return=2.824Doubling: 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.
4.06.5

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.

Java
1static int multipop(Deque<Integer> stack, int k) {
2 int popped = 0;
3 while (!stack.isEmpty() && popped < k) { stack.pop(); popped++; }
4 return popped; // cost = popped; total over all calls <= total pushes
5}

Stack with multipop: the potential method

Interactive · edit the input

Your input

Try

stack (bottom → top)

empty
Setup

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.

Step 1 / 12
Java
1void push(int x) { stack.push(x); } // actual cost 1
2void multipop(int k) {
3 while (k-- > 0 && !stack.isEmpty()) stack.pop(); // actual cost = number popped
4}
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

0 actual cost

potential

0

No condition evaluated on this step
Execution trace (dry run table)Show 12 steps
StepLineStateWhat happens
1L5potential=0Each 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.
2L1op=push(5) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=1push(5): actual cost 1, potential +1, amortised cost 2.
3L1op=push(3) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=2push(3): actual cost 1, potential +1, amortised cost 2.
4L1op=push(8) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=3push(8): actual cost 1, potential +1, amortised cost 2.
5L1op=push(1) actual=1 ΔΦ=1 amortised=2 potential=4 total actual=4push(1): actual cost 1, potential +1, amortised cost 2.
6L3op=multipop(3) actual=3 ΔΦ=-3 amortised=0 potential=1 total actual=7multipop(3) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops.
7L1op=push(4) actual=1 ΔΦ=1 amortised=2 potential=2 total actual=8push(4): actual cost 1, potential +1, amortised cost 2.
8L1op=push(7) actual=1 ΔΦ=1 amortised=2 potential=3 total actual=9push(7): actual cost 1, potential +1, amortised cost 2.
9L3op=multipop(4) actual=3 ΔΦ=-3 amortised=0 potential=0 total actual=12multipop(4) removed 3 items: actual cost 3, potential -3, amortised cost 0. The potential drawn down pays for the pops.
10L1op=push(2) actual=1 ΔΦ=1 amortised=2 potential=1 total actual=13push(2): actual cost 1, potential +1, amortised cost 2.
11L3op=multipop(2) actual=1 ΔΦ=-1 amortised=0 potential=0 total actual=14multipop(2) removed 1 item: actual cost 1, potential -1, amortised cost 0. The potential drawn down pays for the pops.
12L6operations=10 total actual cost=14 total amortised=14 return=14Total 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.
4.06.6

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.

Java
1static int find(int[] parent, int x) {
2 while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } // path halving
3 return 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.