Algorithmic Complexity Analysis (Big O)
When to Use
- Asked to state or derive the time/space complexity ("what's the Big O of this function") of a code snippet or algorithm
- Reviewing code for hidden quadratic behavior:
in on a list inside a loop, += string concatenation, sorted() inside a loop
- Choosing between data structures/algorithms for scale (list vs set membership, sort vs heap)
- Deriving recursive complexity via a recurrence relation, e.g.
T(n) = 2T(n/2) + n (Master theorem)
- Explaining best/average/worst-case or amortized cost (
list.append, hash table lookup, quicksort pivot choice)
Version Compatibility
- Language-agnostic technique; example code uses Python ≥3.8 stdlib only (
math, no third-party dependencies)
Prerequisites
- Comfortable reading Python loops, function calls, and recursion
- No packages to install
Complexity Classes (fastest to slowest)
| Class |
Name |
Example |
| O(1) |
Constant |
Dict lookup, array index |
| O(log n) |
Logarithmic |
Binary search |
| O(n) |
Linear |
Linear scan, single pass |
| O(n log n) |
Linearithmic |
Merge sort, heap sort |
| O(n²) |
Quadratic |
Nested loops, bubble sort |
| O(2ⁿ) |
Exponential |
Naive recursive Fibonacci, subset enumeration |
| O(n!) |
Factorial |
Permutation enumeration, brute-force TSP |
Simplification Rules
O(2n + 5) → O(n) # drop constants
O(n² + n) → O(n²) # drop lower-order terms
O(500) → O(1)
O(n² + n³) → O(n³)
O(A) then O(B) → O(A + B) # sequential loops add
O(A) inside O(B) → O(A × B) # nested loops multiply
Goal: classify the complexity of straight-line and loop-based code.
Approach: identify the loop structure (single / nested / sequential / halving), express operation count as a function of n, then apply the simplification rules above.
def get_first_element(arr):
"""Return the first element. O(1) time, O(1) space.
Direct index access: base_address + index * element_size,
independent of array length.
"""
return arr[0] if arr else None
def binary_search(arr, target):
"""Find target in a sorted array. O(log n) time, O(1) space.
Each iteration halves the search space: after k steps the
remaining size is n / 2**k, so it terminates in ~log2(n) steps.
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2 # O(1)
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # discard left half
else:
right = mid - 1 # discard right half
return -1
def has_duplicate_naive(arr):
"""O(n²) time, O(1) space: compares every pair of elements."""
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] == arr[j]:
return True
return False
def has_duplicate_fast(arr):
"""O(n) time, O(n) space: trades memory for speed via a set."""
seen = set()
for x in arr:
if x in seen: # O(1) average membership test
return True
seen.add(x)
return False
Goal: derive complexity of recursive functions.
Approach: write the recurrence relation T(n) = a*T(n/b) + f(n) (Master theorem form), then match it to a known pattern.
def merge_sort(arr):
"""Sort via divide-and-conquer. O(n log n) time, O(n) space.
Recurrence: T(n) = 2*T(n/2) + O(n) -> O(n log n)
(log n levels of recursion, O(n) merge work per level)
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return _merge(left, right)
def _merge(left, right):
"""Merge two sorted lists. O(n) time, O(n) space."""
result, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def fibonacci_naive(n):
"""Naive recursive Fibonacci. O(2^n) time, O(n) space (call stack).
Recurrence: T(n) = T(n-1) + T(n-2) + O(1) -> O(2^n)
Each call branches into two more calls, forming a binary call tree.
"""
if n <= 1:
return n
return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)
| Pattern |
Recurrence |
Solution |
| Linear recursion |
T(n) = T(n-1) + 1 |
O(n) |
| Binary recursion (no reuse) |
T(n) = 2T(n-1) + 1 |
O(2ⁿ) |
| Divide & conquer (merge sort) |
T(n) = 2T(n/2) + n |
O(n log n) |
| Binary search |
T(n) = T(n/2) + 1 |
O(log n) |
At Scale (n = 1,000,000)
| Complexity |
Operations |
Feasible? |
| O(1) |
1 |
Yes |
| O(log n) |
~20 |
Yes |
| O(n) |
1,000,000 |
Yes |
| O(n log n) |
~20,000,000 |
Yes |
| O(n²) |
10¹² |
No |
| O(2ⁿ) |
∞ |
Never |
Best / Average / Worst
| Algorithm |
Best |
Average |
Worst |
| Binary search |
O(1) |
O(log n) |
O(log n) |
| Quicksort |
O(n log n) |
O(n log n) |
O(n²) |
| Merge sort |
O(n log n) |
O(n log n) |
O(n log n) |
| Hash table lookup |
O(1) |
O(1) |
O(n) |
| BFS/DFS |
O(V+E) |
O(V+E) |
O(V+E) |
Amortized Complexity
- Python
list.append(): O(1) amortized (occasional O(n) resize when capacity doubles, but rare)
- Python
dict/set lookup: O(1) average; O(n) worst case (pathological hash collisions — rare with good hashing)
Pitfalls
- Hidden O(n) inside a loop:
x in some_list is O(n); inside an O(n) loop that's O(n²). Use a set for O(1) membership (see has_duplicate_fast above).
- String concatenation:
s += x in a loop reallocates and copies each time → O(n²) total. Use ''.join(parts).
sorted() is O(n log n): calling it inside a loop makes the loop O(n² log n) — sort once outside the loop.
- Recursion depth: unbounded recursion on large
n hits Python's default ~1000-frame limit. Prefer an iterative rewrite over raising sys.setrecursionlimit.
- Space vs time trade-off: memoization/hash sets trade O(n) space for O(n²) → O(n) or O(1) repeated lookups.
- Worst-case vs average-case: quicksort degrades to O(n²) on already-sorted input with a naive pivot; use random/median-of-three pivot selection or Python's built-in Timsort (
sorted()/list.sort()), which is O(n log n) worst case.
See Also
algo-basic-algorithms — foundational search/sort algorithms referenced above
algo-linear-binary-search — linear vs binary search trade-offs in depth
algo-comparison-sorts — merge sort, quicksort, heapsort implementations
algo-intro-memoization — turning exponential recursion into polynomial time
1---2name: algo-complexity-analysis3description: Derive Big O time/space complexity of loops and recursion via recurrence relations; simplify expressions, compare growth at scale. Use when asked the complexity of code, hunting O(n^2) loops, or worst-case cost.4---56# Algorithmic Complexity Analysis (Big O)78## When to Use9- Asked to state or derive the time/space complexity ("what's the Big O of this function") of a code snippet or algorithm10- Reviewing code for hidden quadratic behavior: `in` on a list inside a loop, `+=` string concatenation, `sorted()` inside a loop11- Choosing between data structures/algorithms for scale (list vs set membership, sort vs heap)12- Deriving recursive complexity via a recurrence relation, e.g. `T(n) = 2T(n/2) + n` (Master theorem)13- Explaining best/average/worst-case or amortized cost (`list.append`, hash table lookup, quicksort pivot choice)1415## Version Compatibility16- Language-agnostic technique; example code uses Python ≥3.8 stdlib only (`math`, no third-party dependencies)1718## Prerequisites19- Comfortable reading Python loops, function calls, and recursion20- No packages to install2122## Complexity Classes (fastest to slowest)2324| Class | Name | Example |25|-------|------|---------|26| O(1) | Constant | Dict lookup, array index |27| O(log n) | Logarithmic | Binary search |28| O(n) | Linear | Linear scan, single pass |29| O(n log n) | Linearithmic | Merge sort, heap sort |30| O(n²) | Quadratic | Nested loops, bubble sort |31| O(2ⁿ) | Exponential | Naive recursive Fibonacci, subset enumeration |32| O(n!) | Factorial | Permutation enumeration, brute-force TSP |3334## Simplification Rules3536```text37O(2n + 5) → O(n) # drop constants38O(n² + n) → O(n²) # drop lower-order terms39O(500) → O(1)40O(n² + n³) → O(n³)41O(A) then O(B) → O(A + B) # sequential loops add42O(A) inside O(B) → O(A × B) # nested loops multiply43```4445**Goal:** classify the complexity of straight-line and loop-based code.46**Approach:** identify the loop structure (single / nested / sequential / halving), express operation count as a function of `n`, then apply the simplification rules above.4748```python49def get_first_element(arr):50 """Return the first element. O(1) time, O(1) space.5152 Direct index access: base_address + index * element_size,53 independent of array length.54 """55 return arr[0] if arr else None565758def binary_search(arr, target):59 """Find target in a sorted array. O(log n) time, O(1) space.6061 Each iteration halves the search space: after k steps the62 remaining size is n / 2**k, so it terminates in ~log2(n) steps.63 """64 left, right = 0, len(arr) - 165 while left <= right:66 mid = (left + right) // 2 # O(1)67 if arr[mid] == target:68 return mid69 elif arr[mid] < target:70 left = mid + 1 # discard left half71 else:72 right = mid - 1 # discard right half73 return -1747576def has_duplicate_naive(arr):77 """O(n²) time, O(1) space: compares every pair of elements."""78 n = len(arr)79 for i in range(n):80 for j in range(i + 1, n):81 if arr[i] == arr[j]:82 return True83 return False848586def has_duplicate_fast(arr):87 """O(n) time, O(n) space: trades memory for speed via a set."""88 seen = set()89 for x in arr:90 if x in seen: # O(1) average membership test91 return True92 seen.add(x)93 return False94```9596**Goal:** derive complexity of recursive functions.97**Approach:** write the recurrence relation `T(n) = a*T(n/b) + f(n)` (Master theorem form), then match it to a known pattern.9899```python100def merge_sort(arr):101 """Sort via divide-and-conquer. O(n log n) time, O(n) space.102103 Recurrence: T(n) = 2*T(n/2) + O(n) -> O(n log n)104 (log n levels of recursion, O(n) merge work per level)105 """106 if len(arr) <= 1:107 return arr108 mid = len(arr) // 2109 left = merge_sort(arr[:mid])110 right = merge_sort(arr[mid:])111 return _merge(left, right)112113114def _merge(left, right):115 """Merge two sorted lists. O(n) time, O(n) space."""116 result, i, j = [], 0, 0117 while i < len(left) and j < len(right):118 if left[i] <= right[j]:119 result.append(left[i]); i += 1120 else:121 result.append(right[j]); j += 1122 result.extend(left[i:])123 result.extend(right[j:])124 return result125126127def fibonacci_naive(n):128 """Naive recursive Fibonacci. O(2^n) time, O(n) space (call stack).129130 Recurrence: T(n) = T(n-1) + T(n-2) + O(1) -> O(2^n)131 Each call branches into two more calls, forming a binary call tree.132 """133 if n <= 1:134 return n135 return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)136```137138| Pattern | Recurrence | Solution |139|---------|------------|----------|140| Linear recursion | T(n) = T(n-1) + 1 | O(n) |141| Binary recursion (no reuse) | T(n) = 2T(n-1) + 1 | O(2ⁿ) |142| Divide & conquer (merge sort) | T(n) = 2T(n/2) + n | O(n log n) |143| Binary search | T(n) = T(n/2) + 1 | O(log n) |144145## At Scale (n = 1,000,000)146147| Complexity | Operations | Feasible? |148|------------|-----------|-----------|149| O(1) | 1 | Yes |150| O(log n) | ~20 | Yes |151| O(n) | 1,000,000 | Yes |152| O(n log n) | ~20,000,000 | Yes |153| O(n²) | 10¹² | No |154| O(2ⁿ) | ∞ | Never |155156## Best / Average / Worst157158| Algorithm | Best | Average | Worst |159|-----------|------|---------|-------|160| Binary search | O(1) | O(log n) | O(log n) |161| Quicksort | O(n log n) | O(n log n) | O(n²) |162| Merge sort | O(n log n) | O(n log n) | O(n log n) |163| Hash table lookup | O(1) | O(1) | O(n) |164| BFS/DFS | O(V+E) | O(V+E) | O(V+E) |165166## Amortized Complexity167168- Python `list.append()`: O(1) amortized (occasional O(n) resize when capacity doubles, but rare)169- Python `dict`/`set` lookup: O(1) average; O(n) worst case (pathological hash collisions — rare with good hashing)170171## Pitfalls172173- **Hidden O(n) inside a loop**: `x in some_list` is O(n); inside an O(n) loop that's O(n²). Use a `set` for O(1) membership (see `has_duplicate_fast` above).174- **String concatenation**: `s += x` in a loop reallocates and copies each time → O(n²) total. Use `''.join(parts)`.175- **`sorted()` is O(n log n)**: calling it inside a loop makes the loop O(n² log n) — sort once outside the loop.176- **Recursion depth**: unbounded recursion on large `n` hits Python's default ~1000-frame limit. Prefer an iterative rewrite over raising `sys.setrecursionlimit`.177- **Space vs time trade-off**: memoization/hash sets trade O(n) space for O(n²) → O(n) or O(1) repeated lookups.178- **Worst-case vs average-case**: quicksort degrades to O(n²) on already-sorted input with a naive pivot; use random/median-of-three pivot selection or Python's built-in Timsort (`sorted()`/`list.sort()`), which is O(n log n) worst case.179180## See Also181- `algo-basic-algorithms` — foundational search/sort algorithms referenced above182- `algo-linear-binary-search` — linear vs binary search trade-offs in depth183- `algo-comparison-sorts` — merge sort, quicksort, heapsort implementations184- `algo-intro-memoization` — turning exponential recursion into polynomial time