Topic 03

Big-O Notation

Complexity

Big-O answers one question: when the input grows, how fast does the work grow? It deliberately throws away everything else. The machine, the language and the constant cost of each step all change a number, while growth changes whether the program finishes at all. Lantern's scan of 2 million records and its index lookup differ by more than any hardware upgrade could close, and Big-O is the compact way to say so.

It is also the shared vocabulary of the whole field. Interviews ask for it because every engineer can check it. Production needs it for a better reason: it predicts what happens to a service when its data grows tenfold, which is the question behind nearly every capacity outage.

Growth, Not Speed

Big-O classifies a procedure by the shape of its cost curve, not by its speed. O(n), read "order n", means that doubling the input roughly doubles the work. O(n²) means that doubling the input quadruples it. O(1) means the work does not grow with the input at all. None of these says how many milliseconds anything takes.

The chart below plots three classes over the same inputs. At n equal to 10 they are close enough to argue about. By n equal to 32 the quadratic curve has left the chart at 1,000 steps, while n log n is near 160 and n is at 32. By 100, n log n has reached about 660 and n has reached 100. The curves never cross back.

Three growth classes over the same inputs
2505007501,0000255075100input size nstepsn: 100 stepsn log n: about 660n²: 1,000 steps at n ≈ 32

Dropping Constants and Lower Terms

Suppose a scan costs three steps per record plus five steps of setup: 3n + 5. The five disappears at any real size. The three depends on what counts as a step and how fast the machine does one, so it changes from one computer to the next. Big-O drops both and calls the scan O(n).

The same rule removes smaller terms. A procedure that costs n² + 1,000n steps is O(n²), because at a million items the second term is a tenth of one percent of the total. What is left after the dropping is the one fact that holds on every machine: how the work grows.

Reading Code for Its Class

Four rules cover most code. Statements in sequence add, and the biggest term wins. A loop multiplies its body's cost by the number of times it runs. Two nested loops over the same input give n². A loop that halves what is left each time runs about log n times: 2 million halves down to one in 21 steps, and a billion in 30.

Reading three functions for their class
def first(records):                 # O(1): one step whatever n is
    return records[0]

def letters_used(records):          # O(n): the inner loop is fixed at 26
    for r in records:
        for letter in "abcdefghijklmnopqrstuvwxyz":
            ...

def duplicate_pairs(records):       # O(n²): every record against every record
    for a in records:
        for b in records:
            ...

The three functions above make the trap visible. The first reads one element and is O(1) no matter how long the list is. The second has two nested loops but is O(n), because the inner loop always runs 26 times: its trip count is fixed, not tied to the input. The third also has two nested loops, and this time both run over the records, so it is O(n²). The indentation of the second and third is identical. The trip counts decide the class.

Lantern's Two Searches in Big-O

The scan is O(n) in the size of the catalogue. The index lookup is O(1) to find the term, on average, plus O(k) to read its k matching records, and neither part depends on the 2 million. Grow the catalogue tenfold to 20 million and the scan costs ten times more. The lookup for a given query costs about the same, because the number of records matching a rare surname hardly changes.

That one sentence makes the case for building an index, and it is the pattern behind every index in every database: pay once, when the data is written, so that every read stops paying in proportion to the data.

Upper Bounds, Lower Bounds, Tight Bounds

Strictly, O is a ceiling: "grows no faster than". Its partner Ω, omega, is a floor: "grows at least as fast as". Θ, theta, is both at once: "grows exactly like". By the strict reading, saying binary search is O(n) is true, because log n grows no faster than n, and it is also useless.

Working engineers write O and mean the tight bound, the smallest honest ceiling. This book does the same from here on. When a page says a hash lookup is O(1), it means the lookup does not grow with the table, not merely that it grows no faster than something larger.

What Big-O Does Not Tell You

Big-O says nothing about constants, nothing about small inputs, nothing about memory, and nothing about which case was analysed. An O(n log n) sort loses to O(n²) insertion sort on a dozen items; real sort functions switch to insertion sort for short runs. An O(n) scan of a short array beats an O(log n) tree because of how processor caches work, which is the subject of Chapter 4. And "hash lookup is O(1)" is a statement about the average, which an attacker can turn into O(n).

So Big-O is the first question, not the last. It picks the design that survives growth. A benchmark then picks between implementations of the same class, and checks that the analysis was right.

Big-O vs a Benchmark

Big-O predicts how cost changes as the input grows and says nothing about the time at any one size. Use it to choose between designs and to predict next year.

A benchmark reports the time at one size on one machine. Use it to choose between two implementations of the same class, and to check the prediction. Big-O without a benchmark misses constants; a benchmark without Big-O misses growth.

Misconceptions
  • "O(1) means fast." It means the cost does not grow with n. A dictionary lookup that hashes a 10-megabyte key is O(1) in the size of the dictionary and still reads 10 megabytes, and a network call is O(1) and costs about a million times more than an addition.
  • "Big-O tells you which algorithm is faster." It tells you which one wins eventually. Below the crossover, a lower class with a large constant loses, and for twenty items a plain scan of a list routinely beats building a set to look things up once.
  • "O(n) means the worst case." O is a bound on a function. Whether that function describes the best, worst, average or amortized case is a separate choice, and "hash lookup is O(1)" is an average-case statement.
  • "Two nested loops always mean O(n²)." The class comes from how many times each loop runs. An outer loop over n records and an inner loop over 26 letters is O(n), and loops over two different inputs are O(n × m), which is not n² when m is ten.
  • "Dropping constants means constants do not matter." They are dropped from the class because they depend on the machine, not because they are small. A factor of fifty between a Python loop and a compiled one is often the whole performance budget.
Why It Matters
  • Write the class of every hot path next to it in the design, with n named. "O(n) in catalogue size" is a claim someone can check.
  • Prefer the lower class when n is large or unbounded, and measure when n is small and fixed. Growth wins at scale; constants win below the crossover.
  • Count a loop by its trip count, not its indentation. Nested loops over different or fixed-size inputs are not n².
  • Say which case an O refers to when it matters. Average-case O(1) and worst-case O(n) describe the same hash table.
RelatedAsymptotic analysis the mathematics Big-O comes fromBenchmarks measure the constant Big-O discardsAmortized analysis a way of charging cost, not a different notation

Knowledge Check

An O(n) job takes 3 minutes on today's data. The data is about to double. Roughly how long should the job take afterwards?

  • About 12 minutes, four times as long as today
  • About 6 minutes, twice as long as today
  • About 3 minutes, the same as it is today
  • About 3 and a half minutes, slightly longer

A function loops over n records and, for each one, loops over the 26 letters of the alphabet. What is its class?

  • O(n²), because it contains two nested loops
  • O(n), because the inner loop runs a fixed 26 times
  • O(1), because 26 is small next to any real input
  • O(n log n), because the alphabet acts like a log factor

Which statement about an O(1) operation is true?

  • It is guaranteed to finish in a few nanoseconds on any machine
  • It is always faster than any O(n) operation, whatever the size
  • Its cost does not grow with n, but that cost can still be large
  • It uses no extra memory, because its cost does not depend on n

Lantern's catalogue grows from 2 million to 20 million records. What happens to the cost of a scan and of an index lookup for a rare surname?

  • The scan costs ten times more; the lookup costs about the same
  • Both cost about ten times more, because both depend on the catalogue
  • The scan costs a little more; the lookup costs exactly the same
  • The scan costs ten times more; the lookup rises by about log n

A sort that is O(n log n) loses to an O(n²) sort on a list of twelve items. What is the best explanation?

  • The Big-O analysis of the faster sort must have been done incorrectly
  • Big-O drops constants, and below the crossover the smaller constant wins
  • The interpreter slows down n log n code more than it slows down n² code
  • Big-O is defined only for inputs of a thousand items or more, not twelve

You got correct