Big O race
Big O notation describes how an algorithm's work grows with the size of its input. Here five real algorithms run on the same input, every operation they perform is counted, and they race on one clock that runs at the same speed for all of them.
Every lane does the same number of operations per unit of time: one per nanosecond on the clock above, roughly a few CPU cycles. To fit both 1 and 244,053 operations into a few seconds, the clock speeds up as it runs; a lane's bar shows the share of its own work done so far. The counts are for one random arrangement of the numbers 0 to n − 1; New random input draws another.
How to use the simulator
Pick an input size. The five tasks run on the same random input and count their operations: a single array read for the constant-time task, key comparisons for the others. Then the race replays those counts on one clock. Insertion sort's count is computed exactly from the input's inversions rather than by running it, because at a million items it would need about 249,999,750,000 comparisons; the count is checked against a real insertion sort in tests.
- At 10 items all five finish within a few dozen operations; the classes are hard to tell apart.
- At 1,000, insertion sort needs 244,053 comparisons and merge sort 8,707.
- At 1,000,000, binary search still needs only 20 comparisons, while insertion sort's lane would take minutes at a nanosecond per comparison.
What is Big O notation?
Big O notation is a way of describing how the cost of an algorithm, usually its running time or memory, grows as its input gets larger. Instead of counting exact steps, which depend on the language, the machine and the details of the code, it keeps only the part that dominates for large inputs. An algorithm that takes 3n + 50 steps and one that takes 100n steps are both O(n): doubling the input roughly doubles the work.
Formally, f(n) = O(g(n)) means there are constants c and n0 such that f(n) ≤ c · g(n) for every n ≥ n0. The constant c absorbs differences in speed per step, and n0 says that only large inputs count. That is what makes Big O useful for comparing algorithms, and also why it says little about small inputs.
Common complexity classes
The best way to read a complexity class is to ask what happens when the input doubles. The last column comes from the simulator's own counts, going from 10,000 to 20,000 items.
| Class | Name | Typical example | Work when n doubles |
|---|---|---|---|
| O(1) | Constant | Read the middle element | × 1.00 |
| O(log n) | Logarithmic | Binary search, value absent | × 1.07 |
| O(n) | Linear | Linear search, value absent | × 2.00 |
| O(n log n) | Linearithmic | Merge sort | × 2.17 |
| O(n²) | Quadratic | Insertion sort | × 4.03 |
| O(2ⁿ) | Exponential | Trying every subset | squared |
Other classes appear too: O(n³) for naive matrix multiplication, O(n!) for trying every ordering, O(√n) for trial division up to the square root, and O(log log n) for some specialised search structures. Hash table lookups are O(1) on average but can degrade with bad hashing.
The counts by input size
Operations counted by the simulator on its default input at each size:
| Task | n = 10 | n = 100 | n = 1,000 | n = 10,000 | n = 100,000 |
|---|---|---|---|---|---|
| Read the middle element O(1) | 1 | 1 | 1 | 1 | 1 |
| Binary search, value absent O(log n) | 4 | 7 | 10 | 14 | 17 |
| Linear search, value absent O(n) | 10 | 100 | 1,000 | 10,000 | 100,000 |
| Merge sort O(n log n) | 23 | 545 | 8,707 | 120,413 | 1,536,409 |
| Insertion sort O(n²) | 29 | 2,610 | 244,053 | 24,892,623 | 2,503,969,242 |
Merge sort's count stays below n log₂ n, and insertion sort's is close to n²/4, the average number of out-of-order pairs in a random arrangement. Going from 10 to 100,000 items multiplies binary search's work by 4.3 and insertion sort's by 86,343,767.
Constants and small inputs
Because Big O drops constant factors, a worse class can be faster on small inputs. Take two algorithms costing 3n + 50 and n²/100 steps. The quadratic one is cheaper until n = 315 and more expensive from n = 316 onward, and the gap then keeps widening.
Library sorts use this: Timsort, introsort and pattern-defeating quicksort all switch to insertion sort for short pieces. It is not that insertion sort makes fewer comparisons. Averaged over random inputs, it makes 5.0 comparisons at 4 items against merge sort's 4.7, and 72.4 against 45.7 at 16. It wins on everything else: no recursion, no buffer, a tight loop over adjacent memory. Counting operations is a model, and the model leaves out the cost per operation.
Big O, Big Omega and Big Theta
Big O gives an upper bound: a function that is O(n) is also, strictly, O(n²). Big Omega, Ω, gives a lower bound, and Big Theta, Θ, says both at once: the function grows at exactly that rate, up to constant factors. Merge sort is Θ(n log n); binary search is Θ(log n) in the worst case. Donald Knuth argued in 1976 for using Ω and Θ in this way in computer science, and that is now standard in textbooks, though in everyday speech "Big O" usually means the tight bound.
Worst, average and amortized cost
An algorithm can have different bounds for different inputs. Quicksort is O(n log n) on average but O(n²) in the worst case; insertion sort is O(n) on input that is already sorted but O(n²) on average. Unless said otherwise, a stated complexity usually means the worst case.
Amortized cost averages over a sequence of operations. Appending to a dynamic array, such as a Python list or a C++ vector, is usually cheap but occasionally copies every element into a larger array. If the capacity doubles each time, a million appends cause 1,048,575 element copies in total, about one per append, so appending is O(1) amortized even though a single append can be O(n).
Big O applies to memory as well as time. Merge sort needs O(n) extra space and heapsort O(1); a recursive algorithm needs stack space proportional to its recursion depth.
What is fast enough
Time taken at one operation per nanosecond, ignoring constant factors:
| Class | n = 1,000 | n = 1,000,000 | n = 1,000,000,000 |
|---|---|---|---|
| O(n) | 1.00 µs | 1.00 ms | 1.00 s |
| O(n log n) | 9.97 µs | 19.9 ms | 29.9 s |
| O(n²) | 1.00 ms | 16.7 minutes | 31.7 years |
| O(2ⁿ) | longer than the age of the universe | longer than the age of the universe | longer than the age of the universe |
Quadratic algorithms are fine for thousands of items and hopeless for billions. Exponential ones are only usable for inputs of a few dozen items, which is why problems whose best known algorithms are exponential, such as the travelling salesman problem, are solved with heuristics or approximations at scale.
History
The O symbol comes from number theory. Paul Bachmann introduced it in 1894, in the second volume of his Analytische Zahlentheorie, and Edmund Landau adopted and popularised it, so O and its relatives are also called Landau symbols. Hardy and Littlewood introduced a version of Ω in 1914. Computer scientists took up the notation to analyse algorithms in the 1960s, and Knuth's 1976 note "Big Omicron and big Omega and big Theta" fixed the definitions of Ω and Θ that computer science now uses.
Big O questions and answers
What is Big O notation?
Big O notation describes how an algorithm’s running time or memory grows as the input grows, ignoring constant factors and smaller terms. Saying an algorithm is O(n²) means that, for large enough n, its cost is at most some constant times n². It is used to compare how algorithms scale, not to predict exact running times.
What does O(n log n) mean?
The cost grows a little faster than the input: doubling n slightly more than doubles the work. Efficient comparison sorts such as merge sort are O(n log n). In this simulator, merge sort on 20,000 items makes 2.17 times as many comparisons as on 10,000.
Is O(n²) always slower than O(n log n)?
Not for small inputs. Big O ignores constant factors, so an O(n²) algorithm with very little work per step can beat an O(n log n) algorithm with a lot of overhead when n is small. That is why library sorts switch to insertion sort for short pieces. As n grows the O(n²) algorithm always loses eventually.
What is the difference between Big O, Big Omega and Big Theta?
Big O is an upper bound (grows no faster than), Big Omega a lower bound (grows at least as fast as), and Big Theta both at once (grows at exactly this rate, up to constants). Saying merge sort is Θ(n log n) is more precise than saying it is O(n log n), which would also be true of a linear-time algorithm. In everyday use, people often say Big O when they mean Big Theta.
What is amortized complexity?
The average cost per operation over a long sequence, even if a few operations are expensive. Appending to a dynamic array occasionally copies everything into a bigger array, but if the capacity doubles each time, a million appends cause 1,048,575 copies in total, about one per append, so each append is O(1) amortized.
What is O(log n) in practice?
The work grows by a constant amount each time the input doubles. Binary search on a sorted array of a million items takes at most 20 comparisons, and on a billion about 30. Balanced search trees and heaps have O(log n) operations for the same reason: each step halves what is left.
Further reading
- Knuth, SIGACT News, 1976Big Omicron and big Omega and big ThetaThe short note that settled how computer science uses O, Ω and Θ.
- Sedgewick and Wayne, Algorithms, 4th editionAnalysis of AlgorithmsMeasuring and modelling running time, with order-of-growth classifications.
- SemicolonySorting visualizerSix sorting algorithms, with comparisons and writes counted.