Back to Technology

Amortized Analysis & Dynamic Array Growth

June 10, 2026 Wasil Zafar 14 min read

Worst-case analysis often lies. Understand why dynamic array append is O(1) amortized — not O(n) — through aggregate, accounting, and potential method proofs. Essential for senior-level interviews and systems thinking.

Table of Contents

  1. Why Amortized Analysis
  2. Aggregate Method
  3. Accounting Method
  4. Potential Method
  5. Growth Factor Tradeoffs
  6. Other Amortized Structures
  7. Interview & Practice

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.

Dynamic Array: Resize Events During Append Sequence
                                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)CostCredits deposited
Pay actual cost1+2 credits (for future copy)
Cheap append total1Bank: +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 / ImplementationGrowth FactorRationale
CPython list~1.125 (incremental)Minimises memory waste; uses a formula: new_size = old_size + old_size >> 3 + 6
Java ArrayList1.5×Balance between memory overhead and copy frequency
C++ std::vector2× (GCC), 1.5× (MSVC)Standard doesn't specify; 2× is most common
Go slices2× (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