Skip to main content

Big-O Notation & Complexity Analysis: The Ultimate Guide

A single, authoritative reference bridging classical algorithm analysis with modern AI/ML/LLM systems. Written for practitioners who need both the formal rigor and the production-grade intuition.


1. Foundations & Notation​

1.1 What is Big-O?​

Big-O notation describes the asymptotic upper bound on the growth rate of a function — typically the running time or memory usage of an algorithm — as the input size n approaches infinity. It answers one question: "As my input grows without bound, how does the cost grow?"

Formal definition:

f(n) = O(g(n)) if and only if there exist positive constants c and n₀ such that 0 ≤ f(n) ≤ c · g(n) for all n ≥ n₀.

In plain terms: beyond some input size n₀, f(n) is bounded above by a constant multiple of g(n). Big-O deliberately discards constants and lower-order terms because they become irrelevant at scale.

3n² + 200n + 5000   →   O(n²)

  • The n² term dominates as n → ∞.
  • Constants (3, 200, 5000) are ignored — they don't change the shape of growth.

Key mental model: Big-O is about scalability, not speed on a specific machine. An O(n²) algorithm may beat an O(n log n) one for small n (smaller constants), but the O(n log n) one always wins eventually.

📌 Analogy — Big-O: Think of Big-O as describing the shape of a hill, not the speed of the runner. Two runners on the same hill shape will both slow down proportionally as the hill gets steeper — the hill shape (the growth class) determines the long-run outcome, regardless of who is momentarily faster.

Chain of thought — multi-level lens:

  • Beginner: Big-O = "how does runtime grow when data grows?"
  • Senior engineer: Big-O = the tool for reasoning about which algorithm survives a 100× data increase without falling over.
  • DS/AI/ML relevance: Every training loop, every inference pass, every vector search is a function of dataset size, sequence length, or dimensionality — Big-O tells you which one becomes the bottleneck first.
  • Common misconception: "Big-O tells me my program is fast." No — it tells you how it scales. Constants and hardware still matter for real latency.

1.2 Big-O vs Big-Θ vs Big-Ω​

These three notations bound a function from different directions. Confusing them is the single most common formal error in interviews.

NotationNameBoundsMeaningIntuition
O(g(n))Big-OUpperf grows no faster than g"At most this bad"
Ω(g(n))Big-OmegaLowerf grows at least as fast as g"At least this costly"
Θ(g(n))Big-ThetaTightf grows exactly like g"Precisely this rate"

Formal definitions:

  • Big-Ω (lower bound): f(n) = Ω(g(n)) iff ∃ c > 0, n₀ such that f(n) ≥ c · g(n) for all n ≥ n₀.
  • Big-Θ (tight bound): f(n) = Θ(g(n)) iff f(n) = O(g(n)) and f(n) = Ω(g(n)). This means c₁ · g(n) ≤ f(n) ≤ c₂ · g(n).

Worked example — Merge Sort:

  • Best case: Θ(n log n)
  • Worst case: Θ(n log n)
  • Because best and worst coincide, we can say Merge Sort is Θ(n log n) — a tight, precise characterization.

Worked example — Insertion Sort:

  • Best case (already sorted): Θ(n) — one pass, no shifts.
  • Worst case (reverse sorted): Θ(n²).
  • We say Insertion Sort is O(n²) (upper bound) and Ω(n) (lower bound), but there is no single Θ across all inputs.

💡 Expert Insight: In practice, engineers say "Big-O" but usually mean Big-Θ (the tight bound). Saying "Merge Sort is O(n²)" is technically true (it's a valid, if loose, upper bound) but misleading. Precision matters: in a design review, state the tight bound and the case it applies to.

📌 Analogy — the three bounds: Estimating a commute. Big-O is "it'll take at most 60 min" (worst traffic). Big-Ω is "it'll take at least 20 min" (empty roads). Big-Θ is "it reliably takes about 35 min ± a bit" — a tight, dependable estimate.

1.3 Why Complexity Matters​

Worst-case, best-case, average-case analysis:

  • Worst-case (most common in practice): The maximum cost over all inputs of size n. This is what you design for in production — it bounds your SLA. "What's the worst that can happen?"
  • Best-case: The minimum cost. Rarely useful alone; can be misleading (a bogus algorithm can have a great best case).
  • Average-case: Expected cost over a probability distribution of inputs. Powerful but requires assumptions about input distribution (often uniform randomness), which may not hold in the real world.

Why it matters — concrete stakes:

  • At n = 1,000,000, an O(n²) algorithm does ~10¹² operations (~minutes to hours); an O(n log n) algorithm does ~2×10⁷ (~milliseconds). That's the difference between a viable product and a timeout.
  • In ML, the "input size" is often dataset size, feature dimensionality, or sequence length — and these routinely reach millions or billions. Complexity errors here mean unshippable models or ruinous cloud bills.

💡 Expert Insight: Always ask "what is n?" before quoting complexity. In a transformer, complexity in sequence length (O(n²)), model width (O(d²)), and dataset size (O(N)) are three different axes with three different scaling behaviors — conflating them is a classic senior-level mistake.


2. Complexity Classes​

The hierarchy below runs from fastest-growing-is-best (O(1)) to catastrophic (O(n!)).

Visual ASCII Growth-Rate Table​

Operations required as n grows (rounded / order-of-magnitude):

 n        O(1)   O(log n)   O(n)      O(n log n)   O(n²)        O(2ⁿ)              O(n!)
-----------------------------------------------------------------------------------------------
1 1 0 1 0 1 2 1
10 1 3 10 33 100 1,024 3,628,800
100 1 7 100 664 10,000 1.3×10³⁰ 9.3×10¹⁵⁷
1,000 1 10 1,000 9,966 10⁶ ~10³⁰¹ (astronomical)
10,000 1 13 10,000 132,877 10⁸ (overflow) (overflow)
1,000,000 1 20 10⁶ ~2×10⁷ 10¹² (overflow) (overflow)

Growth shape, visualized:

cost
^ O(n!) O(2ⁿ)
| * *
| * *
| * * O(n²)
| * * .
| * * .
| * * . O(n log n)
| * * . - - - - - -
| * * . - - - O(n)
| * * . - - - _ _ _ _ _ _ _ _ _ _
| * * . - - _ _ _ _ O(log n)
| * * . - - _ _ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| * . - _ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ O(1)
+--------------------------------------------------------------------> n

Rule of thumb — what's tractable:

ClassFeasible input size (roughly, ~1 sec)Verdict
O(1), O(log n)Effectively unlimited✅ Ideal
O(n), O(n log n)10⁷–10⁸✅ Excellent, the practical sweet spot
O(n²)~10⁴⚠️ Fine for small n, dangerous at scale
O(n³)~few hundred⚠️ Watch out
O(2ⁿ)~20–25❌ Only tiny inputs
O(n!)~10–12❌ Brute-force only

2.1 O(1) — Constant​

Definition: Runtime is independent of input size. The same number of operations regardless of whether n is 10 or 10 billion.

# O(1) - Constant time
# WHY: Dictionary hash lookup and arithmetic take the same time
# no matter how large the structures are.
def get_first_element(arr):
return arr[0] # single index access — O(1)

def hash_lookup(d, key):
return d.get(key) # average-case hash lookup — O(1)

def is_even(n):
return n % 2 == 0 # single arithmetic op — O(1)

📌 Analogy — O(1): Grabbing a book off a shelf when you already know the exact slot number. It doesn't matter if the library has 100 books or 100 million — you walk straight to the slot.

📌 DS/ML Analogy — O(1): Looking up a token's embedding vector in an embedding table. Whether the vocabulary is 30K or 300K tokens, embedding_matrix[token_id] is a single indexed memory read.

💡 Expert Insight: Beware "hidden O(1) that isn't." Python's list.append is amortized O(1) but individual appends can trigger O(n) reallocation (see §4). And hash lookups are O(1) average but O(n) worst-case under adversarial collisions (see §2.3 edge case). In interviews, always state "amortized" or "average" when it applies.


2.2 O(log n) — Logarithmic​

Definition: Runtime grows logarithmically — each step eliminates a constant fraction (usually half) of the remaining input. Doubling n adds only one more step.

# O(log n) - Binary Search
# WHY: Search space halves at each step → log₂(n) steps maximum
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # discard the left half
else:
right = mid - 1 # discard the right half
return -1

Why it's O(log n): Start with n candidates → n/2 → n/4 → ... → 1. The number of halvings to reach 1 is log₂(n).

📌 Analogy — O(log n): Finding a word in a dictionary. You don't read every word; you open to the middle, decide which half to keep, and repeat. Each step eliminates half the remaining options.

📌 DS/ML Analogy — O(log n): Traversing a balanced decision tree (or a single tree in a gradient-boosted ensemble like XGBoost) at inference. A tree of depth d handles 2^d leaf regions, so classifying one sample costs O(d) = O(log(#leaves)).

💡 Expert Insight: O(log n) is nearly as good as O(1) in practice — log₂(10⁹) ≈ 30. Prerequisite trap: binary search requires sorted input. If you sort first (O(n log n)) just to search once, you've made it worse than a linear scan. Log-time wins only when the sorted structure is reused across many queries (e.g., a database B-tree index).


2.3 O(n) — Linear​

Definition: Runtime grows proportionally with input size. Touch each element a constant number of times.

# O(n) - Linear Search / single pass
# WHY: In the worst case we inspect every one of the n elements once.
def linear_search(arr, target):
for i, value in enumerate(arr): # n iterations
if value == target:
return i
return -1

def sum_list(arr):
total = 0
for x in arr: # exactly n additions
total += x
return total

📌 Analogy — O(n): Reading every name on a guest list to find one person, one line at a time. Twice the guests → twice the reading time.

📌 DS/ML Analogy — O(n): A single forward pass over n training examples to compute a full-batch loss, or one epoch of streaming data through a data loader. Each sample is visited once.

💡 Expert Insight — the hash-collision edge case: Hash map operations are O(1) average, but if many keys hash to the same bucket (adversarial input, or a bad hash function), lookups degrade to O(n) as the bucket becomes a linear chain. This is a real attack vector (HashDoS) — worst-case O(n) per lookup can turn an O(n) algorithm into O(n²). Production systems mitigate with randomized hashing (Python's PYTHONHASHSEED) or balanced-tree fallbacks (Java 8+ HashMap converts long chains to red-black trees, giving O(log n) worst case).


2.4 O(n log n) — Linearithmic​

Definition: n work done log n times (or vice versa). This is the provable lower bound for comparison-based sorting — you cannot sort by comparisons faster than Ω(n log n).

# O(n log n) - Merge Sort
# WHY: log n levels of recursion (halving), and each level does
# O(n) total work to merge → n * log n.
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # T(n/2)
right = merge_sort(arr[mid:]) # T(n/2)
return merge(left, right) # O(n) merge

def merge(left, right):
result, i, j = [], 0, 0
while i < len(left) and j < len(right): # O(n) total across level
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

Recurrence: T(n) = 2T(n/2) + O(n) → by the Master Theorem, T(n) = Θ(n log n).

📌 Analogy — O(n log n): Organizing a huge deck of cards by repeatedly splitting into piles, sorting each pile, and merging back. The splitting depth is log n; each merge round touches all n cards.

📌 DS/ML Analogy — O(n log n): Building a k-d tree or ball tree to accelerate nearest-neighbor search costs O(n log n). You pay this once to convert brute-force O(n) queries into O(log n) queries — the classic preprocessing-vs-query trade-off.

💡 Expert Insight: O(n log n) is the practical ceiling for "efficient." Python's built-in sorted() / list.sort() uses Tim Sort — an adaptive hybrid of merge sort and insertion sort that runs in O(n) on already-sorted or nearly-sorted runs and O(n log n) worst-case. It exploits real-world data's partial ordering. Never hand-roll a sort in production; the standard library's is battle-tested and cache-optimized.


2.5 O(n²) — Quadratic​

Definition: Runtime grows with the square of input size — typically nested loops each running n times.

# O(n²) - Bubble Sort (nested iteration)
# WHY: Outer loop runs n times; inner loop runs up to n times each
# → n * n comparisons in the worst case.
def bubble_sort(arr):
n = len(arr)
for i in range(n): # n iterations
for j in range(0, n - i - 1): # up to n iterations
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr

# O(n²) - All pairwise comparisons (e.g., naive duplicate detection)
def has_duplicate_pairs(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)): # every pair once
if arr[i] == arr[j]:
return True
return False

📌 Analogy — O(n²): A handshake at a party where everyone shakes hands with everyone else. With n people there are ~n²/2 handshakes — the room's total effort explodes as guests arrive.

📌 DS/ML Analogy — O(n²): Computing a full pairwise distance/similarity matrix for n points (e.g., the kernel matrix in an SVM, or a Gram matrix). Every pair must be compared — and this is exactly the structural reason transformer self-attention is O(n²) (§6.1).

💡 Expert Insight: O(n²) is the classic "works in the demo, dies in production" trap. It's fine at n = 1,000 (10⁶ ops) but fatal at n = 1,000,000 (10¹² ops). Interview pitfall: candidates write nested loops without realizing they've built O(n²); the fix is often a hash set (trade space for time → O(n)) or sorting first (O(n log n)).


2.6 O(2ⁿ) — Exponential​

Definition: Runtime doubles with each additional input element. Characteristic of naive recursive branching that re-solves overlapping subproblems.

# O(2ⁿ) - Naive recursive Fibonacci
# WHY: Each call spawns TWO more calls; the recursion tree has
# ~2ⁿ nodes because subproblems are recomputed exponentially.
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2) # two branches per call

# Fix with memoization → O(n): each subproblem solved once.
def fib_memo(n, cache=None):
if cache is None:
cache = {}
if n <= 1:
return n
if n not in cache:
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]

📌 Analogy — O(2ⁿ): A rumor where each person tells two new people, who each tell two more. The number of people "informed" doubles each round — it engulfs a town shockingly fast.

📌 DS/ML Analogy — O(2ⁿ): Exhaustive feature subset selection — evaluating every possible combination of n features means 2ⁿ model fits. This is why we use greedy/regularized selection (L1/LASSO) or importance-based methods instead of brute force.

💡 Expert Insight: Exponential blowup is almost always a signal to reach for dynamic programming (cache overlapping subproblems) or pruning (branch-and-bound). The Fibonacci example above collapses from O(2ⁿ) to O(n) with a single dictionary. Recognizing overlapping subproblems is the core DP interview skill.


2.7 O(n!) — Factorial​

Definition: Runtime grows factorially — you enumerate every permutation/ordering of the input. The fastest-exploding class you'll routinely encounter.

# O(n!) - Brute-force Traveling Salesman Problem
# WHY: We evaluate every permutation of n cities → n! orderings.
from itertools import permutations

def tsp_bruteforce(cities, dist):
best_cost, best_route = float('inf'), None
for perm in permutations(cities): # n! permutations
cost = sum(dist[perm[i]][perm[i + 1]] for i in range(len(perm) - 1))
if cost < best_cost:
best_cost, best_route = cost, perm
return best_route, best_cost

📌 Analogy — O(n!): Trying to find the best seating arrangement for a dinner party by physically testing every possible order of guests around the table. Even 15 guests yield over a trillion arrangements.

📌 DS/ML Analogy — O(n!): Optimal ordering problems like finding the single best sequence in which to present training curricula, or exact optimal join ordering in query planners — both are permutation searches, which is why real systems use heuristics, approximation, or learned policies instead.

💡 Expert Insight: O(n!) (and O(2ⁿ)) problems are often NP-hard. The professional response is not "make the brute force faster" but "change the goal": use approximation algorithms (e.g., Christofides for TSP, ~1.5× optimal), heuristics (nearest-neighbor, 2-opt), or metaheuristics (simulated annealing, genetic algorithms). Knowing when to stop seeking exact optimality is a hallmark of senior judgment.


3. Space Complexity​

Definition: Space complexity measures the total memory an algorithm needs as a function of input size n, including:

  • Input space — the memory for the input itself (often excluded from analysis).
  • Auxiliary space — the extra memory the algorithm allocates beyond the input. This is usually what we mean when we say "the space complexity."
  • Total space = input + auxiliary.

⚠️ Precision note: "Space complexity O(1)" almost always means **auxiliary space **O(1) — the input itself still occupies O(n). State which you mean.

3.1 Time vs Space Trade-offs​

You can very often buy time with space (and vice versa). This is one of the most powerful levers in all of computing.

TechniqueSpendsSavesExample
Memoization / cachingSpace O(n)Time (exponential → linear)fib_memo above
Hash set for lookupsSpace O(n)Time (O(n²) → O(n))Duplicate detection
Precomputed index (B-tree, k-d tree)Space O(n)Query time (O(n) → O(log n))Database index
In-place algorithmTime (sometimes)Space (O(n) → O(1))In-place quicksort partition
Recomputation (gradient checkpointing)Time (extra forward passes)Space (activations)Training huge LLMs
# Time-space trade-off in action:

# O(n²) time, O(1) auxiliary space — check duplicates by comparing all pairs
def has_dup_slow(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
return True
return False

# O(n) time, O(n) auxiliary space — trade memory for speed with a hash set
def has_dup_fast(arr):
seen = set() # O(n) extra memory
for x in arr:
if x in seen: # O(1) average lookup
return True
seen.add(x)
return False

💡 Expert Insight: There is no universally "right" point on the time-space curve — it depends on the binding constraint. On a memory-starved edge device, you recompute to save RAM. In a latency-critical API, you cache aggressively. In LLM training, gradient checkpointing deliberately recomputes activations during backprop to fit larger models in GPU memory — trading ~30% more compute for a large memory reduction.

3.2 Recursion & Stack Space​

Every recursive call pushes a stack frame (local variables, return address). The maximum stack depth determines the space cost — even if the algorithm allocates no heap memory.

# O(n) stack space — linear recursion depth
# WHY: The call stack grows to depth n before any call returns.
def sum_recursive(arr, i=0):
if i == len(arr):
return 0
return arr[i] + sum_recursive(arr, i + 1) # n nested frames

# O(1) stack space — the same logic, iterative (no recursion)
def sum_iterative(arr):
total = 0
for x in arr:
total += x
return total

Recursion space by structure:

  • Linear recursion (one call per frame): O(n) stack.
  • Balanced binary recursion (e.g., merge sort, balanced-tree traversal): O(log n) stack depth (the tree height), even though total work is higher.
  • Tail recursion: In languages with tail-call optimization (TCO), can be O(1). Python does NOT optimize tail calls and has a default recursion limit (~1000) — deep recursion raises RecursionError. Convert to iteration or an explicit stack for large inputs.

📌 Analogy — recursion stack: A stack of sticky notes. Each recursive call adds a note reminding you "come back and finish this." A deep recursion means a tall, wobbly stack — knock past the ceiling (recursion limit) and it topples (stack overflow).

💡 Expert Insight: Two functions with identical O(n log n) time can have different space: merge sort needs O(n) auxiliary space for merging, while heapsort sorts in O(1) auxiliary space. When memory is tight, the space profile — not the time — is the deciding factor between two equally fast algorithms.

3.3 ML Memory Considerations​

Memory is the dominant constraint in modern deep learning. GPU/TPU RAM (e.g., 40–80 GB on an A100/H100) is the hard wall that dictates model and batch size.

Where the memory goes during training:

ComponentScales withNotes
Model parametersO(P)P = parameter count. FP32 = 4 bytes/param; a 7B model ≈ 28 GB just for weights.
GradientsO(P)One gradient per parameter — another ×1 of the weights.
Optimizer statesO(P) (×2 for Adam)Adam stores momentum + variance → 2× params. This is why Adam training needs ~4× the model size in memory (weights + grads + 2 moments).
ActivationsO(batch × seq_len × d × layers)Stored for backprop. Often the largest term for long sequences; the target of gradient checkpointing.
Attention scoresO(batch × heads × n²)The n² sequence-length blowup (§6.1).

Practical formula (rough, Adam, FP16 mixed precision):

Training memory ≈ P × (2 weights + 2 grads + 4+4 optimizer + 4 fp32 master)  ≈  16 × P bytes
→ a 7B-parameter model needs ~112 GB just for optimizer/weights, before activations.

Levers to fit models in memory:

  • Batch size — linear knob on activation memory; reduce it (or use gradient accumulation to simulate large batches with small memory).
  • Mixed precision (FP16/BF16) — halves weight/activation memory vs FP32.
  • Quantization (INT8/INT4) — for inference, cut weight memory 4–8×.
  • Gradient checkpointing — recompute activations instead of storing them (time↔space trade-off).
  • Sharding (ZeRO / FSDP) — split parameters, gradients, and optimizer states across GPUs.

💡 Expert Insight: In inference, the KV cache is the sneaky memory hog: it stores past keys/values for every token generated, scaling as O(batch × seq_len × layers × d). For long-context LLMs, the KV cache can exceed the model weights themselves — which is why techniques like multi-query attention (MQA), grouped-query attention (GQA), and PagedAttention (vLLM) exist specifically to shrink it.


4. Amortized Analysis​

Definition: Amortized analysis measures the cost of an operation averaged over a sequence of operations, guaranteeing the average even when individual operations occasionally cost much more. It's not probabilistic (unlike average-case) — it's a worst-case guarantee across a sequence.

Three methods: aggregate (total cost ÷ number of ops), accounting (prepay "credits"), and potential (a potential function tracks stored work). Aggregate is the most intuitive.

Dynamic Arrays (Python list)​

Appending to a dynamic array is **amortized **O(1), even though some appends trigger an O(n) resize.

# Amortized O(1) append
# WHY: When the array is full, Python allocates a LARGER buffer
# (typically ~1.125–2× growth) and copies all n elements — an
# O(n) event. But that cost is spread across the many cheap
# appends before it, averaging to O(1) per append.
lst = []
for i in range(1_000_000):
lst.append(i) # occasionally O(n) to resize; O(1) amortized

The aggregate argument: Starting empty and doing n appends with doubling, the resizes cost 1 + 2 + 4 + ... + n ≈ 2n total copies. Total work across n appends is O(n), so **average per append is **O(1).

📌 Analogy — amortized O(1): Paying rent monthly vs. a big annual insurance bill. Most months are cheap; occasionally a large expense hits. Averaged over the year, your effective monthly cost is smooth and predictable — that average is the amortized cost.

Hash Table Resizing​

Hash maps maintain a low load factor (elements ÷ buckets, e.g., kept below ~0.66). When it's exceeded, the table rehashes all entries into a larger table — an O(n) operation. But because resizes happen geometrically less often as the table grows, insertion stays **amortized **O(1).

💡 Expert Insight: "Amortized O(1)" matters because it lets you reason about throughput over a workload rather than fearing individual worst cases. But watch the tail latency: a single append or insert can stall for O(n) during a resize. In latency-sensitive systems (real-time inference, trading), that occasional O(n) spike may violate a p99 SLA — which is why some systems pre-size structures (dict/list with known capacity) or use incremental-resizing hash maps to smear the cost. Amortized ≠ worst-case-per-op; know which one your SLA cares about.

⚠️ Interview pitfall: Don't confuse amortized (guaranteed average over a sequence, no probability) with average-case (expected over a random input distribution). Dynamic-array append is amortized O(1) with certainty; hash lookup is average-case O(1) assuming a good hash.


5. Algorithm Complexity Reference Table​

Sorting Algorithms​

AlgorithmBestAverageWorstSpace (aux)Stable?Notes
Bubble SortΘ(n)Θ(n²)O(n²)O(1)✅Teaching only; never in production
Insertion SortΘ(n)Θ(n²)O(n²)O(1)✅Great for tiny / nearly-sorted data
Merge SortΘ(n log n)Θ(n log n)O(n log n)O(n)✅Predictable; needs extra memory
Quick SortΘ(n log n)Θ(n log n)O(n²)O(log n)❌Fast in practice; worst case on bad pivots
Heap SortΘ(n log n)Θ(n log n)O(n log n)O(1)❌In-place, guaranteed n log n
Tim SortΘ(n)Θ(n log n)O(n log n)O(n)✅Python/Java default; adaptive hybrid
Counting/Radix SortΘ(n + k)Θ(n + k)O(n + k)O(n + k)✅Non-comparison; beats n log n for bounded integers

Why Quick Sort's worst case is O(n²): if the pivot is always the min/max (e.g., already-sorted input with naive pivot choice), partitions are maximally unbalanced. Randomized or median-of-three pivots make this vanishingly unlikely → expected O(n log n).

Searching & Graph Traversal​

AlgorithmTimeSpacePrecondition
Linear SearchO(n)O(1)None
Binary SearchO(log n)O(1) iterative / O(log n) recursiveSorted input
BFS (Breadth-First Search)O(V + E)O(V)Graph; finds shortest path in unweighted graphs
DFS (Depth-First Search)O(V + E)O(V) (stack/recursion)Graph; topological sort, cycle detection

V = vertices, E = edges. Both BFS/DFS visit every vertex and edge once → O(V + E).

Data Structure Operations (typical)​

StructureAccessSearchInsertDeleteSpaceNotes
Array (dynamic)O(1)O(n)O(1)* amortized end / O(n) middleO(n)O(n)*append amortized
Hash Map—O(1) avg / O(n) worstO(1) avgO(1) avgO(n)Worst case on collisions
Balanced BST (e.g., red-black)O(log n)O(log n)O(log n)O(log n)O(n)Ordered iteration
Binary HeapO(1) peekO(n)O(log n)O(log n)O(n)Priority queue
Graph (adjacency list)—O(V+E) traverseO(1) edgeO(E)O(V+E)Sparse-graph friendly
Graph (adjacency matrix)O(1) edge check—O(1)O(1)O(V²)Dense graphs; edge-existence queries

ML-Specific Operations​

OperationComplexityn / variables meanNotes
Matrix multiplication (m×k · k×p)O(m·k·p)naive; O(n³) for square n×nStrassen O(n^2.807); the workhorse of every neural net layer
Gradient Descent (per step)O(N·d)N samples, d featuresFull-batch; SGD is O(batch·d) per step
k-NN query (brute force)O(N·d) per queryN points, d dimsReduced to ~O(d log N) with k-d/ball trees (low d) or ANN indexes (high d)
k-Means (per iteration)O(N·k·d)k clustersRepeated over iterations until convergence
Decision Tree (training)O(N·d·log N)Sorting features at each split drives the log N
Transformer self-attentionO(n²·d)n = sequence lengthThe infamous quadratic bottleneck (§6.1)
Transformer FFN / linear layersO(n·d²)Dominates when d > n (short sequences, wide models)

6. AI / ML / LLM Complexity Deep Dive​

6.1 Transformer Attention: The O(n²) Problem​

The core operation. Self-attention lets every token attend to every other token. For a sequence of length n with model dimension d, we compute:

Attention(Q, K, V) = softmax( Q · Kᵀ / √d ) · V

  • Q, K, V are each n × d matrices.
  • Q · Kᵀ produces an n × n** attention-score matrix** — this is where the quadratic cost is born.

Complexity breakdown:

  • Computing Q·Kᵀ: O(n² · d) time.
  • Storing the score matrix: O(n²) memory.
  • Softmax + weighted sum with V: another O(n² · d).
  • Total: O(n² · d) time and O(n²) memory in sequence length.

Why this is the bottleneck. Doubling context length (2K → 4K → 8K → 128K tokens) quadruples attention cost and memory. This is the single biggest obstacle to long-context LLMs.

📌 Analogy — O(n²) attention: A meeting where every attendee must have a one-on-one side conversation with every other attendee before deciding anything. Add attendees and the number of required conversations explodes quadratically — the meeting becomes unmanageable.

# Self-attention (illustrative, NumPy) — note the n×n scores matrix
import numpy as np

def self_attention(X, Wq, Wk, Wv):
Q = X @ Wq # (n, d)
K = X @ Wk # (n, d)
V = X @ Wv # (n, d)
scores = Q @ K.T # (n, n) ← O(n²·d) time, O(n²) memory
scores = scores / np.sqrt(Q.shape[-1])
weights = np.exp(scores) # softmax numerator
weights /= weights.sum(axis=-1, keepdims=True)
return weights @ V # (n, d) ← another O(n²·d)

How the field fights the O(n²) wall:

ApproachComplexityIdea
FlashAttentionStill O(n²) compute, but O(n) memoryTiling + not materializing the full score matrix; IO-aware. Doesn't change asymptotics — changes the constant and memory profile dramatically.
Sparse attention (Longformer, BigBird)O(n·√n) or O(n·w)Each token attends only to a local window + a few global tokens.
Linear attention (Performer, Linformer)O(n)Kernel approximation / low-rank projection of the attention matrix.
State-space models (Mamba/S4)O(n)Replace attention with a recurrent/convolutional state-space mechanism.

💡 Expert Insight: FlashAttention is the most important practical lesson in this entire guide: asymptotic complexity is not the whole story. FlashAttention keeps the O(n²) FLOP count but achieves large real-world speedups by being memory-IO-aware — avoiding slow round-trips to GPU HBM. Two O(n²) algorithms can differ by 10× in wall-clock time due to constants, memory access patterns, and hardware. Senior engineers optimize the constant and the memory hierarchy, not just the exponent.

6.2 Training vs Inference Complexity​

These are fundamentally different regimes and are analyzed separately.

Training complexity (per step):

  • Dominated by forward + backward passes over a batch.
  • Roughly O(batch × n² × d) for attention + O(batch × n × d²) for the feed-forward layers, per layer, per step.
  • Backprop ≈ 2× the forward cost (you compute gradients for every operation).
  • Total training cost scales with dataset_size × epochs × per-step-cost — and this is why frontier model training runs cost millions of dollars.

Inference complexity:

  • Prefill (processing the prompt of length n): O(n² · d) — you attend over the whole prompt once.
  • Decoding (generating each new token): with a KV cache, generating token t costs O(t · d) (attend to t cached tokens), so generating m tokens is O(m² · d) total — but without the cache it would be far worse (re-encoding the whole prefix each step).
AspectTrainingInference
Gradients / backprop✅ Yes (2× forward)❌ No
Optimizer state memory✅ O(P)–O(2P)❌ None
Dominant memoryActivations + optimizer stateKV cache + weights
Key knobBatch size, seq lenKV cache size, batch of requests
PrecisionFP16/BF16 + FP32 masterINT8/INT4 quantization common

💡 Expert Insight: The KV cache turns naive O(n)-per-token re-encoding into O(1)-ish incremental work per new token (attending to cached keys/values), at the cost of O(n) growing memory. This is a textbook time-space trade-off applied at inference scale — and its memory growth is why long-context serving is expensive and why GQA/MQA and PagedAttention exist to compress it.

6.3 Scaling Laws & Complexity​

Scaling laws (Kaplan et al. 2020; Chinchilla, Hoffmann et al. 2022) empirically relate model loss to three quantities: parameter count P, dataset size D (tokens), and compute C.

The key complexity relationship:

Compute (FLOPs) ≈ 6 × P × D

  • The factor of 6 ≈ 2 (forward) + 4 (backward) FLOPs per parameter per token.
  • Training compute scales **linearly in both P and **D — but you must scale them together.

Chinchilla-optimal insight: For a fixed compute budget, loss is minimized by scaling parameters and training tokens in roughly equal proportion (~20 tokens per parameter). Earlier models (e.g., GPT-3) were over-parameterized and under-trained; Chinchilla showed a smaller model trained on more data can win at the same compute.

📌 DS/ML Analogy — scaling laws: Baking a bigger cake. It's not enough to add more flour (parameters) — you must scale flour and eggs and baking time (data and compute) proportionally, or the cake collapses. Scaling one axis alone yields diminishing returns.

💡 Expert Insight: Scaling laws recast a systems question as a complexity/economics question. Because compute is ~6PD and loss improves as a power law (not linearly) in compute, each additional increment of capability costs disproportionately more FLOPs. This is why the frontier is defined by compute budgets, why inference-time efficiency (quantization, distillation, MoE sparsity) is now as strategically important as training scale, and why Mixture-of-Experts architectures matter — they raise parameter count P (capacity) while keeping per-token compute low by activating only a top-k subset of experts.


7. Expert Takeaways & Interview Tips​

Best Practices​

  • Always identify n first. "What is the input we're scaling in?" Sequence length, dataset size, feature count, and vocabulary are different axes with different complexities.
  • State the case and the bound tightly. Say "Merge sort is Θ(n log n) worst-case," not a loose "O(n²)."
  • Distinguish amortized, average, and worst-case explicitly — they answer different questions (throughput vs. expected vs. tail latency).
  • Auxiliary vs. total space — be explicit which one your O(...) refers to.
  • Profile before optimizing. Asymptotics guide you to the right algorithm; profilers find the actual hot path. Constants and memory access often dominate at real-world n.

Common Interview Pitfalls​

  • ❌ Quoting Big-O when you mean Big-Θ (loose upper bounds mislead).
  • ❌ Forgetting binary search requires sorted input (and that sorting first may negate the benefit).
  • ❌ Missing hidden costs: string concatenation in a loop (O(n²) in many languages), list.insert(0, x) being O(n), x in list being O(n) vs x in set being O(1).
  • ❌ Ignoring recursion stack space (Python's recursion limit is a real failure mode).
  • ❌ Claiming hash operations are O(1) without the "average-case / good hash" caveat — and missing the collision → O(n) edge case.
  • ❌ Conflating amortized with average-case.

Production Trade-offs​

  • Small n? A simpler O(n²) algorithm with tiny constants can beat a complex O(n log n) one. Insertion sort is used inside Tim Sort for small runs precisely for this reason.
  • Tail latency matters? Amortized O(1) can still spike to O(n) on a resize — pre-size structures or use incremental strategies for p99-sensitive systems.
  • Memory-bound (edge/GPU)? Trade time for space: recompute (gradient checkpointing), quantize, stream data.
  • Latency-bound (serving)? Trade space for time: cache, precompute indexes, keep a KV cache.
  • Big-O picks the algorithm; hardware-awareness (cache lines, GPU HBM, vectorization) wins the last 10×. FlashAttention is the canonical proof.

8. Quick-Reference Cheat Sheet​

Complexity Classes at a Glance​

FASTEST ─────────────────────────────────────────────────────► SLOWEST
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
✅ ✅ ✅ ✅ ⚠️ ⚠️ ❌ ❌
constant log linear linearithmic quad cubic exp factorial

"What is n?" — feasibility rule of thumb (~1 second)​

O(log n), O(1)      → unlimited
O(n), O(n log n) → up to ~10⁸ (the practical sweet spot)
O(n²) → up to ~10⁴
O(n³) → up to ~few hundred
O(2ⁿ) → up to ~20–25
O(n!) → up to ~10–12

Algorithm quick-recall​

Sorting (comparison)  → Ω(n log n) lower bound; use Tim Sort (Python default)
Binary search → O(log n), REQUIRES sorted input
BFS / DFS → O(V + E)
Hash map → O(1) avg, O(n) worst (collisions)
Balanced BST → O(log n) all ops, ordered iteration
Matrix multiply (n×n) → O(n³) naive
k-NN (brute) → O(N·d) per query
Transformer attention → O(n²·d) time, O(n²) memory ← the LLM bottleneck
Training compute → ≈ 6 · P · D FLOPs

Optimization decision reflexes​

Nested loops / O(n²)      → hash set (→O(n)) or sort first (→O(n log n))
Exponential recursion → memoize / dynamic programming (→O(n) or O(n²))
Repeated searches → build an index once (sort, tree, hash, ANN)
NP-hard (2ⁿ, n!) → approximation / heuristics / metaheuristics
Out of GPU memory → smaller batch, mixed precision, gradient checkpoint, quantize, shard
Long-context LLM too slow → FlashAttention, sparse/linear attention, SSMs (Mamba)
Slow but "correct" Big-O → profile; fix constants & memory access, not the exponent

The three notations​

O(g)  → upper bound   → "at most"    → worst-case guarantee
Ω(g) → lower bound → "at least" → best-case / inherent difficulty
Θ(g) → tight bound → "exactly" → best == worst growth rate

Space complexity reflexes​

Recursion depth        = stack space (Python limit ~1000!)
In-place = O(1) auxiliary (heapsort, in-place partition)
Not in-place = O(n) auxiliary (merge sort)
ML training memory ≈ 16 × P bytes (Adam, mixed precision) + activations
ML inference memory = weights + KV cache (grows with context length)
Time↔Space lever = cache/index (space for time) ⇄ recompute (time for space)


Final principle: Big-O tells you which algorithm survives scale; profiling and hardware-awareness tell you which one wins the clock. Master both — the asymptotics for architecture decisions, and the constants for the last mile.


See also: Sorting Algorithms · Recursion & The Call Stack

Section: Foundation · All guides