Big-O on autopilot
📖 Walk me through it — plain English
Big-O is a shorthand for how fast the amount of work grows as the input gets bigger. It is NOT a stopwatch — it does not tell you "this takes 4 milliseconds." It answers a different question: if your input suddenly got 10 times larger, would the work stay the same, grow 10 times, grow 100 times, or blow up completely? We write it as O(...): O(1) means flat (the work never changes), O(n) means it grows in step with the input, and O(n²) means it grows like the input squared. The "n" is just the size of the input — the number of items in your list, say.
Because we only care about that growth shape at large sizes, we throw away two kinds of clutter. We drop constants (a "times 3" or "times 100" out front), and we drop lower-order terms (smaller pieces added on). So 3n + 50 simplifies to just O(n): when n is huge the "+50" is a rounding error, and the "times 3" doesn't change the fact that it's still a straight line. Only the fastest-growing piece survives.
Turning a chunk of code into a Big-O is mostly two rules. Steps done one after another ADD, and you keep the biggest — a loop followed by a separate loop is O(n) + O(n), which is still just O(n). Steps nested inside each other MULTIPLY — a loop running inside another loop, both over the same n items, is O(n) × O(n) = O(n²). A "loop" here just means repeating a block of work once per item.
Picture a classroom. If the teacher walks the row and high-fives each student, that's one pass — O(n), and doubling the class doubles the high-fives. But if every one of the n students greets every student (a loop inside a loop), each does about n greetings, so the total is about n × n. Double the class and the greetings roughly quadruple. That explosion is the O(n²) "compare every pair" pattern — most clever algorithms exist to dodge it. Here it is traced on a tiny list of 3 numbers, [3, 5, 9]: the outer loop picks one item (highlighted), and the inner loop walks every item to pair against it.
Why does this matter so much? Because the growth shape decides whether your solution finishes at all. The lesson's constraint decoder turns this into a hint: if a problem says n can be up to 10⁶ (a million), an O(n²) solution would need a trillion steps and never finish — so the size of n is quietly telling you which Big-O you're allowed to aim for. Reading "n ≤ 10⁶" as "I need O(n) or O(n log n)" is the reflex this whole lesson is training.
Before you can pick the right algorithm, you need a shared language for how the work grows as the input grows. That's all Big-O is — and once it's reflexive, every later phase gets cheaper.
Big-O measures growth rate, not wall-clock time. It answers one question: "if I 10× the input, does the work stay flat, grow 10×, 100×, or explode?" That's why we drop constants and lower-order terms — 3n + 50 and n both grow as a straight line, so both are O(n). At large n, the fastest-growing term is the only one that matters.
Turning code into a complexity is really just two rules:
- Sequential steps add — you keep the biggest. A loop, then a separate loop, is O(n) + O(n) = O(n).
- Nested steps multiply. A loop inside a loop over the same n is O(n) × O(n) = O(n²).
Worked example: the outer loop runs n times; for each, the inner loop runs n times → n × n = n² total. That's why "compare every pair" is the O(n²) you're always trying to beat — and most of the patterns ahead exist to dodge exactly that nested loop.
One more: when a problem has two different inputs, they don't collapse into one n. Scanning two lists is O(m + n); filling an m×n grid is O(m·n). Name both.
Every term, defined once
The walkthrough above gave you the feel; this section pins down each word so nothing is left fuzzy. Read it once and the rest of the lesson is just vocabulary you already own.
- Big-O — a notation for the upper bound on how the work (or memory) of an algorithm grows as the input size
ngrows. "O" stands for "order of." It deliberately ignores hardware, language, and exact step counts; it captures only the shape of the growth curve at largen. - n — the size of the input. Usually the number of items (length of a list, number of nodes in a graph, characters in a string). If there are two independent sizes, we name them separately, e.g.
mandn. - Time complexity — how the number of operations grows with
n. This is what people mean by default when they say "the Big-O." - Space complexity — how the extra memory grows with
n. "Extra" means memory you allocate beyond the input itself: new arrays, hash maps, and — easy to forget — the recursion call stack (see the space note below). - Best / worst / average case — the same algorithm can do different amounts of work depending on the specific input. Best case is the luckiest input, worst case is the most punishing, average case is what you expect over typical inputs. Interviews almost always mean worst case unless they say otherwise, because that's the guarantee you can actually promise. Example: linear search for a value is O(1) best case (it's the first element) but O(n) worst case (it's last or absent), so we call it O(n).
- Amortized — the average cost per operation across a long sequence, even when one occasional operation is expensive. Appending to a dynamic array (Python
list.append) is O(1) amortized: almost every append is cheap, and the rare "the array is full, copy everything to a bigger buffer" step (which is O(n)) is so infrequent that, spread over all the appends, the average stays O(1). Amortized is a promise about the long run, not about any single call.
Why "drop constants and lower-order terms" is allowed. Big-O is a statement about large n. Take 2n² + 100n + 5000. At n = 10 the constant 5000 dominates — but Big-O does not care about n = 10, it cares about the trend. At n = 1,000,000 the 2n² term is two trillion while 100n is a hundred million (0.005% of the total) and the 5000 is invisible. The fastest-growing term wins, and its constant multiplier (the "2") never changes the shape of the curve — so we keep only O(n²). Same logic retires the "+50" and "×3" from the walkthrough's 3n + 50.
With that lens, here's the ladder to recognize on sight — and the solution shapes that land on each rung:
| Complexity | n = 10⁶ feels like | Typical shape |
|---|---|---|
| O(1) | instant | hash lookup, math, array index |
| O(log n) | instant | binary search, balanced-tree op |
| O(n) | ~10ms | one pass, two pointers, sliding window |
| O(n log n) | ~50ms | sort, merge-sort recursion, n heap pushes |
| O(n²) | infeasible | nested loop on same n, brute pair-finding |
| O(2ⁿ) | infeasible past n≈25 | subsets, naive recursion (no memo) |
| O(n!) | past n≈11 | permutations brute force |
Each class, in one line of plain English
The table above is the cheat sheet; here is what each rung actually means, plus a rough cap on how big n can be before that complexity stops finishing in about one second (assume a machine does very roughly 10⁸–10⁹ simple operations per second — a useful back-of-envelope, not a guarantee):
O(1) — constant. Work never changes with n. Feasible n: unlimited. Looking up d[key] in a hash map or arr[i] by index takes the same time whether the collection holds 10 or 10 billion items.
O(log n) — logarithmic. Each step throws away half the remaining input. Feasible n: effectively unlimited (log₂ of a billion is only ~30). Binary search and balanced-tree operations live here.
O(n) — linear. Touch each item a constant number of times. Feasible n: ~10⁸ in a second. One pass, two pointers, a single sweep.
O(n log n) — linearithmic. A linear pass repeated a logarithmic number of times. Feasible n: ~10⁶–10⁷. This is the speed of a good sort, and the practical ceiling for "I have to look at everything but can afford to sort it first."
O(n²) — quadratic. Work for every pair of items. Feasible n: ~10⁴ (a few thousand). The nested-loop / "compare everything to everything" shape you're usually trying to escape.
O(2ⁿ) — exponential. Work doubles with each added item. Feasible n: ~20–25. Enumerating every subset, or naive branching recursion without memoization.
O(n!) — factorial. Work for every ordering of the items. Feasible n: ~11. Brute-forcing all permutations (the travelling-salesman-by-hand shape).
Counting loops: traced snippets
You already have the two rules — sequential adds, nested multiplies. Here they are applied to real code so the counting becomes mechanical. The trick is to ask, for each loop, "how many times does this run, as a function of n?" then combine.
# SEQUENTIAL — two separate loops, one after the other.
for x in items: # runs n times
print(x)
for y in items: # runs n times (separate sweep)
print(y)
# total = n + n = 2n -> drop the constant -> O(n)
# NESTED — a loop inside a loop, both over the same n items.
for x in items: # outer runs n times
for y in items: # inner runs n times FOR EACH outer step
print(x, y)
# total = n * n = n^2 -> O(n^2)
# TRIANGULAR — inner loop starts at i, so it shrinks each pass.
for i in range(n): # outer runs n times
for j in range(i, n): # inner runs (n-i) times
work(i, j)
# total = n + (n-1) + ... + 1 = n(n+1)/2 ~ n^2/2 -> still O(n^2)
# LESSON: half of a square is still a square. Drop the 1/2.
# HALVING — the counter multiplies, so the loop count is logarithmic.
i = 1
while i < n: # i doubles: 1, 2, 4, 8, ... up to n
work(i)
i = i * 2 # it takes ~log2(n) doublings to reach n
# total ~ log2(n) iterations -> O(log n)
# SORT THEN SCAN — sequential, so add and keep the biggest term.
items.sort() # O(n log n)
for x in items: # O(n)
work(x)
# total = O(n log n) + O(n) -> O(n log n) (n log n grows faster)
Two traps when counting nested loops. First: a nested loop is only O(n²) when the inner loop genuinely depends on n. for x in items: for k in range(10): is O(n) — the inner loop is a fixed 10 iterations, a constant, so it's n × 10 = O(n). Second: don't double-count work that's hidden inside a call. If the inner line is if y in some_list: and some_list is a Python list, that membership test is itself O(n) — so the whole thing is O(n²) even though you only wrote one visible loop. Switch some_list to a set and the test becomes O(1), pulling it back to O(n). The cost of every operation, including library calls, counts.
Space complexity: the call stack counts
Time is only half the story; interviewers also ask about extra memory. The rule mirrors time: count the size of any data you allocate as a function of n. Building a new list of all n items is O(n) space; using a handful of fixed variables is O(1) space ("in place").
The piece people forget: recursion is not free in space. Every pending recursive call sits on the call stack — a stack of paused function frames waiting for the deeper calls to return — and each frame uses memory. So the space cost of a recursive function is at least the maximum depth it reaches, even if it allocates no arrays at all.
# Counts down to 0. Each call waits for the next, so n frames
# are stacked at the deepest point.
def countdown(n):
if n == 0:
return
countdown(n - 1) # one deeper call each time
# TIME: O(n) — n calls total
# SPACE: O(n) — stack depth reaches n before any call returns
Compare that to a loop doing the same job: for i in range(n): ... is O(n) time but O(1) space — no frames pile up. And a recursion that halves its input each call, like binary search, only reaches depth log n, so it's O(log n) space. The shape of the recursion's depth, not its total number of calls, sets the stack space.
The constraint decoder, reversed
The most useful trick in a timed setting runs the table backwards: the problem hands you the maximum n, and that number quietly tells you which complexity you're allowed to aim for. If your first idea is too slow for the given n, the constraint is a hint to find a better one.
n ≤ 10⁸ — must be O(n) or O(log n).
n ≤ 10⁶ — O(n) or O(n log n).
n ≤ 10⁴ — O(n²) is fine.
n ≤ 500 — O(n³) opens up.
n ≤ 20 — bitmask DP, O(2ⁿ·n).
n ≤ 11 — permutations, O(n!).
Common pitfalls.
- Treating Big-O as a stopwatch. An O(n) algorithm with a huge constant can be slower than an O(n log n) one at small sizes. Big-O describes the trend, not the speed at any single
n— it's the right tool for "does this scale," not "is this fast right now." - Hidden costs inside calls.
x in a_list, slicinga[1:],"".join(...), andsorted(...)all do real work. A "simple" loop body can secretly be O(n), turning an O(n) loop into O(n²). - Collapsing two inputs into one n. Two lists is O(m + n); an m×n grid is O(m·n). Don't pretend a second input doesn't exist — name it.
- Forgetting the call stack in space. A recursive solution is not O(1) space; its stack depth counts (see above).
- Quoting best case. Unless asked, report the worst case — that's the guarantee. "It's O(1) if the answer is the first element" is rarely the answer they want.
Takeaway: Big-O is the growth shape of work (time) or memory (space) as the input n grows — not a clock. Drop constants and lower-order terms; only the fastest-growing term survives. Sequential steps add (keep the biggest); nested steps multiply. Count every loop and every hidden library call, remember the recursion stack as space, report the worst case, and read the constraint on n as a direct hint to the complexity you must hit.
Go deeper (optional): the formal definitions behind the shorthand are Big-O (upper bound), Big-Ω (lower bound), and Big-Θ (tight bound, when upper and lower match). For a fuller treatment with proofs, the standard reference is Introduction to Algorithms (Cormen, Leiserson, Rivest, Stein) — but you will not need any of it to read code fluently; the rules in this lesson are enough.