Hermes Wiki

Big O and Complexity Analysis

Concept

Big O notation describes how an algorithm's resource usage — time or space — grows as input size n grows, stripped of machine-specific constants. O(f(n)) is an upper bound: the algorithm's actual cost is at most some constant multiple of f(n) for large enough n. It answers "how does this scale," not "how fast is this on my laptop today" — a O(n log n) sort and an O(n^2) sort can have the faster-in-practice implementation be the O(n^2) one at small n, because constants and cache behavior dominate before the asymptotic term does. That's why Big O is a scaling argument, not a benchmark.

A companion idea, amortized analysis, answers a different question: not "what does the worst single operation cost" but "what does a sequence of operations cost on average, guaranteed." A dynamic array's append is O(n) in the single worst case (the backing array is full and must be resized and copied), but O(1) amortized — because doubling the array size on resize means resizes become exponentially rarer, and the total copying cost across n appends is O(n), spread over n operations. Amortized analysis is what makes claims like "hash table insert is O(1) amortized" honest rather than misleading.

Three related notations matter for precision: O (upper bound, worst case or amortized), Ω (lower bound, best case), and Θ (tight bound, when upper and lower coincide). In practice, engineers say "O(n)" when they usually mean Θ(n) — the distinction rarely matters day to day, but it matters when an interviewer or a code reviewer asks "is that actually tight?"

Tradeoffs

Complexity class Example 10x input → cost multiplier Practical ceiling (rough, modern hardware)
O(1) Hash map lookup 1x Unbounded
O(log n) Binary search ~1.1x (adds a constant) Unbounded
O(n) Linear scan 10x Billions of elements
O(n log n) Comparison sort (mergesort) ~11x Hundreds of millions
O(n^2) Naive nested-loop dedup 100x Tens of thousands
O(2^n) Naive recursive Fibonacci, subset enumeration astronomically worse ~30-40 items

The table is the actual argument for why complexity class matters more than micro-optimizing constants: an O(n^2) algorithm optimized to be 10x faster in absolute terms is still O(n^2) — it hits the same wall, just slightly later. An O(n log n) algorithm that's 10x slower in absolute terms because of a naive implementation still wins decisively once n is large enough, because the growth curves diverge, not just the starting points.

When to use / when not to

  • State the complexity of any proposed solution unprompted, before being asked — in an interview and in a design review alike, this is the first thing a competent reviewer checks, and volunteering it signals the analysis already happened rather than being an afterthought.
  • Reach for amortized analysis specifically whenever a data structure has an occasional expensive operation masking a cheap steady state — dynamic arrays, hash table resizing, splay trees. Reporting only the worst-case single-operation cost undersells the structure; reporting only "it's usually fast" oversells it without a guarantee.
  • Don't treat Big O as a proxy for real-world speed at small or fixed n — a O(n^2) insertion sort reliably beats O(n log n) mergesort below roughly n=20-50 in most implementations, because mergesort's overhead (recursion, allocation) dominates at that scale. Complexity class is a statement about the limit as n → ∞, not a promise about any specific n.
  • Don't stop at "the algorithm is O(n)" without stating space complexity too — an in-place O(n) algorithm and one that allocates O(n) extra memory are both "O(n) time" but represent very different tradeoffs on memory-constrained systems.

Common pitfall

Quoting the complexity of the wrong operation, or of one operation while ignoring another that dominates in practice. The canonical example: "hash map is O(1)" is true for average-case lookup with a well-distributed hash function, but worst-case (all keys colliding into one bucket, which a poorly chosen or adversarial hash function can force) is O(n) per lookup. A second common version: quoting an algorithm's complexity while ignoring a hidden cost buried in a library call — e.g., treating Python's list.pop(0) as O(1) because it "just removes an element," when it's actually O(n) because every remaining element shifts left one position.

Engineering Lens

Complexity analysis is the shared vocabulary that lets two engineers agree on whether a proposed fix actually solves a scaling problem or just moves the wall further out. The strongest version of "I chose this approach because of its complexity" names the specific operation whose growth rate was the bottleneck (not "the algorithm" in the abstract), states whether the bound is worst-case or amortized, and — critically — ties it back to a real n: a service handling 200 requests/day doesn't need an O(n) fix turned into O(log n) if n never exceeds a few hundred, and that engineering judgment (knowing when not to optimize) is as much a signal of seniority as knowing the optimization itself. The failure mode to watch for in review is complexity-class tunnel vision — obsessing over asymptotic behavior on a hot path that never actually sees large n, while a genuinely unbounded n elsewhere in the same system goes unexamined.

Sources

Hermes Wiki