Space Complexity & Amortized Analysis
Algorithms & Complexity
Chapter 8 · Space Complexity & Amortized Analysis
Every chapter so far measured time. This chapter measures the other resource that matters just as much in practice — memory — and then covers a genuinely different kind of question: what happens when a single operation is occasionally expensive, but only rarely?
Auxiliary Space vs. Input Space
Bubble sort (Chapter 7) sorts in place — a couple of temporary variables for swapping, regardless of array size: O(1) auxiliary space, even though it touches all O(n) of the input.
Merge Sort's Hidden Cost: O(n) Space
O(n) auxiliary space. Bubble sort's O(n²) time comes with only O(1) space; merge sort's much better O(n log n) time comes at the cost of genuine extra memory. Neither algorithm is simply "better" in every dimension — this is exactly the kind of tradeoff a real engineering decision has to weigh.
Recursive Call Stack Space
Every recursive call adds a frame to the call stack, and that frame occupies real memory until the call returns. Chapter 5's own countdown function makes n nested calls before hitting its base case — O(n) auxiliary space, purely from stacked call frames, even though it allocates nothing else.
fib(20) makes 21,891 total calls over its full execution, yet its call stack never exceeds depth 19. Time (total work done) and space (maximum simultaneous memory) are genuinely different measurements — a function can be catastrophically slow while still being memory-cheap.
Time and Space Side by Side
| Algorithm | Time | Space | Why |
|---|---|---|---|
| Bubble sort | O(n²) | O(1) | Sorts in place, a few temp variables |
| Merge sort | O(n log n) | O(n) | Temporary arrays needed for merging |
| Countdown (Ch.5) | O(n) | O(n) | Call stack depth grows with n |
| Naive Fibonacci (Ch.5) | O(2ⁿ) | O(n) | Exponential total calls, but only linear stack depth at once |
Amortized Analysis: The Dynamic Array
A dynamic array (like Python's own list) grows by doubling its capacity whenever it fills up — copying every existing element into a new, larger array. That single resize is O(n). But it doesn't happen on every append.
Simulating 16 appends, starting from capacity 1 and doubling each time it fills:
| Resize event | Elements copied |
|---|---|
| Capacity 1 → 2 | 1 |
| Capacity 2 → 4 | 2 |
| Capacity 4 → 8 | 4 |
| Capacity 8 → 16 | 8 |
Total copy cost: 1+2+4+8 = 15. Plus 16 individual element placements: 15 + 16 = 31 total operations for 16 appends — ≈1.94 operations per append, even though the single most expensive append (the one triggering the capacity-8-to-16 resize) cost 9 operations on its own.
n=100: 227 total operations, ≈2.27 per append. At n=1,000: 2,023 total operations, ≈2.02 per append. The per-append cost doesn't grow with n — it hovers around 2, confirming O(1) amortized time, even though any single append can, rarely, cost O(n).
Space & Amortized Analysis in Code
Hands-On Exercises
A function reverse_new(arr) builds and returns a brand-new list containing arr's elements in reverse order. A second function reverse_in_place(arr) swaps elements within the same array using two index pointers moving toward each other. State the auxiliary space complexity of each, and explain the difference using this chapter's own input-space-vs-auxiliary-space distinction.
Using this chapter's own dynamic-array-doubling method, compute the total operation count and the amortized cost per append for n = 32 appends (starting from capacity 1, doubling at each resize). Show each resize event's own copy cost.
Explain, in your own words, why naive recursive Fibonacci uses only O(n) auxiliary space despite making O(2ⁿ) total function calls over its full execution. Ground your answer in this chapter's own distinction between total work done (time) and maximum simultaneous memory in use (space).
📄 View solutionChapter 8 Quick Reference
- Auxiliary space (extra memory beyond the input) is what space complexity almost always measures — input space is usually not counted
- Recursive calls consume stack space proportional to recursion depth, not total calls made — naive Fibonacci is O(2ⁿ) time but only O(n) space
- Merge sort's O(n log n) time comes with a real O(n) space cost, versus bubble sort's O(1) space at O(n²) time — a genuine tradeoff, not a strict improvement
- Amortized analysis: the average cost per operation across a long sequence, even when individual operations vary wildly
- Dynamic array doubling: occasional O(n) resizes, but O(1) amortized time per append — verified directly (≈2 operations per append, staying constant as n grows)
- Next chapter: Beyond polynomial time — exponential growth and a taste of P vs. NP