01 — Time/Space Complexity & Dynamic Arrays

At a glance

After completing this lesson you can:

Time: ~50 minutes · Prerequisites: Phase 0 Complete

Reading Companion

Why this matters in production

In production, an algorithm running at $O(n)$ handles a surge of 100,000 incoming webhooks in 20 milliseconds. An algorithm running at $O(n^2)$—often introduced innocently via repeated list.insert(0, ...) or string/list additions inside loops—will freeze CPU cores for 45 minutes, triggering gateway timeouts (HTTP 504) and cascading service failures across the fleet.

Deep dive

1. Big-O: Asymptotic Analysis for Production

Big-O measures how resource consumption (time or memory) scales as input size $n$ approaches infinity:

2. CPython Dynamic Arrays (list) Under the Hood

In CPython, a list is not a linked list; it is a contiguous array of pointer references (PyObject**).

CPython list resize pattern:
Size 0 -> Allocated 0
Append 1 -> Allocates 4 slots [p0, _, _, _]
Append 2, 3, 4 -> Fills existing slots (O(1))
Append 5 -> Reallocates to 8 slots [p0..p4, _, _, _] (copies pointers)

Hands on

python3 phase1-cs-and-architecture/01-data-structures/01-complexity-and-arrays/examples/01_demo.py
python3 phase1-cs-and-architecture/01-data-structures/01-complexity-and-arrays/examples/02_trap.py

AI Trap & Code Review

# BAD AI-GENERATED CODE — UNINTENTIONAL O(n^2) PAYLOAD ASSEMBLY
def prepend_audit_logs(events: list[dict], new_events: list[dict]) -> list[dict]:
    result = []
    for ev in events:
        result.append(ev)
    for new_ev in new_events:
        result.insert(0, new_ev)  # O(n) shift on EVERY insertion -> O(n*m) runtime!
    return result

Exercises

  1. Rung 1 (estimate_growth_category): Classify runtime scaling ratios into "O(1)", "O(log n)", "O(n)", "O(n log n)", or "O(n^2)" given baseline and scaled timing measurements.
  2. Rung 2 (collect_events_fifo — Fix the AI Bug): Fix an $O(n^2)$ FIFO collector that uses list.insert(0, item) by refactoring it to linear $O(n)$ execution time.
  3. Rung 3 (amortized_capacity_tracker): Simulate CPython's list append growth curve: given a sequence of append operations, record every reallocation step (old capacity, new capacity, copied element count).
  4. Rung 4 (merge_sorted_intervals): Implement $O(n \log n)$ interval merging for overlapping time ranges [(start_1, end_1), ...].
  5. Rung 5 (bounded_fast_buffer): Build an in-memory fixed-capacity append buffer that drops the oldest items when exceeding max_size in strict $O(1)$ amortized time per append and index lookup.

Wrapping up

Can you explain?

  1. Why is list.append amortized $O(1)$ whereas list.insert(0, val) is strictly $O(n)$?
  2. What is the difference between time complexity and auxiliary space complexity?
  3. Why does slicing a list (items[:k]) allocate new memory in Python?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 01 Complexity & Dynamic Arrays

  1. Why list.append is amortized $O(1)$: CPython dynamically over-allocates proportional extra memory whenever the array fills up. Because an $O(n)$ copy is performed only after $n$ elements have been added, the cost of copying averaged over all additions is $O(1)$.
  2. list.insert(0) catastrophe: Inserting at position 0 requires shifting all existing $n$ pointer references in RAM by 1 slot. Doing this $n$ times produces an $O(n^2)$ total runtime.
  3. collections.deque vs list: A deque uses a doubly-linked list of 64-element memory blocks, allowing $O(1)$ appends and pops from both ends without shifting arrays.

Self-check

Run python3 -m unittest phase1-cs-and-architecture/01-data-structures/01-complexity-and-arrays/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.