Beginner: foundations · Thinking in Big-O · topic 3 of 61 · 30 min
Space complexity and amortised cost
Explanation
Why memory matters too
Memory is limited. A phone app, a small cloud server or an online judge with a 256 MB limit will crash or be killed if your code builds huge lists it does not need. So interviewers often ask two questions: how much time, and how much space?
Space complexity is how the extra memory your code uses grows with n. "Extra" (also called auxiliary space) means memory beyond the input itself. Most interviewers do not count the input, and many do not count the output you must return; if unsure, say which you mean.
def total(nums): # O(1) extra space: one number
s = 0
for x in nums:
s += x
return s
def squares(nums): # O(n) extra space: a new list of n items
return [x * x for x in nums]
print(total([1, 2, 3]), squares([1, 2, 3])) # Output: 6 [1, 4, 9]In-place algorithms
An in-place algorithm changes the input directly and uses only a few extra variables, O(1) extra space. Reversing a list by swapping the two ends and moving inwards is in place. Building a reversed copy with nums[::-1] is not: it creates a whole new list, O(n) extra space.
def reverse_in_place(nums):
left, right = 0, len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
a = [1, 2, 3, 4]
reverse_in_place(a)
print(a) # Output: [4, 3, 2, 1]Recursion uses stack space
When a function calls itself, each call that has not finished waits in memory on the call stack, in a "frame" holding its local variables. If the recursion goes n levels deep, it uses O(n) space even if it creates no lists. Python stops very deep recursion with a RecursionError; the default limit is about 1,000 levels (sys.getrecursionlimit() returns 1000 by default).
def total_rec(nums, i=0):
if i == len(nums):
return 0
return nums[i] + total_rec(nums, i + 1)
print(total_rec([5, 6, 7])) # Output: 18Amortised cost: why append is fast
A Python list is a dynamic array: a block of memory with some spare room at the end. append puts the new item in the spare room, which is O(1). When the room runs out, Python allocates a bigger block and copies every item across, which is O(n) for that one append.
Amortised cost means the average cost per operation over a long sequence of operations. Suppose the capacity doubles each time it fills: 1, 2, 4, 8 … To reach n items, the copies add up to 1 + 2 + 4 + … which is less than 2n. So n appends cost O(n) in total, which is O(1) amortised per append. CPython actually grows by a smaller factor (roughly an eighth extra), but any growth by a constant factor gives the same O(1) amortised result.
| Append number | Capacity before | Copies made | Total copies so far |
|---|---|---|---|
| 1 | 0 → grow to 1 | 0 | 0 |
| 2 | 1 (full) → grow to 2 | 1 | 1 |
| 3 | 2 (full) → grow to 4 | 2 | 3 |
| 5 | 4 (full) → grow to 8 | 4 | 7 |
| 9 | 8 (full) → grow to 16 | 8 | 15 |
Trading memory for time
Often you can spend memory to save time. A set of seen items costs O(n) memory but turns O(n²) duplicate checks into O(n). Caching, also called memoisation, stores answers you have already computed so you never compute them twice.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(80)) # Output: 23416728348467685Common mistakes
- Forgetting the recursion stack. Fix: count the maximum depth of recursion as space.
- Slicing inside recursion or loops. nums[1:] copies n − 1 items each time, so a recursive sum written with slices is O(n²) time and space. Fix: pass an index instead.
- Calling something in place when it builds a new list. Fix: in-place code changes the list you were given and does not create another of size n.
- Thinking amortised O(1) means every single append is O(1). Fix: one append in many can be O(n); only the average is O(1).
- Rebinding instead of changing: nums = nums[::-1] inside a function does not change the caller's list. Fix: change items by index, or write nums[:] = … if a copy is acceptable.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Space complexity and amortised cost · 1/8
Time is not the only cost. This lesson is about memory: space complexity, in-place code, recursion and amortised cost.
Prefer a video by a teacher? Find free videos on “space complexity amortized analysis dynamic array explained” on YouTube ↗
In real life: A school marks portal on a small server
A school results portal runs on a small cloud server shared by many schools. A report that keeps a running class total uses O(1) extra memory, whatever the class size. A report that first copies every student record into a new list for each subject uses O(students × subjects) memory and can make the server run out of memory during results week. Thinking about space, not only time, keeps such systems stable.
marks = [78, 91, 66, 85]
running = 0
for i in range(len(marks)):
running += marks[i]
print("After student", i + 1, "total is", running)Practice (0/3 done)
Write reverse_in_place(nums) that reverses the list nums IN PLACE with O(1) extra space and returns None. Do not use reversed(), slicing (such as nums[::-1]) or nums.reverse(); swap items from the two ends instead.
Write running_sums(nums) that changes nums IN PLACE so that nums[i] becomes the sum of nums[0] to nums[i], and returns the same list. Use O(1) extra space (no second list). Example: [1, 2, 3, 4] becomes [1, 3, 6, 10].
What is the TIME complexity of this function on a list of n numbers? def total(nums): if not nums: return 0 return nums[0] + total(nums[1:])
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Best, average and worst case; analysing loops and recursionThinking in Big-O · same module
- Recursion: functions that call themselvesRecursion and backtracking
- Arrays: traversal and in-place updatesArrays and strings
- Recursion and sorting algorithmsPython Programming · Algorithms, data structures and testing
- std::vector in depthC++ Programming (modern C++ and DSA) · The Standard Template Library (STL)
- RecursionC Programming (from zero to systems) · Functions and arrays
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does O(1) extra space mean?
Answer: The extra memory stays the same size whatever n is
A fixed number of variables, however large the input: constant extra space.
A recursive function calls itself n levels deep and makes no lists. What is its space complexity?
Answer: O(n)
Each waiting call keeps a frame on the call stack, so n frames means O(n) space.
Why is list.append O(1) amortised and not O(1) every time?
Answer: Occasionally the list is full and must be copied to a bigger block
Most appends use spare room; a few trigger an O(n) copy. Averaged over many appends, the cost is O(1).
Which reverse is in place?
Answer: Swapping a[l] and a[r] while moving l and r towards each other
Only swapping changes the original list using O(1) extra space; the others build a new list.
Memoisation (caching answers) usually trades…
Answer: more memory for less time
Stored answers cost memory but avoid repeated work.
What is the extra space of a recursive function that goes n levels deep and creates no lists?Related
Answer: O(n)
n frames wait on the call stack at the deepest point, so the space is O(n).
What is the extra space of the three-reversal rotation?Related
Answer: O(1)
It only swaps items using a couple of index variables.
What does it mean that Python's sort is stable?Related
Answer: Items that compare equal keep their original order
Stable sorting keeps equal items in the order they had before, which lets you sort by several keys in steps.
Inside a loop over n items you write if x in my_list, where my_list also has n items. What is the total cost?Related
Answer: O(n²)
x in a list scans the list, O(n), and it runs n times: O(n²). A set would make it O(n).
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Thinking in Big-O” before its exam: Best, average and worst case; analysing loops and recursion, The real cost of Python and JavaScript built-ins.
- Module 1 exam: Thinking in Big-O · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Arrays and strings. 61 topics left in the course before the final exam.