coldwa.st
All guidesProgrammingWebDataToolsDatabasesHaskellConceptsCabal & buildsToolchainCompilerPerformanceEditor & HLS

Programming · concepts · algorithms

What is Big O notation?

By ColdwastUpdated Jul 14, 20268 min read#algorithms#complexity#concepts
Close-up of colorful programming code on a screen
Big O is not about how fast one machine runs your code - it is about how the running time grows as the input gets bigger.

Big O notation is a way to describe how an algorithm's running time (or memory use) grows as its input gets larger. It answers one question: when the amount of data doubles, or grows a thousandfold, does your code stay fast, slow down a little, or grind to a halt? It deliberately ignores exact seconds and machine speed, and focuses on the shape of the growth - because that is what decides whether code that works on 100 items still works on 100 million.

Why not just measure the time?

Because seconds depend on the machine, the language, the compiler and what else is running. Run the same code on a laptop and a server and you get different numbers. Big O throws all of that away and measures something more durable: how the work scales with the input size, usually called n. Two algorithms can both take "a few milliseconds" on small input, yet one stays quick at scale while the other becomes unusable. Big O is how we tell them apart before we hit that wall.

The common complexities, best to worst

These are the growth rates you will meet most often, from the fastest-scaling to the most dangerous:

NotationNameRough idea
O(1)ConstantSame work no matter the input size - e.g. reading array[5].
O(log n)LogarithmicHalves the problem each step - e.g. binary search.
O(n)LinearWork grows in step with the input - e.g. scanning a list once.
O(n log n)LinearithmicThe best you can do for general sorting - e.g. mergesort.
O(n²)QuadraticA loop inside a loop - fine for 100 items, painful for a million.
O(2ⁿ)ExponentialDoubles with each added element - only usable for tiny inputs.
O(n!)FactorialEvery ordering of the input - practical only for very small n.

Why constants and small terms are dropped

Big O cares about growth, not exact counts, so it simplifies. If an algorithm does 2n + 3 steps, that is written O(n): the 2 and the 3 do not change the shape of the curve as n gets large. Likewise n² + n becomes O(n²), because the quadratic term dominates once n is big. This is why Big O is about the dominant term - the part that decides behaviour at scale - and not a precise stopwatch reading.

A vintage mechanical stopwatch with a black dial
Big O ignores the stopwatch. It measures how the work grows with the input, not how many seconds one run takes.

A concrete example

Say you want to check whether a list contains a duplicate. One approach compares every item with every other item - two nested loops, roughly n × n comparisons, so O(n²). Another approach adds each item to a hash set and checks membership as it goes - one pass, so O(n). On 1,000 items the quadratic version does about a million comparisons; on 100,000 items it does ten billion, while the linear version does 100,000. Same task, wildly different fate at scale. That gap is exactly what Big O is built to expose.

Worst, average and best case

Strictly, Big O describes an upper bound - usually the worst case, the most work the algorithm could do. You will also see two relatives: Big Omega (Ω) for the best case, a lower bound, and Big Theta (Θ) when the best and worst grow at the same rate, giving a tight bound. In everyday use people say "Big O" loosely to mean the worst-case growth, because that is what you plan around: you want to know how bad it can get, not how lucky you might occasionally be.

Space complexity, too

The same notation describes memory, not just time. An algorithm that copies the whole input into a new structure uses O(n) extra space; one that rearranges the input in place uses O(1) extra space. On large data, memory can be the real limit, so it is worth asking both "how does the time grow?" and "how does the memory grow?"

How to reason about your own code

  • Count the loops over the input. One pass is usually O(n); a loop inside a loop over the same data is usually O(n²).
  • Halving is a logarithm. If each step throws away half the remaining data (like binary search), that is O(log n).
  • Lookups in a hash map/set are ~O(1). Replacing a nested scan with a hash lookup is the classic way to turn O(n²) into O(n).
  • Drop the constants. Three separate single passes is still O(n), not O(3n) - the shape is what matters.

FAQ

Is a lower Big O always faster? At scale, yes - but not always for small inputs. Constants and overhead that Big O ignores can make an O(n²) method beat an O(n log n) one on a handful of items. Big O tells you who wins as the data grows, which is usually what matters.

What is the best possible complexity? O(1), constant time - the work does not grow with the input at all. Next best is O(log n). For problems that must look at every item at least once, O(n) is the floor.

Why is O(n log n) special for sorting? It is the proven lower bound for general comparison-based sorting, so mergesort, heapsort and good quicksort implementations all aim for it. You cannot generally sort arbitrary data faster by comparisons alone.

Do I need Big O for everyday coding? You do not need the formal maths, but the intuition is invaluable: recognising when a nested loop will explode on large input, or when a hash lookup saves you, prevents whole classes of performance bugs.

Independent, community-maintained guide. coldwa.st is a programming-resources site; this explainer covers Big O notation at an introductory level. Complexities and examples are standard computer-science results.