Complexity & Big-O
Reason about time and space the way interviewers do. Watch how different growth rates diverge as input size climbs.
Before you can compare two solutions, you need a language for “how expensive is this?” that ignores the noise of a particular laptop, language, or input. That language is Big-O notation: a way to describe how an algorithm’s cost grows as its input grows.
The key word is grows. Big-O throws away constants and lower-order terms because, as the input gets large, only the fastest-growing term matters. A loop that does 3n + 50 operations is simply O(n) — double the input and you roughly double the work.
Watch the curves diverge
The whole reason we obsess over Big-O is this picture. At small inputs, an O(n²) algorithm can look perfectly fine. Drag the slider and watch what happens as the input grows — the quadratic curve leaves everything else behind.
Notice that O(log n) is almost flat: even at n = 40 it has barely moved. That flatness is why binary search and balanced trees feel like magic. Meanwhile O(n²) is the shape of a nested loop, and it is the complexity you most often need to avoid in an interview.
The complexities you must recognize on sight
| Notation | Name | Typical source |
|---|---|---|
| O(1) | Constant | Hash lookup, array index, arithmetic |
| O(log n) | Logarithmic | Binary search, balanced tree operations |
| O(n) | Linear | A single pass over the data |
| O(n log n) | Linearithmic | The best comparison sorts, divide & conquer |
| O(n²) | Quadratic | Nested loops over the same collection |
| O(2ⁿ) | Exponential | Naive recursion over subsets |
Time is not the only cost
Every algorithm also has a space complexity — the extra memory it needs beyond the input. A recursive function that goes n levels deep uses O(n) stack space. Building a hash set of every element uses O(n) memory. Interviewers love the time/space trade-off, because “make it faster by using more memory” (or the reverse) is the single most common optimization in real systems.
Rule of thumb: state your solution’s time and space complexity out loud, unprompted. It signals that you think about cost by default.
How to reason about it quickly
- Count the loops. Independent loops add; nested loops over the same input multiply. Two separate passes are O(n) + O(n) = O(n). A loop inside a loop is O(n²).
- Find the halving. If each step throws away a constant fraction of the remaining work, you have a logarithm.
- Recursion → recurrence. “Do O(n) work, then two half-size calls” is the merge-sort recurrence and resolves to O(n log n).
Keep this lens on as you go through the rest of the module — every pattern that follows is really just a trick to move a solution down this table.
Pattern recognition
Variations
Worked problems
These are not about inventing an algorithm. They are about reading code like an interviewer reads it: count the dominant work, then state the space trade-off.
Analyze a Set-Based Scan
Given this function, identify its time and auxiliary space complexity in terms of n = len(nums):
def first_duplicate(nums):
seen = set()
for x in nums:
if x in seen:
return x
seen.add(x)
return NoneApproach. The loop may return early, but Big-O usually asks for the worst case. Hash lookups and inserts are average O(1), so the loop dominates. The set can grow to hold every value.
Show solution
def first_duplicate(nums):
seen = set() # up to n elements
for x in nums: # at most n iterations
if x in seen: # average O(1)
return x
seen.add(x) # average O(1)
return NoneComplexity. O(n) time and O(n) auxiliary space. Early returns can improve a specific run, but they do not change the worst-case bound.
Count the Nested Work
What is the time complexity of this pair-counting function, and why is it not O(n) just because the inner loop gets shorter?
def count_pairs(nums):
total = 0
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] < nums[j]:
total += 1
return totalApproach. Add the inner-loop lengths: (n - 1) + (n - 2) + ... + 1. That sum is n(n - 1) / 2, and Big-O keeps the dominant term.
Show solution
def pair_iterations(n):
return n * (n - 1) // 2Complexity. O(n²) time and O(1) auxiliary space. Triangular nested loops are still quadratic; they just do about half as many iterations as the full n * n grid.
Choose the Better Membership Strategy
You have n blocked user IDs and q incoming IDs to check. Approach A scans the blocked list for every query. Approach B builds a set once, then checks each query against the set. Which approach scales better?
Approach. Compare total cost, not just setup. A repeated scan costs O(n) per query, so O(n · q). A set costs O(n) to build and average O(1) per query, so O(n + q), at the price of O(n) extra memory.
Show solution
def mark_blocked(blocked_ids, incoming_ids):
blocked = set(blocked_ids)
return [user_id in blocked for user_id in incoming_ids]Complexity. O(n + q) time and O(n) auxiliary space. This beats O(n · q) once you have more than a tiny number of queries.