Big-O & Complexity Analysis
How to measure an algorithm's cost, read constraints to guess the intended solution, and reason about time and space like an interviewer.
Why complexity matters
Two correct programs can differ by a factor of a million in speed. Complexity analysis is how we predict that before running anything. In interviews it's also a language: when you say “this is O(n log n) time and O(n) space” you're telling the interviewer you understand why your code is fast.
We count how the number of basic operations grows as the input size n grows, ignoring constant factors and lower-order terms. We care about the growth rate because for large inputs, it dominates everything else.
The notation
| Notation | Meaning | Informal reading |
|---|---|---|
| O(f(n)) | Upper bound: grows no faster than f(n) | “at most” |
| Ω(f(n)) | Lower bound: grows at least as fast as f(n) | “at least” |
| Θ(f(n)) | Tight bound: both O and Ω | “exactly, up to constants” |
In practice (and in interviews) people say “Big-O” when they mean the tight bound for the worst case. That's fine — just be precise when it matters, e.g. “quicksort is O(n log n) on average but O(n²) in the worst case.”
Rules for simplifying
- Drop constants: O(3n + 5) → O(n).
- Drop lower-order terms: O(n² + n log n + 100) → O(n²).
- Different inputs get different variables: looping over array
athen arraybis O(a + b), not O(n). Nested loops over them are O(a·b). - Logs don't care about base: log₂ n and log₁₀ n differ by a constant factor, so we just write log n.
The common growth classes
| Class | Name | Typical source | n = 10⁶ ops (approx.) |
|---|---|---|---|
| O(1) | constant | array index, hash lookup | 1 |
| O(log n) | logarithmic | binary search, balanced tree op | 20 |
| O(√n) | square root | trial division primality | 1,000 |
| O(n) | linear | single pass | 10⁶ |
| O(n log n) | linearithmic | sorting, divide & conquer | 2·10⁷ |
| O(n²) | quadratic | all pairs | 10¹² ❌ |
| O(n³) | cubic | all triples, Floyd-Warshall | 10¹⁸ ❌ |
| O(2ⁿ) | exponential | all subsets | impossible |
| O(n!) | factorial | all permutations | impossible |
Reading constraints to guess the solution
This is the most practical skill on this page. A typical judge runs roughly 10⁸ simple operations per second. So the input limits tell you which complexities are allowed:
| Max n | Acceptable complexity | Likely technique |
|---|---|---|
| n ≤ 10 | O(n!), O(n·2ⁿ) | permutations, brute-force backtracking |
| n ≤ 20 | O(2ⁿ), O(n·2ⁿ) | subsets, bitmask DP |
| n ≤ 100 | O(n³) | Floyd-Warshall, interval DP |
| n ≤ 1,000 | O(n²) | 2D DP, all pairs |
| n ≤ 10⁵ | O(n log n) | sorting, heaps, binary search, segment trees |
| n ≤ 10⁶ | O(n) or O(n log n) with small constant | two pointers, prefix sums, hashing |
| n ≤ 10⁹ or more | O(log n) or O(1) | binary search on answer, math formula |
Analyzing code
Sequential statements add
for x in arr: # O(n)
print(x)
for x in arr: # O(n)
for y in arr: # O(n) each → O(n²)
print(x, y)
# total: O(n + n²) = O(n²)
Nested loops multiply — but check the bounds
for i in range(n):
for j in range(i + 1, n): # runs n-1, n-2, ..., 0 times
...
# sum = n(n-1)/2 → still O(n²)
i = 1
while i < n:
i *= 2 # i doubles: 1, 2, 4, ... → O(log n) iterations
for i in range(n):
j = 1
while j < n:
j *= 2 # O(log n) per outer iteration → O(n log n) total
The “harmonic” loop
for i in range(1, n + 1):
for j in range(i, n + 1, i): # n/i iterations
...
# n/1 + n/2 + n/3 + ... + n/n = n · H(n) ≈ n ln n → O(n log n)
This shows up in sieves and divisor enumeration and surprises a lot of people.
Amortized analysis
Some operations are occasionally expensive but cheap on average over a sequence. The classic example is a dynamic array (Python list, C++ vector): when it fills, it doubles capacity and copies everything (O(n)), but that happens so rarely that n appends cost O(n) total → O(1) amortized per append.
Other amortized examples you'll meet:
- Monotonic stack: an inner
whileloop pops, but each element is pushed and popped at most once → O(n) total. - Sliding window: the left pointer only moves forward → O(n) total even though there's a nested loop.
- Union-Find with path compression + union by rank: nearly O(1) per operation (inverse Ackermann).
Recursion complexity
For a recursive function, time = (number of calls) × (work per call). Draw the recursion tree.
def fib(n):
if n < 2: return n
return fib(n - 1) + fib(n - 2)
Each call branches into 2, depth n → about 2ⁿ calls → O(2ⁿ). With memoization each n is computed once → O(n).
The Master Theorem (divide and conquer)
For recurrences of the form T(n) = a·T(n/b) + O(nᵈ):
| Case | Condition | Result | Example |
|---|---|---|---|
| 1 | d < log_b a | O(n^(log_b a)) | T(n) = 8T(n/2) + n² → O(n³) |
| 2 | d = log_b a | O(nᵈ log n) | Merge sort: 2T(n/2) + n → O(n log n) |
| 3 | d > log_b a | O(nᵈ) | T(n) = 2T(n/2) + n² → O(n²) |
Quick reference:
- Binary search: T(n) = T(n/2) + O(1) → O(log n)
- Merge sort: T(n) = 2T(n/2) + O(n) → O(n log n)
- Tree traversal: T(n) = 2T(n/2) + O(1) → O(n)
Space complexity
Count the extra memory your algorithm uses as a function of n:
- Variables and pointers: O(1)
- A new array/hash map of size n: O(n)
- A 2D DP table: O(n·m)
- Recursion stack: O(depth). A DFS on a skewed tree is O(n) stack space, a balanced one O(log n).
Best, average, worst case
| Algorithm | Best | Average | Worst |
|---|---|---|---|
| Linear search | O(1) | O(n) | O(n) |
| Binary search | O(1) | O(log n) | O(log n) |
| Quicksort | O(n log n) | O(n log n) | O(n²) |
| Hash table lookup | O(1) | O(1) | O(n) (all collide) |
| Insertion sort | O(n) (sorted) | O(n²) | O(n²) |
Worked example: three solutions, three complexities
Problem: does an array contain any duplicate?
# O(n²) time, O(1) space — compare every pair
def has_dup_brute(nums):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] == nums[j]:
return True
return False
# O(n log n) time, O(1) extra (in-place sort) — duplicates become adjacent
def has_dup_sort(nums):
nums.sort()
return any(nums[i] == nums[i - 1] for i in range(1, len(nums)))
# O(n) time, O(n) space — trade memory for speed
def has_dup_hash(nums):
seen = set()
for x in nums:
if x in seen:
return True
seen.add(x)
return False
// O(n) expected time, O(n) space
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> seen;
for (int x : nums) {
if (seen.count(x)) return true;
seen.insert(x);
}
return false;
}
This time–space trade-off (spend memory on a hash set or table to save time) is the single most common optimization in interview problems.
Hidden costs to watch for
- String concatenation in a loop in Python/Java creates a new string each time → O(n²). Use a list and
''.join()/StringBuilder. - Slicing
arr[1:]copies → O(n) per call. Recursion that slices is often secretly O(n²). Pass indices instead. x in listis O(n);x in setis O(1) average.list.pop(0)andlist.insert(0, x)are O(n). Usecollections.deque.- Sorting inside a loop multiplies by O(n log n).
- C++
map/setare O(log n) balanced trees;unordered_mapis O(1) average.
Summary
- Big-O = growth rate; drop constants and lower-order terms.
- Read the constraints first — they tell you the target complexity.
- Nested loops multiply only if the inner loop restarts; pointers that only move forward are amortized O(n).
- Recursion: calls × work per call; divide & conquer: Master Theorem.
- Always state both time and space, and mention the recursion stack.
Practice problems
Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.
Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.