13. Dynamic Arrays & Amortized Analysis
799 words
4 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 13. Dynamic Arrays & Amortized Analysis > **What problem does this solve?** Static arrays have fixed capacity.

13. Dynamic Arrays & Amortized Analysis
What problem does this solve? Static arrays have fixed capacity. If you need more space, you must create a larger array and copy everything. Python's list is a dynamic array that grows automatically. Amortized analysis shows that despite occasional expensive resizes, each append is O(1) on average.
1. The Problem with Static Arrays
A static array has fixed capacity. Once it's full, you can't add more elements:
pseudoFixed array of capacity 4: [_, _, _, _] → full → can't insert!
Solution: When full, allocate a new array with double the capacity, copy all elements, and free the old array.
2. Dynamic Array Growth Strategy
(Diagram)
Python Simulation
python# runnable import ctypes # Low-level arrays class DynamicArray: """Simplified dynamic array (like Python's list).""" def __init__(self, capacity=1): self._n = 0 # Number of elements self._capacity = capacity # Actual allocated size self._A = self._make_array(capacity) def __len__(self): return self._n def __getitem__(self, k): if not 0 <= k < self._n: raise IndexError("Index out of bounds") return self._A[k] def append(self, obj): """Add element to end. Amortized O(1).""" if self._n == self._capacity: self._resize(2 * self._capacity) # Double when full self._A[self._n] = obj self._n += 1 def _resize(self, c): """Resize internal array to capacity c.""" B = self._make_array(c) for i in range(self._n): B[i] = self._A[i] self._A = B self._capacity = c def _make_array(self, c): """Return new array of capacity c.""" return (c * ctypes.py_object)() def insertion_cost(self, n): """Show how many copies each append requires.""" copies = 0 for i in range(n): if self._n == self._capacity: old_cap = self._capacity self._resize(2 * self._capacity) copies += old_cap # Copies during resize self._A[self._n] = i self._n += 1 return copies # Test amortized cost da = DynamicArray() print(f"Total copies for 1000 appends: {da.insertion_cost(1000)}") # If naive O(n²): 1000*999/2 = 499,500 copies # With doubling: ~2000 copies (much less!)
3. Amortized Analysis — The Banker's Method
Mental Model
Think of each append as depositing 3intoabankaccount.Eachnormalappendcosts1 (paid from the deposit). When a resize happens, it costs ntocopynelements—wepaythisfromtheaccumulateddeposits.Sinceeachappendpaid3 but most cost only $1, there's always enough saved for the expensive resize.
Why Each Append Is O(1) Amortized
| Operation | Cost | Deposit | Balance |
|---|---|---|---|
| Append 1st | 1 (resize to 1, copy 0) | $3 | $2 |
| Append 2nd (full) | 1 + 1 (resize, copy 1) | $3 | $3 |
| Append 3rd | 1 | $3 | $5 |
| Append 4th (full) | 1 + 2 (resize, copy 2) | $3 | $5 |
| Append 5th | 1 | $3 | $7 |
| Append 6th | 1 | $3 | $9 |
| Append 7th | 1 | $3 | $11 |
| Append 8th (full) | 1 + 4 (resize, copy 4) | $3 | $9 |
Total cost for n appends: Each of n elements is copied at most once per doubling. The number of doublings is log n. Each doubling copies at most n elements total. So total copies = O(n), and average cost per append = O(1).
Formal Proof
Let cᵢ be the cost of the i-th append.
- If the array isn't full: cᵢ = 1 (just write)
- If the array is full (size before = i - 1): cᵢ = i (write + copy i-1 elements) The actual total cost: 1 + 2 + 4 + 8 + ... + n ≈ 2n (since we only copy at powers of 2) Amortized cost per operation = 2n / n = O(1).
4. Comparison: Static vs Dynamic Arrays
| Feature | Static Array | Dynamic Array | Linked List |
|---|---|---|---|
| Access by index | O(1) | O(1) | O(n) |
| Append | N/A (fixed size) | O(1) amortized | O(1) with tail |
| Insert at front | O(n) | O(n) | O(1) |
| Memory | Minimal | Up to 2× waste | Node overhead |
| Cache locality | ✅ | ✅ (contiguous) | ❌ |
| Worst-case append | — | O(n) (resize) | O(1) |
Practice Questions
Q1. If you start with capacity 1 and double each time, how many copies occur for 100 appends?
Q2. What if you increased capacity by adding 10 instead of doubling? What would the amortized cost be?
Q3. Why does Python's list use 1.125× growth instead of 2×?
Q4. What is the space complexity of a dynamic array with n elements? What's the worst-case waste?
Q5. Prove that inserting at the front of a dynamic array is O(n).
AnswersA1. 1 + 2 + 4 + 8 + 16 + 32 + 64 = 127 copies for 100 appends (resizes at sizes 1, 2, 4, 8, 16, 32, 64).A2. If you add a constant k each resize: copies = k + 2k + 3k + ... + (n/k)*k = O(n²). Doubling is essential for O(1) amortized.A3. Memory efficiency — 1.125× wastes less space than 2× while still maintaining O(1) amortized growth.A4. Space = O(n). Worst case: n elements with capacity 2n (just before resize). So O(n) space with up to 100% waste.A5. Inserting at front requires shifting all n elements right by 1 — O(n) per insert. Join Discord Previous12. Recursion & BacktrackingNext14. Hash Tables — Dictionaries Under the Hood