Skip to content
Part IA Lent Term

Dynamic Arrays: Amortized Analysis

Dynamic arrays (Python lists, Java ArrayLists, C++ vectors) provide O(1)O(1) amortised append by doubling capacity when full. All three amortised analysis methods confirm this bound.

Dynamic array doubling: capacity grows 1\u21922\u21924\u21928 on repeated appends

The Data Structure

A dynamic array stores elements in an underlying static array. When the array fills up (size=capacity\text{size} = \text{capacity}), a new array of double the capacity is allocated, all elements are copied, and the old array is freed.

  • append: write element at position size, then size++. If size == capacity before the write, resize first.
  • Actual cost: O(1)O(1) normally, O(k)O(k) when copying kk elements during resize.

Aggregate Analysis

Starting from capacity 1, resizes occur at sizes 1,2,4,8,,2logn1, 2, 4, 8, \ldots, 2^{\lfloor \log n \rfloor}. Total element copies over nn appends:

i=0logn2i=2logn+11<2n\sum_{i=0}^{\lfloor \log n \rfloor} 2^i = 2^{\lfloor \log n \rfloor + 1} - 1 < 2n

Plus nn writes for the appends themselves. Total cost: <3n< 3n. Amortised per append: <3=O(1)< 3 = O(1).

Accounting Method

Charge c^=3\hat{c} = 3 per append:

  • 1 unit pays for writing the element
  • 2 units are stored as credit on the inserted element

When resizing from capacity kk to 2k2k, the kk elements in the array have accumulated 2k2k total credit. Copying them costs kk, consuming kk credits. The remaining kk credits are “lost” with the old array, but this is acceptable because elements in the new array will accumulate fresh credits.

Crucial insight: the credit on each element only needs to last until the next resize. At any point, at most half the elements (those in the first half of the array) need to have accumulated their 2 credits, because the resize only copies the first half’s worth of elements once. The inequality holds: total credit stored \ge total copying cost over any sequence.

Potential Method

Define the potential function:

Φ=2sizecapacity\Phi = 2 \cdot \text{size} - \text{capacity}

This is always non-negative (since capacitysize\text{capacity} \ge \text{size}, and capacity2size\text{capacity} \le 2 \cdot \text{size} — the array doubles only when full, so capacity is at most twice the size at all times except right after a resize).

Φ(empty)=0\Phi(\text{empty}) = 0 (size = 0, capacity = 1: 201=12 \cdot 0 - 1 = -1 — this is not 0\ge 0!). Fix: start with capacity 0, or define Φ=max(0,2sizecapacity)\Phi = \max(0, 2 \cdot \text{size} - \text{capacity}). With capacity 0 initially, Φ=0\Phi = 0.

Append without resize (size ss, capacity cc, s<cs < c):

  • Actual cost: c=1c = 1 (one write)
  • ΔΦ=[2(s+1)c][2sc]=2\Delta\Phi = [2(s+1) - c] - [2s - c] = 2
  • c^=1+2=3\hat{c} = 1 + 2 = 3

Append with resize (size s=cs = c, old capacity kk, new capacity 2k2k):

  • Actual cost: c=1+kc = 1 + k (one write plus kk copies)
  • Before: Φbefore=2kk=k\Phi_{\text{before}} = 2k - k = k, After: Φafter=2(k+1)2k=2\Phi_{\text{after}} = 2(k+1) - 2k = 2
  • ΔΦ=2k\Delta\Phi = 2 - k
  • c^=(k+1)+(2k)=3\hat{c} = (k+1) + (2-k) = 3

Amortised cost per append: O(1)O(1) consistently.

Deletion and Shrinking

To maintain O(1)O(1) amortised for both append and delete (pop), shrink the array when the load factor drops below 1/41/4. When deleting causes size<capacity/4\text{size} < \text{capacity}/4, halve the capacity. This prevents thrashing (alternating append/delete triggering resize every time).

Potential function for both operations: Φ=2sizecapacity\Phi = |2 \cdot \text{size} - \text{capacity}|, or the textbook variant Φ=2sizecapacity\Phi = 2 \cdot \text{size} - \text{capacity} for append-heavy and a symmetric form for delete-heavy.

Summary

MethodResultKey detail
AggregateO(1)O(1) amortisedSum of copies <2n< 2n
AccountingO(1)O(1) amortisedCharge 3, store 2 credits per element
PotentialO(1)O(1) amortisedΦ=2sizecapacity\Phi = 2 \cdot \text{size} - \text{capacity}
Shrink threshold1/41/4 load factorPrevents thrashing

Past Tripos: y2024p1q9, y2023p2q7.