Fundamentals

Amortized Analysis

What Amortized Analysis Is

Some data structures have an operation that is usually trivial and occasionally enormous. Appending to a dynamic array normally writes one slot, but when the buffer fills it allocates a bigger one and copies everything across. Quoting the worst case for that operation gives O(n), which is technically correct and deeply misleading — it suggests that building a list of a million items costs a trillion operations, when it actually costs about two million.

Amortized analysis measures the average cost per operation across a worst-case sequence of operations.Instead of asking "how bad can one append be?", it asks "how bad can n appends be, in total?" — and then divides. When the expensive cases are structurally guaranteed to be rare, that average is the honest number.

The Motivating Example — a Doubling Array

A dynamic array keeps a buffer with some capacity. Appending writes one element; if the buffer is full, it first allocates one of double the capacity and copies every existing element across:

function append(arr, value) {
  if (arr.size === arr.capacity) {
    const bigger = new Array(arr.capacity * 2);   // Θ(n) work,
    for (let i = 0; i < arr.size; i++) {          // but only when full
      bigger[i] = arr.buffer[i];
    }
    arr.buffer = bigger;
    arr.capacity *= 2;
  }

  arr.buffer[arr.size] = value;                   // Θ(1) the rest of the time
  arr.size++;
}

The copy loop is what makes the occasional append expensive.

Append #Capacity afterCostWhat happened
111No resize needed
221 + 1Capacity full: copy 1 element
341 + 2Capacity full: copy 2 elements
441Room to spare
581 + 4Capacity full: copy 4 elements
6–881 eachRoom to spare
9161 + 8Capacity full: copy 8 elements
amortized ≈ 1.97171append number
Most appends cost 1. The red spikes are resizes, and they double in height — but they also halve in frequency, which is exactly why the average stays flat.

The picture contains the entire argument. The spikes double in height — but they also double in spacing. Each resize is twice as expensive as the last and happens half as often, and those two effects cancel exactly.

Method 1 — Aggregate Analysis

The simplest method: add up the cost of the whole sequence, then divide by the number of operations. For n appends, the writes cost n, and the copies happen at sizes 1, 2, 4, 8, … up to n:

total = n + (1 + 2 + 4 + … + n)writes + copies
= n + (2n − 1)geometric sum
< 3n
amortized = total / n < 3 = O(1)

Running this for real confirms it: the total cost of n appends divided by n settles at about 2.05 and stays there, whether n is a thousand or a million. The bound of 3 is comfortable, and the important point is that it is a constant — it does not creep upward with n.

The reason doubling works is that the growth is geometric. If you instead grew the buffer by a fixed 10 slots each time, you would resize n/10 times, copying an average of n/2 elements each time — Θ(n²) in total, and Θ(n) amortized per append. The choice of growth factor is what creates the guarantee.

Aggregate Analysis of a Binary Counter

A second classic. Incrementing a binary counter flips a trailing run of 1s to 0 and then one 0 to 1, so a single increment can flip Θ(log n) bits. Watch what actually happens over a sequence:

IncrementCounterBits flippedRunning total
00 0 0 0
10 0 0 111
20 0 1 023
30 0 1 114
40 1 0 037
50 1 0 118
60 1 1 0210
70 1 1 1111
81 0 0 0415

After 8 increments only 15 bits have flipped, not 8 × 4. The reason is visible in the table: bit 0 flips every time, bit 1 flips every second increment, bit 2 every fourth, and so on:

total flips = n + n/2 + n/4 + n/8 + …
< 2ngeometric sum
amortized = O(1) per increment

Measured over 100,000 increments, the ratio is 1.99998 flips per increment — converging on 2 exactly as the series predicts.

Method 2 — The Accounting Method

Also called the banker's method. You invent a charge for each operation — its amortized cost — which may be more or less than what the operation really costs. The surplus is stored as credit on the data structure and later spent on expensive operations.

One rule makes the method valid: the credit balance must never go negative. If it never does, the total amount charged is an upper bound on the total real cost, which is exactly what an amortized bound claims.

For the dynamic array, charge 3 units per append and spend them as:

  • 1 unit pays for writing this element into the buffer — spent immediately.
  • 1 unit is banked to pay for copying this element at the next resize.
  • 1 unit is banked to pay for copying one older element — specifically, one of the elements that were already present at the last resize and so have no credit of their own.

When a resize of a size-n buffer happens, the n elements to be copied are covered: the n/2 elements added since the last resize carry 2 units each, which is enough for themselves and for one older element apiece. The balance never goes negative, so append is O(1) amortized.

The method's advantage over aggregate analysis is that different operations can carry different charges. For a stack supporting push, pop and multipop(k), charge 2 for a push (1 to push, 1 banked for the eventual pop) and 0 for pop and multipop. Every pop is then paid for by the credit its push left behind — so even a multipop that removes a thousand elements is free, because those thousand pushes already paid.

Method 3 — The Potential Method

The physicist's method, and the most powerful of the three. Instead of tracking credit on individual elements, define a potential function Φ that maps the whole state of the data structure to a number — the stored-up work it represents.

amortized cost = actual cost + Φ(after) − Φ(before)

Summed over a sequence, the Φ terms telescope: every intermediate value appears once positive and once negative, leaving only Φ(end) − Φ(start). So as long as Φ never drops below its starting value — usually arranged by setting Φ(D₀) = 0 and keeping Φ ≥ 0 — the total amortized cost is an upper bound on the total real cost.

Binary counter. Let Φ be the number of 1 bits. An increment that flips k trailing 1s to 0 and one 0 to 1 has actual cost k + 1, and changes the bit count by 1 − k:

amortized = (k + 1) + (1 − k)
= 2the k cancels entirely
= O(1)

The k vanishing is the whole trick. An expensive increment is expensive precisely because it destroys many 1 bits, and destroying them releases exactly the potential needed to pay for the work.

Dynamic array. Let Φ = 2·size − capacity. Immediately after a resize, size is half of capacity so Φ = 0; as appends fill the buffer, Φ climbs to equal capacity by the time the next resize is due — having accumulated precisely enough potential to fund the copy. Working through both cases gives an amortized cost of 3 per append, agreeing with the accounting method.

Comparing the Three Methods

MethodHow it worksTrade-offTypical use
AggregateTotal cost of n operations, divided by nSimplest, but gives one average for all operation typesBinary counter, dynamic array
AccountingOvercharge cheap operations, store the surplus as creditDifferent operations can have different amortized costsStack with multipop, dynamic array
PotentialDefine Φ over the data structure; amortized = actual + ΔΦMost powerful and most mechanical; no bookkeeping of individual creditsSplay trees, Fibonacci heaps, union-find

All three are provably equivalent in power — any bound one can establish, the others can too. Aggregate analysis is the one to reach for when every operation is the same kind. The accounting method suits structures with several operation types. The potential method is the standard choice for anything genuinely difficult, because once Φ is chosen the rest is mechanical.

Amortized Is Not Average-Case

These two are constantly conflated, and the distinction matters:

  • Average-case analysis assumes a probability distribution over inputs.
    • Quick sort is O(n log n) on average because we assume the pivot is usually reasonable. An unlucky input still costs O(n²).
  • Amortized analysis assumes nothing and involves no probability.
    • It is a worst-case guarantee about a whole sequence. No adversary can construct an input that makes n appends cost more than O(n) in total.
  • The guarantee is about the sequence, not any single operation.
    • One individual append genuinely can cost Θ(n). Amortized analysis never claims otherwise — it claims those expensive operations cannot happen often.

Quick sort is O(n log n) average-case: get unlucky and you still pay O(n²). A dynamic array is O(1) amortized: there is no bad luck available, because no sequence of n appends can cost more than O(n) in total. One is a statement about probability; the other is a guarantee.

Where Amortized Bounds Show Up

OperationWorst case, single opAmortizedWhy the amortized bound holds
Dynamic array appendΘ(n) on resizeO(1)Resizes double in cost but halve in frequency
Binary counter incrementΘ(log n) bit flipsO(1)Bit i only flips every 2ⁱ increments
Stack with multipopΘ(n) for one multipopO(1)Each element can only be popped once after being pushed
Hash table insert with rehashΘ(n) on rehashO(1)Same doubling argument as the dynamic array
Union-Find (rank + path compression)Θ(log n)O(α(n))α is the inverse Ackermann function — effectively ≤ 4
Splay tree operationsΘ(n)O(log n)A costly splay restructures the tree, making later ones cheap
Fibonacci heap extract-minΘ(n)O(log n)Consolidation is deferred until an extract forces it

Union-Find is the most striking entry. With union by rank and path compression, a sequence of m operations costs O(m·α(n)), where α is the inverse Ackermann function — a function that grows so slowly it is below 5 for any n that could be written down in this universe. The per-operation worst case is still logarithmic; only the amortized analysis reveals that the structure is effectively constant-time.

When an Amortized Bound Is Not Enough

Amortized analysis answers "how much work in total?". Some systems need to ask "how long will this onecall take?", and for them the average is the wrong statistic:

  • Real-time and safety-critical systems.
    • A pacemaker or an anti-lock braking controller cannot accept one operation taking 100× longer, even if the average is excellent. These systems need worst-case-per-operation bounds.
  • Interactive latency and tail percentiles.
    • A resize that stalls one request in a thousand shows up as a p99 latency spike. The amortized average is genuinely O(1) and the user experience is still bad.
  • Short sequences.
    • The guarantee is about long runs. If you perform three operations and one of them triggers a resize, the amortized bound has not had room to pay off.

The engineering response is not to abandon the structure but to spread the cost deliberately — incremental or background resizing, or preallocating capacity up front when the final size is known. Both convert a rare large stall into a small predictable overhead on every operation.

Common Mistakes

  • Confusing amortized with average-case.
    • Average-case involves probability over inputs. Amortized is a deterministic worst-case bound on a sequence, with no randomness anywhere.
  • Saying "append is O(1)" without qualification.
    • A single append is O(n) in the worst case. The correct phrasing is O(1) amortized, and dropping the qualifier is exactly what confuses people who then see a latency spike.
  • Letting credit go negative in the accounting method.
    • The proof only works if the balance is non-negative at every point in the sequence. Verifying that is the actual work of the method.
  • Choosing a potential function that can decrease below its start.
    • Φ must satisfy Φ(Dᵢ) ≥ Φ(D₀) for all i, usually arranged by making Φ(D₀) = 0 and Φ non-negative. Otherwise the telescoping sum does not bound the real cost.
  • Assuming growth by a constant amount works too.
    • Growing a array by adding 10 slots instead of doubling makes n appends cost Θ(n²) in total. Only geometric growth gives O(1) amortized.

Frequently Asked Questions

What is amortized analysis?

It is a way of measuring the cost of an operation by averaging it over a worst-case sequence of operations, rather than looking at a single operation in isolation. It is used when an occasional expensive operation is guaranteed to be paid for by many cheap ones — like appending to a dynamic array, where the rare resize is offset by all the appends that fit without resizing.

Is amortized analysis the same as average-case analysis?

No, and this is the most common confusion. Average-case analysis assumes a probability distribution over inputs and tells you what happens typically. Amortized analysis involves no probability at all: it is a worst-case guarantee about the total cost of a sequence. No adversarial input can make n appends to a dynamic array cost more than O(n) overall.

Why is appending to a dynamic array O(1) amortized?

When the array is full it allocates a buffer of double the size and copies everything across, which costs Θ(n). But doubling means the next resize is twice as far away. Over n appends the copies total 1 + 2 + 4 + ... + n, which is less than 2n, so the whole sequence costs O(n) and each append averages O(1).

What are the three methods of amortized analysis?

Aggregate analysis totals the cost of n operations and divides by n. The accounting method assigns each operation an amortized charge, banking the surplus from cheap operations as credit to pay for expensive ones. The potential method defines a function Φ over the data structure's state, with the amortized cost being the actual cost plus the change in Φ. All three give the same answers; they differ in convenience.

What is the potential method?

You define a potential function Φ that maps the data structure's state to a number representing stored-up work. The amortized cost of an operation is its actual cost plus Φ(after) − Φ(before). Because the Φ terms telescope across a sequence, the total amortized cost bounds the total actual cost whenever Φ never drops below its starting value.

When is an amortized bound not good enough?

When any individual operation being slow is unacceptable. Real-time systems, safety-critical controllers and latency-sensitive services all care about the worst single operation, not the average across a sequence — a resize that stalls one request in a thousand still shows up as a p99 latency spike even though the amortized cost is O(1).

Key Takeaways

  • Amortized cost is the average per operation over a worst-case sequence — not over a distribution of inputs.
  • It applies when expensive operations are structurally rare, such as a resize that doubles in cost but halves in frequency.
  • Aggregate analysis divides the total by n; the accounting method banks credit; the potential method tracks a function Φ.
  • The accounting method requires credit to stay non-negative; the potential method requires Φ never to fall below its starting value.
  • Geometric growth is what creates the guarantee — growing by a fixed amount gives Θ(n) amortized, not O(1).
  • An amortized bound says nothing about any single operation, so it is the wrong tool for real-time or tail-latency requirements.

Measuring the Amortized Cost Yourself

// Amortized analysis, measured rather than asserted.

// ---- A dynamic array that reports what each append really costs ----
class DynamicArray {
  constructor() {
    this.buffer = new Array(1);
    this.capacity = 1;
    this.size = 0;
    this.totalCost = 0;
  }

  append(value) {
    let cost = 1;                       // the write itself

    if (this.size === this.capacity) {  // full: grow and copy
      const bigger = new Array(this.capacity * 2);
      for (let i = 0; i < this.size; i++) bigger[i] = this.buffer[i];

      cost += this.size;                // Theta(n) - but only sometimes
      this.buffer = bigger;
      this.capacity *= 2;
    }

    this.buffer[this.size++] = value;
    this.totalCost += cost;
    return cost;
  }
}

// Aggregate analysis says total < 3n, so the average is O(1).
function measureAppends(n) {
  const arr = new DynamicArray();
  for (let i = 0; i < n; i++) arr.append(i);

  console.log(`n=${n}\ttotal=${arr.totalCost}\tper append=${(arr.totalCost / n).toFixed(3)}`);
}

// ---- Growing by a CONSTANT instead: the guarantee disappears ----
function measureConstantGrowth(n, step = 10) {
  let capacity = step, size = 0, totalCost = 0;

  for (let i = 0; i < n; i++) {
    let cost = 1;
    if (size === capacity) {
      cost += size;                     // copy everything, again
      capacity += step;                 // ... only to fill up 10 slots later
    }
    totalCost += cost;
    size++;
  }
  console.log(`n=${n}\ttotal=${totalCost}\tper append=${(totalCost / n).toFixed(1)} <- grows with n`);
}

// ---- Binary counter: a single increment can flip many bits ----
// Potential method: let Phi = number of 1 bits. An increment flipping k
// trailing 1s has actual cost k+1 and changes Phi by 1-k, so the
// amortized cost is (k+1) + (1-k) = 2.
function measureCounter(n) {
  const bits = [];
  let flips = 0;

  for (let k = 0; k < n; k++) {
    let i = 0;
    while (bits[i] === 1) { bits[i] = 0; flips++; i++; }
    bits[i] = 1; flips++;
  }
  console.log(`n=${n}\tflips=${flips}\tper increment=${(flips / n).toFixed(5)}`);
}

console.log("Doubling array - per-append cost stays constant:");
[1000, 10000, 100000, 1000000].forEach(measureAppends);

console.log("\nConstant growth - per-append cost does NOT stay constant:");
[1000, 10000, 100000].forEach((n) => measureConstantGrowth(n));

console.log("\nBinary counter - converges on 2 flips per increment:");
[100, 10000, 1000000].forEach(measureCounter);