DSA · File 01 of 10

Complexity Analysis: Big-O from first principles

Every other topic gets judged through the lens you build here. Interviewers don't just want the right answer — they want to hear you reason about cost, out loud. This is the grammar of that conversation.

01The problem Big-O actually solves

You're on-call. A batch job that processed 10K records in 2 seconds suddenly gets 10M. Will it take ~30 minutes (linear — annoying), ~3 weeks (quadratic — page someone), or ~heat death of the universe (exponential — redesign)?

You can't answer by timing code on your laptop — wall-clock depends on hardware, load, cache temperature. So we ask a sharper question:

The question: as the input grows, how does the amount of work grow? Big-O is not about speed. It's the shape of the growth curve — and the curve always wins. O(n²) in C eventually loses to O(n log n) in Python.
n → work O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ)
The only chart that matters. Below the O(n) line you scale forever; above O(n log n) you start negotiating with n; the red zone is where inputs over ~10⁴ go to die.

The one-sentence definition (know it): f(n) is O(g(n)) if, past some input size, f(n) is bounded above by a constant multiple of g(n). Translation: "eventually, my cost grows no faster than g(n), ignoring constants."

Why we legally drop constants and small terms

3n² + 50n + 1000O(n²). At n = 1,000,000 the n² term is 3·10¹², while 50n is 5·10⁷ — the linear term contributes 0.001% of the work. Big-O describes the large-n regime, which is exactly the regime where you care.

Backend analogy: you don't tune the 2ms of JSON parsing when the query behind it takes 800ms. Big-O is the mathematical version of "profile first, optimize the dominant term" — exactly how you took that endpoint from 71s to 2s.

02The seven curves, with real intuition

Big-ONameGut feelingYou've already met it
O(1)constant"look it up"Redis GET, dict access, array index
O(log n)logarithmic"halve what's left each step"Postgres B-tree index, binary search
O(n)linear"touch everything once"Full table scan, replaying a Kafka partition
O(n log n)linearithmic"sort-shaped work"ORDER BY without an index, merge sort
O(n²)quadratic"everything vs everything"Naive JOIN, nested loops over one list
O(2ⁿ)exponential"try every subset"Unmemoized recursion, brute-force search
O(n!)factorial"try every ordering"Brute-force traveling salesman

O(1) — flat, not fast

The work doesn't depend on input size. A dict lookup in a 10-element dict and a 10-million-element dict cost roughly the same. "Constant" can still be a big constant — it just doesn't grow.

O(log n) — the most underappreciated curve in computing

Every step cuts the remaining problem by a constant fraction. That's the signature. Hear "halve", "binary", "tree height", "divide the range" → think log.

Why log is absurd: log₂(1,000,000) ≈ 20. log₂(1,000,000,000) ≈ 30. You scaled the input 1000× and the work went up by 10 steps. This is why a Postgres B-tree finds one row among a billion in a handful of page reads.
def binary_search(a, target):
    lo, hi = 0, len(a) - 1
    while lo <= hi:               # runs ~log2(n) times
        mid = (lo + hi) // 2
        if a[mid] == target: return mid
        if a[mid] < target: lo = mid + 1
        else: hi = mid - 1
    return -1

O(n) — one pass (or two, or five)

Touch each element a constant number of times. Two passes is still O(n) — but say "I make two passes" out loud anyway; it signals you know and chose to ignore the constant.

O(n log n) — "sort-shaped"

Two ways to get here: O(log n) levels of O(n) work each (merge sort), or n operations at O(log n) each (n pushes through a heap). Rule of thumb: if your solution sorts, you're at least n log n. Comparison sorting cannot beat it — n! orderings, each comparison halves the candidates, so you need ≥ log₂(n!) ≈ n log n comparisons.

O(n²) — the all-pairs shape

for i in range(n):
    for j in range(i + 1, n):   # every pair: n(n-1)/2 → O(n²)
        ...

n = 10⁴ → 10⁸ comparisons → seconds in Python. n = 10⁵ → 10¹⁰ → minutes-to-hours. Heuristic: if n can reach 10⁵, quadratic times out — the interviewer wants n log n or n.

O(2ⁿ) and O(n!) — combinatorial explosions

"Every subset" = 2ⁿ. "Every ordering" = n!. Only acceptable when n ≤ ~20 (subsets) or n ≤ ~10 (orderings) — which is exactly the range where interviewers expect backtracking (file 07).

03Reading complexity off code — three rules

Rule 1

Sequential blocks add → take the max. O(n) then O(n²) = O(n²).

Rule 2

Nested blocks multiply. Loops over different inputs → say O(n·m), not "n²". Precision reads as skill.

Rule 3

Know the cost of what you call. The classic slip: a linear operation hiding inside a loop.

✗ The classic trap — O(n²)
for x in items:          # O(n) iterations…
    if x in some_list:  # …× O(n) scan
        ...              # total: O(n²)
✓ One-word fix — O(n)
for x in items:          # O(n) iterations…
    if x in some_set:   # …× O(1) hash
        ...              # total: O(n)

Python costs you must know cold

OperationCostNote
lst[i], lst.append(x)O(1)append is amortized — §5
lst.insert(0,x), lst.pop(0)O(n)shifts everything — use deque
x in lstO(n)linear scan
x in set_ / d[k]O(1) avghashing (file 02)
lst.sort() / sorted()O(n log n)Timsort (file 08)
slice lst[a:b]O(b−a)copies!
s += t in a loopO(n²) totalstrings immutable — use ''.join
heapq.heappush/popO(log n)file 06
min(lst) / max(lst)O(n)full scan every call

04Space — the forgotten sibling

Same idea, applied to extra memory (by convention, the input doesn't count).

The universal trade: you can almost always buy time with space. A hashmap is precomputed answers-by-key. That's literally what caching is — Redis in front of Postgres is "spend O(n) memory to turn slow queries into O(1) lookups."

When an interviewer asks "can you do better?", your first two thoughts, in order:

  1. Trade space for time? — hashmap, prefix sums, memoization
  2. Exploit structure? — sorted input → binary search / two pointers
Don't forget recursion: the call stack is space. DFS on a tree of depth d = O(d) stack — O(log n) if balanced, O(n) if it's a degenerate stick. Python's ~1000-frame recursion limit makes this practical, not theoretical. Mention it and offer the iterative version — free points.

05Amortized analysis — why append is O(1) even when it isn't

Python lists are dynamic arrays: contiguous memory with spare capacity. Usually append drops into a free slot — O(1). When full, Python allocates bigger and copies all n elements — that one call is O(n).

capacity 4:  [a][b][c][d]                 append(e) → FULL!
                  │
                  ▼  allocate 8 · copy 4 · insert
capacity 8:  [a][b][c][d][e][ ][ ][ ]     ← next 3 appends: cheap O(1)

The banker's argument (say this one out loud): charge every append 3 coins. One pays for the insert; two go into the bank. When a resize must copy n elements, the bank — filled by the n/2 appends since the last resize — pays for the whole copy. No operation ever goes into debt → amortized O(1).

Equivalently: total copy work to reach size n with doubling is n/2 + n/4 + … ≈ n. So n appends cost ≈ 2n → O(1) each.

Where amortized thinking wins interviews

  • list.append and hashmap inserts (resize at load factor) — amortized O(1)
  • Queue-from-two-stacks: each element moves at most twice → amortized O(1) per op
  • Monotonic stack loops (file 03): the inner while looks nested, but each element is pushed and popped once → total O(n)
The magic phrase: "Each element is processed a constant number of times across the whole run, so the total is O(n) even though a single iteration can be O(n)."
Three different things — don't blur them: Amortized = guarantee over any sequence (no probability). Average-case = assumes random inputs (hashmap O(1) is this; adversarial colliding keys force O(n) — a real DoS vector: hash-flooding). Worst-case = the ceiling for a single operation.

06Best / average / worst — which to report

Default to worst case unless told otherwise. The three classic splits:

AlgorithmAverageWorstWhy the worst happens
QuicksortO(n log n)O(n²)adversarial pivots (sorted input + naive pivot)
Hashmap opsO(1)O(n)all keys collide into one bucket
BST opsO(log n)O(n)sorted inserts → a stick (hence self-balancing trees)

07Narrating analysis — the meta-skill being graded

Interviewers grade the narration as much as the answer. The rhythm:

  1. Before coding, state the target. "Brute force is O(n²) — all pairs. n is up to 10⁵ so that's 10¹⁰ ops — too slow. I'll aim for O(n) with a hashmap." Thirty seconds; enormous signal.
  2. While coding, price what matters. "This in is against a set, so O(1)… this slice copies, but it's outside the loop."
  3. After coding, state both bounds unprompted. "Time O(n) — one pass, O(1) per element. Space O(n) worst case for the map."

Phrases that buy goodwill

  • "Let me get a correct brute force first, then optimize." (then actually do it)
  • "The bottleneck is this membership test — a set fixes it."
  • "This is amortized O(1) — one call can resize, but n calls total O(n)."
  • "Recursion depth is O(n) worst case; I can convert to iterative if stack limits worry us."

08The decoder ring — constraints reveal the intended answer

The constraint in the problem statement is a hint about the intended complexity. Read it first, always:

ConstraintIntended complexitySmells like
n ≤ 10–12O(n!), O(2ⁿ·n)permutations, brute force
n ≤ 20–25O(2ⁿ)subsets, bitmask, backtracking
n ≤ ~500O(n³)Floyd–Warshall, interval DP
n ≤ ~5,000O(n²)2D DP, all-pairs
n ≤ ~10⁵–10⁶O(n log n) or O(n)sort, heap, sliding window, hashmap
n ≤ ~10⁹+O(log n) or O(1)binary search on the answer, math

Calibration: Python does roughly ~10⁷ simple ops/second pessimistically; judges budget a few seconds.

09Feel the curves — interactive

If n = 100,000, how much work is each shape?

1010³10⁵10⁷10⁹
ShapeOperationsAt ~10⁷ ops/secVerdict

Verdicts assume a typical online-judge budget of a few seconds — the same instinct works for API latency budgets.

10Rapid-fire interview Q&A

Q1 · while i < n: i *= 2 — complexity?
O(log n) — i doubles, so ~log₂(n) iterations. Multiplying/dividing the loop variable ⇒ log.
Q2 · for i in range(n): for j in range(i): … ?
O(n²). Inner runs 0+1+…+(n−1) = n(n−1)/2. Triangular is still quadratic — half a square is still square-shaped.
Q3 · Why is x in my_list slow but x in my_set fast?
List: linear scan, O(n). Set: hash x, jump to its bucket, compare a handful of entries, O(1) average.
Q4 · Two nested loops over different inputs?
O(n·m), not O(n²). Keep the variables distinct.
Q5 · Why is repeated string concatenation O(n²)?
Strings are immutable; each s += t copies everything accumulated so far. Copies of size 1+2+…+n ⇒ O(n²). Fix: collect parts in a list, ''.join(parts).
Q6 · Binary search on a linked list — O(log n)?
No. Reaching the midpoint costs O(n) — no O(1) indexing. Total stays O(n). Data-structure choice constrains algorithm choice.
Q7 · Can O(n²) ever beat O(n log n) in practice?
Yes — small n, constants dominate. That's why Timsort switches to insertion sort for tiny runs. Saying this shows you know Big-O is asymptotic, not a benchmark.
Q8 · Naive recursive fib(n)?
O(2ⁿ) (precisely O(φⁿ)) — the call tree explodes, recomputing the same subproblems. Memoization collapses it to O(n). The doorway to DP.

11Self-test — answer before you peek

  1. Define Big-O in one sentence a junior engineer would understand.
  2. Simplify: O(5n² + 3n log n + 200n + 7).
  3. lst.pop() vs lst.pop(0) — costs and why?
  4. Explain amortized O(1) append using the doubling argument, ≤ 3 sentences.
  5. n ≤ 10⁵: what complexities are viable? What does n ≤ 20 hint instead?
  6. Extra space of recursive DFS on a balanced tree? On a degenerate stick?
  7. Why can't any comparison sort beat O(n log n)?
  8. One worst-case scenario each: quicksort, hashmap lookup, BST insert.
  9. What's wrong with while target not in seen_list: inside a loop, and the fix?
  10. Say your solution's time and space in two sentences, interviewer-style.
Spot-check answers
  1. "As input grows, the work grows no faster than this function, ignoring constants."
  2. O(n²).
  3. pop() O(1) (end); pop(0) O(n) — shifts every element left.
  4. Doubling makes resizes rare; total copy work to reach n is ≈ n; spread over n appends ⇒ O(1) each.
  5. O(n log n) or better; n ≤ 20 hints exponential/backtracking is intended.
  6. O(log n) balanced; O(n) stick.
  7. n! orderings; each comparison at most halves candidates ⇒ ≥ log₂(n!) ≈ n log n comparisons.
  8. Sorted input + first-element pivot; all keys in one bucket; inserting sorted keys.
  9. List membership is O(n), making the loop O(n²); use a set.
  10. "Time O(…) because …; space O(…) for the …" — practice the sentence shape.

Source: 01-dsa/01-complexity-analysis.mdNext: Arrays, Strings & Hashing →