Quiz 2

13. Dynamic Arrays & Amortized Analysis

799 words
4 min read
Python Week 1: the first filter for runtime behavior
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:
pseudo
Fixed 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.Eachnormalappendcosts3 into a bank account. Each normal append costs1 (paid from the deposit). When a resize happens, it costs ntocopynelementswepaythisfromtheaccumulateddeposits.Sinceeachappendpaidn to copy n elements — we pay this from the accumulated deposits. Since each append paid3 but most cost only $1, there's always enough saved for the expensive resize.

Why Each Append Is O(1) Amortized

OperationCostDepositBalance
Append 1st1 (resize to 1, copy 0)$3$2
Append 2nd (full)1 + 1 (resize, copy 1)$3$3
Append 3rd1$3$5
Append 4th (full)1 + 2 (resize, copy 2)$3$5
Append 5th1$3$7
Append 6th1$3$9
Append 7th1$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

FeatureStatic ArrayDynamic ArrayLinked List
Access by indexO(1)O(1)O(n)
AppendN/A (fixed size)O(1) amortizedO(1) with tail
Insert at frontO(n)O(n)O(1)
MemoryMinimalUp to 2× wasteNode overhead
Cache locality✅ (contiguous)
Worst-case appendO(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).
Answers
A1. 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
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.