python/volume-3/chapter-32ch 32 / 64 · 7 min

32The growth zoo — Big-O, and its cousins

In Chapter 31 we threw the stopwatch out and started counting the work an algorithm does instead — a number that doesn't care which laptop it runs on. Here we give that number its name and its grammar. First we take one real operation-count and watch a single term of it grow until it swallows everything else, then give what survives a two-character name. After that we line the whole family up on one pair of axes, from the tame to the monstrous. All the way through, we keep asking the one question that turns counting into judgement: if I double n, what happens to the work? By the end you'll look at a loop and say its growth out loud — O(n), O(n²), O(log n) — without running a line. You'll know why we throw away the "3" in 3n² and the whole +100n+7 behind it, and why that's not sloppiness but the plainest honesty about what breaks at scale. And you'll meet the growth zoo, nine animals from the tame O(1) to the monstrous O(n!), and know on sight which cage your algorithm is in.

Complexity is a price tag, not a stopwatch Why we throw away the constants and the small terms Read the code: count the deepest nest Big-O, Ω, Θ — the upper, the lower, the tight
scroll to begin

01Complexity is a price tag, not a stopwatch

Let's pick up exactly where the stopwatch left us. A stopwatch measures this run, on this machine, this second. Move the same code to a laptop twice as fast and the number just halves. So that number tells you almost nothing durable. What an engineer actually needs is the thing that doesn't flinch when you swap machines: how the work grows as the input grows. That has a name — time complexity — and the language we write it in is Big-O.

The unit is not seconds. It is operations: comparisons, additions, array reads, the elementary steps the machine actually takes. Call the input size n — the length of the playlist, the number of users, the pixels in the frame. Then ask a single question: if I double n, what happens to the number of operations? Linear search through an n-song playlist does, worst case, n comparisons. Double the playlist, and you double the work. We write that O(n) and read it "order n". The machine's speed sets how many operations fit in a second; Big-O sets how fast that count climbs. Those are two different facts, and Big-O is the one that decides whether your program survives contact with a big n.

Same algorithm, two machines — the clock disagrees, the curve does not. slow phone n=1M → 8.0 s fast server n=1M → 0.4 s both, as a curve both straight lines → O(n) opsn →
Fig — The machine sets the slope's steepness (the constant); Big-O captures the shape. Both machines run an O(n) algorithm — straight lines — no matter how far apart their stopwatches read.

So Big-O keeps the shape and drops the machine. But it drops something else too — it throws away most of the formula. Watch why that is the honest thing to do →

02Why we throw away the constants and the small terms

Suppose you count a routine's operations exactly and get f(n) = 3n² + 100n + 7. Big-O reports just O(n²). It deletes the coefficient 3, the entire 100n term, and the +7. That looks like vandalism. It is not. At the scale where performance actually matters, one term eats all the others alive. So let's not just assert it — let's count it.

where the work goes in 3n² + 100n + 7 n = 10 n² 23% 100n 76% n = 1000 n² = 96.8% ← the mountain 100n shrinks to 3%
Fig — Multiply n by 100 and the amber n² block goes from a fifth of the work to almost all of it. Big-O reports whichever block wins this race — here, O(n²).
dominance.pypython
def cost(n):
    return 3*n**2 + 100*n + 7          # an exact op-count, three terms

for n in (1, 100, 1000, 1_000_000):
    total       = cost(n)
    square_part = 3*n**2               # just the n² term
    print(n, f"{100*square_part/total:6.4f}%  is the n² term")

# 1         2.7273%  is the n² term
# 100      74.9869%  is the n² term
# 1000     96.7740%  is the n² term
# 1000000  99.9967%  is the n² term

Line by line: cost(n) returns the full three-term count. For each n we compute the total, then isolate square_part — the contribution of the term alone — and print its share of the whole. The story is in the last column. At n=1 the term is a rounding error: 2.7% of the work. By n=1000 it is 96.8%. At a million it is 99.9967%, and the 100n and the +7 together are three parts in a hundred thousand. Reporting anything but would be describing the dust and ignoring the mountain. That is the rule, and now you have watched it come true. Keep only the fastest-growing term, and drop its constant multiplier. The constant shifts the curve up and down, but it cannot change which term wins the race to infinity.

InteractiveSlide n — watch the n² term swallow the formula
f(n) = 3n² + 100n + 7 at n = 100 ▪ 3n² term: 75.0% ▪ 100n term: 25.0% ▪ +7: 0.0% As n climbs, the amber block eats the bar. That is why Big-O keeps only Big-O ⇒ O(n²)
100
The highest-order term always wins eventually — the only question is how big n has to get.
↺ The thing people get backwards
"Big-O ignores constants, so the constants don't matter." They matter enormously — just not for scaling. Two O(n) loops can differ by 50× in real time: one strides through a contiguous array (every cache line the CPU prefetched is a hit), the other chases pointers around a linked list (a cache miss per node — see Volume 1's memory hierarchy). Same Big-O, wildly different speed. Big-O tells you how the cost grows; the constant tells you how fast it is at your actual n. You need both — this volume teaches you to read the first, and later chapters teach you to fight for the second.
The deeper cut — what O(n²) actually means, formally

Big-O is a statement about an upper bound that holds eventually. Precisely: f(n) = O(g(n)) means there exist constants c > 0 and n₀ such that 0 ≤ f(n) ≤ c·g(n) for every n ≥ n₀. It's an existence claim. You win the argument by producing one pair (c, n₀) that works.

For our f(n) = 3n² + 100n + 7, take c = 4 and n₀ = 101. For n ≥ 101 we have 100n + 7 ≤ n² — at n=101, n² = 10201 against 100n+7 = 10107 — so 3n² + 100n + 7 ≤ 3n² + n² = 4n². Done: f(n) ≤ 4·n² forever after. That single witnessed pair is the whole proof. It's exactly why the lower-order terms are allowed to vanish — they can always be absorbed into a slightly larger constant.

Wait —
If I have to count operations to get the formula, do I have to trace every line by hand? No — you can read the growth straight off the shape of the code. There's a trick, and it fits on a sticky note.

03Read the code: count the deepest nest

You almost never count exact operations. Instead you read structure, and three rules cover most code you will ever meet:

That leaves one sticky-note rule to rule them all: find the most deeply nested piece of work, count how many times it runs, and that dominates the answer. Everything shallower is a lower-order term you're about to drop anyway.

The halving rule deserves a proof, because log n is the hinge that separates "instant" from "impossible" all over this volume. Watch a million collapse to a handful of steps:

readcode.pypython
n = 1_000_000
steps = 0
while n > 1:
    n //= 2          # throw away half of what's left, every step
    steps += 1
print(steps)         # 19        ← that's it. one million → 19 steps
import math
print(math.log2(1_000_000))   # 19.931568569324174

Line by line: we start at a million and keep integer-halving until nothing is left to split. It takes 19 steps, because log₂(1,000,000) ≈ 19.93 and you can't take a fraction of a step. Halving is doubling run backwards: 2¹⁰ ≈ a thousand, 2²⁰ ≈ a million, 2³⁰ ≈ a billion. So every time you multiply n by a thousand, a halving algorithm needs only ten more steps. That is why a database can find one row among a billion with about thirty comparisons, and why binary-searching your bug — delete half the code, does it still break? — beats reading every line.

InteractiveDerive the Big-O — pick a code shape, read off its cost
for i in range(n): step() operations f(n) = n = 16 at n = 16 O(n) Deepest nesting wins: one level → n, two levels → n², halving → log₂n.
16
The Big-O never mentions the constant or the coefficient — only which shape you picked.
structure → growth, at a glance for i in n: O(n) for i in n: for j in n: O(n²) while n>1: n //= 2 O(log n)
Fig — You can see the growth in the indentation. One loop climbs like a line, a loop-in-a-loop bends into a parabola, and a halving loop flattens almost immediately.
🌍 Where you meet this — every single day
Halving (log n) is the reason a database with a B-tree index finds your row among a billion in ~30 hops — it's binary search wearing a disk costume. Nested loops (n²) are the "why is this page slow" bug in a thousand codebases: an inner query inside an outer loop. And sequential-adds is why chaining ten fast passes over your data is still fast — ten times O(n) is still O(n). You reason this way off the screen too: the way you find a word in a dictionary (open the middle, not page one) is O(log n) by instinct.

Three rules, and you can read most code's cost off its shape. But "O(n) worst case" hid a word — worst. What about the best case, and the typical one? Meet Big-O's two cousins →

04Big-O, Ω, Θ — the upper, the lower, the tight

Big-O is precisely one thing: an upper bound. "This algorithm grows no faster than g(n)." It is a promise about the ceilingthe worst that can happen. But a full description of an algorithm has three walls, and each has its own name:

Linear search makes it concrete. Best case, the song is first in the playlist: one comparison, Ω(1). Worst case, it's last or missing: n comparisons, O(n). The two walls are different shapes, so there is no single Θ for "one run." But averaged over a random position, it does about n/2 comparisons — still Θ(n) on average, because n/2 is just n with a constant we drop. In casual speech everyone says "Big-O" and means "the tight bound," and usually gets away with it. Knowing all three lets you be exact when it counts.

n → c₁·g(n) — O (ceiling) c₂·g(n) — Ω (floor) f(n) — the real cost both walls same shape ⇒ f(n) = Θ(g(n))
Fig — f(n) runs forever between a floor Ω and a ceiling O. When both walls are the same shape g(n), the function is Θ(g(n)) — squeezed tight.

Myth

"Big-O is the average or typical running time." So an O(n²) sort must "usually" do work.

Reality

Plain Big-O is the worst-case ceiling — an upper bound, nothing more. A hash lookup is O(n) worst case (every key collides) yet Θ(1) in practice. If you mean "tight, typical," the correct symbol is Θ. Conflating them is how people end up shocked when the rare worst case shows up in production.
Wait —
We keep saying O(n²) is "worse" than O(n log n). Worse by how much? Line the whole family up from cheapest to most catastrophic, and the gaps aren't gaps — they're chasms.

05The growth zoo — from cheap to catastrophic

Here is the entire cast, ordered by how fast the cost climbs. Learn this ladder cold. Ranking two algorithms is often just asking "whose class is lower on this list."

O(1) < O(log n) < O(√n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)

input size n → operations / cost → O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ) at any fixed n, the vertical gap between curves is the whole story
Fig — The hero picture of the volume. Same axes, same n — and the curves fan apart catastrophically. Green stays on the floor; red leaves the building. Every optimisation in this book is a move from a higher curve to a lower one.

Curves are pretty, but the shock is in the numbers. Below is the "how big can n be in one second" table. It uses a measured rate for CPython 3.12 on this machine: a tight Python loop runs roughly 3×10⁷ simple steps per second (I ran it — ~3.17×10⁷, best of five — and it's heavily machine- and language-dependent; compiled C would be 10–100× more). What matters is not the exact rate but the gaps between the rows:

Classops at n=1,000,000largest n in ~1 s (this machine, rough)where it shows up
O(1)1no limitdict / array index, hash lookup
O(log n)~20astronomically largebinary search, B-tree index
O(√n)1,000~10¹⁵trial-division primality, grids
O(n)1,000,000~3×10⁷scan, sum, max, one pass
O(n log n)~2×10⁷~1.5×10⁶merge / quick / Timsort, FFT
O(n²)10¹²~5,600bubble / selection sort, all-pairs
O(n³)10¹⁸~320naïve matrix multiply
O(2ⁿ)~25every subset, naïve Fibonacci
O(n!)~10every ordering, brute-force TSP

Read the third column slowly. An O(n) algorithm chews through thirty million items a second. Drop to O(n²) and that collapses to about 5,600. Drop to O(2ⁿ) and you're stuck at 25, where one more item doubles the time. At O(n!), ten items is already the edge of the world, and eleven takes eleven times longer than ten. This is why "just brute-force it" quietly stops being an option, and why finding a smarter algorithm can be worth more than a thousand faster machines.

InteractiveSlide n — watch the growth zoo separate
operations at n = 16 · bar length is a log₁₀ scale (each unit = ×10) O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ) At small n they're neck-and-neck. Push n right and the red bar laps the field.
16
Without the log scale, the O(2ⁿ) bar at n=64 would be longer than the observable universe is wide.

You now hold the map of the whole zoo. The last thing to install isn't a fact — it's a reflex, the one that turns all this into the way you attack a problem you've never seen →

06The move: think in growth, hunt the exponent

Everything above collapses into one mental reflex — the move this whole volume is here to install. Faced with any new problem, before you write a line, ask these three questions in order:

1 · What is my n? name what grows 2 · What curve? read the deepest nest 3 · Drop a curve? sort · hash · memoise The whole method on a sticky note — ask these three, in this order, before writing a line.
Fig — The reflex this chapter installs: name n, read the curve, then hunt for a cheaper one.

That is the difference the volume's opening promised. The mediocre engineer optimises the constant — a faster loop, a cleverer line. The strong engineer changes the curve, and a curve change beats every constant once n is big enough. It even leaks into ordinary life. You binary-search a bug — does it still break with half the code gone? You hash instead of re-derive, writing down the answer so you never solve it twice. You refuse to re-solve what you already solved. Complexity thinking isn't just for code; it's a way of noticing where effort is being wasted as things scale.

InteractiveThe race — a 100×-faster machine vs a better curve
brute-force O(n²) on a 100×-faster machine vs a smart O(n log n) on a phone · n = 1,000,000 SUPERCOMPUTER · O(n²) · ×100 hardware 100 s PHONE · O(n log n) · ×1 hardware 0.20 s WHO FINISHES FIRST — AND BY HOW MUCH phone wins · 502× sooner 100 s vs 0.20 s — the 100× hardware lead, erased by a better curve. below n ≈ 1,000 brute force still wins; past it the curve decides — and the lead only widens.
1,000,000
Give brute force a machine 100× faster and it still loses once n passes ~1,000 — because O(n²) outgrows O(n log n) forever. Make the machine 1000× faster and the crossover barely nudges right; the winner never changes. Changing the curve beats buying a faster machine.
A note on where this notation came from
The "O" is genuinely old — it comes out of 19th- and early-20th-century number theory, where the "O" stood for the order of a function's growth. Donald Knuth carried it into computer science in the 1970s and sharpened the family, insisting programmers also use Ω for lower bounds and Θ for tight ones. The lesson in the history is the lesson of the chapter: the people who invented this weren't measuring seconds — they were asking a purer question, "how does this grow?", and that question turned out to outlive every machine they could have run it on.

You can now price an algorithm in time. But time is only half the bill — every algorithm also spends memory, and the two trade against each other in ways that decide real systems. The next chapter opens the second ledger: space complexity, the stack versus the heap, and why an O(n)-time algorithm can still blow up your RAM →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 32, in working code

Twelve tiny programs that never once start a stopwatch. They count the work instead, so you can watch O(1), log n, √n, n, n², 2ⁿ and n! pull apart in plain integers — the same on every machine on Earth.

Count the price, not the clock
A stopwatch measures this run, on this machine, this second. Counting operations gives a number that belongs to the algorithm — the same integer everywhere — and lets us drop the constants and small terms that clutter it.
Read the cost off the shape
You almost never count exact operations — you read structure. Three rules cover most code you meet: sequential blocks ADD, nested loops MULTIPLY, and halving gives log n. Find the deepest nest and you've found the answer.
Big-O and its two cousins
Big-O is one thing: the ceiling, the worst case. Ω (Omega) is the floor, the best case. Θ (Theta) is the tight sandwich, when floor and ceiling share a shape. Linear search shows all three; binary search shows why a lower curve wins.
Walk the growth zoo, then drop a curve
Line the family up from cheap to catastrophic: √n, then 2ⁿ and n! at the far end. The whole game of this volume is one move — spot your curve, then drop to a lower one. Remembering answers instead of recomputing them turns exponential into linear.
end of chapter 32 · six sections
iolinked.com
Written by Ajai Raj