Skip to content

Level 1 · Chapter 1.2

Algorithms and complexity (big-O)

How to measure an algorithm before running it: counting steps instead of seconds, growth rates and big-O notation, best, worst and average cases, why n log n beats n², and where the real machine bends the rules — with every count taken from live runs.

An algorithm is a precise, finite recipe that turns an input into an output: a list of names into a sorted list, a map into a shortest route, a number into its prime factors. Before any programming language gets involved, algorithms can be compared on one question: how much work do they do as the input grows?

Count steps, not seconds

Timing a program measures the program, the compiler and the machine all at once. To compare algorithms, we count their basic operations instead — comparisons, swaps, array reads — as a function of the input size n. The count doesn't depend on the CPU, and it shows how the cost grows. For large inputs, the growth is what matters.

Here are two ways to look for a value in a list of 32 numbers. Linear search checks each element in turn. Binary search only works on a sorted list: it looks at the middle, throws away the half that can't contain the value, and repeats.

Algorithms · Linear search
target= 18
0
comparisons
of 8 total
…
result
≤ n = 32 probes
0
writes
array elements
0
reads
array elements
comparedswapped / writtenfoundrange lo..hi

Initial array.

step 0 / 9

Linear search — Checks every element left to right until it meets the target. Best O(1), average O(n), worst O(n).

Cost vs n· comparisons, random input, target present
01002008163264128256n →log₂ nnn = 8: 7 comparisonsn = 16: 4 comparisonsn = 32: 8 comparisonsn = 64: 62 comparisonsn = 128: 127 comparisonsn = 256: 174 comparisons
measuredreference curves (unscaled: log₂ n, n)
comparisons at n = 256 — click to switch input
Algorithms · Binary search
target= 16
0
comparisons
of 2 total
…
result
≤ ⌊log₂ 32⌋ + 1 = 6 probes
0
writes
array elements
0
reads
array elements
comparedswapped / writtenfoundrange lo..hi

Initial array.

step 0 / 5

Binary search — Needs sorted input: each probe of the middle element halves the range lo..hi. Best O(1), average O(log n), worst O(log n). The generated array is sorted first.

Cost vs n· comparisons, random input, target present
02.557.58163264128256n →log₂ nnn = 8: 3 comparisonsn = 16: 2 comparisonsn = 32: 2 comparisonsn = 64: 5 comparisonsn = 128: 7 comparisonsn = 256: 7 comparisons
measuredreference curves (unscaled: log₂ n, n)
comparisons at n = 256 — click to switch input

Switch the target to absent — the worst case — and look at the cost vs n chart under each demo. Linear search makes one comparison per element: 8, 16, 32 … 256. Binary search halves the range at each step, so it needs 3, 4, 5 … 8 comparisons. Doubling the input adds one step. At a million elements, that's 20 comparisons against a million.

nlinear searchbinary search
883
32325
2562568
1,000,0001,000,00020

Big-O notation

Big-O describes how a cost grows, ignoring constant factors and small inputs. Formally, a cost f(n) is O(g(n)) if there are constants c and n₀ such that f(n) ≤ c·g(n) for every n ≥ n₀. In practice: keep the fastest-growing term and drop its coefficient. For example, 3n² + 5n + 20 is O(n²).

The classes you'll meet most often, with the time they'd take at n = 1,000,000 if each step took a nanosecond:

ClassNameExamplen = 10⁶
O(1)constantreading a[i]1 ns
O(log n)logarithmicbinary search20 ns
O(n)linearlinear search, summing an array1 ms
O(n log n)linearithmicmerge sort, heap sort20 ms
O(n²)quadraticinsertion sort, comparing all pairs17 minutes
O(2ⁿ)exponentialtrying every subsetlonger than the age of the universe

Each step down the table is a different world. No faster machine rescues an O(n²) algorithm on a large enough input, and a better algorithm often gains more than any hardware upgrade.

Best, worst and average case

The same algorithm can do very different amounts of work on inputs of the same size. Insertion sort takes each element and slides it left until it's in place:

Algorithms · Insertion sort on reversed input
0
comparisons
of 120 total
0
swaps
of 120 total
0
writes
array elements
0
reads
array elements
3230282624222018161412108642
comparedswapped / writtenin final placeactive range

Initial array.

step 0 / 256

Insertion sort — Grows a sorted prefix; each new element sinks left until it meets a smaller one. Best O(n), average O(n²), worst O(n²).

Cost vs n· comparisons, reversed input
010k20k30k8163264128256n →nn log₂ nn²n = 8: 28 comparisonsn = 16: 120 comparisonsn = 32: 496 comparisonsn = 64: 2016 comparisonsn = 128: 8128 comparisonsn = 256: 32640 comparisons
measuredreference curves (unscaled: n, n log₂ n, n²)
comparisons at n = 256 — click to switch input

On 16 reversed elements, every new element has to travel all the way to the front: 120 comparisons, that is n(n − 1)/2. Switch the input to sorted: 15 comparisons, one per element. On random input it's in between, 88 here. At n = 256 the gap becomes 255 comparisons for sorted input, 15,874 for random, and 32,640 for reversed.

So "insertion sort is O(n²)" is a statement about the worst case (and here also the average). Its best case is O(n), which is why it's excellent on data that's already almost sorted.

Worst cases hide in unexpected places. The quicksort in this visualizer picks the last element as its pivot. On random input it needs 2,325 comparisons for 256 elements, but on already-sorted input every partition is maximally lopsided, and it needs 32,640, as bad as insertion sort. Real libraries guard against this: they choose pivots more carefully and switch algorithms when a partition goes wrong. C++'s std::sort, for example, is usually an introsort, a quicksort that falls back to heap sort.

n log n versus n²

Sorting shows the gap between the two most common classes. On 256 random elements:

AlgorithmComparisonsGrowth
insertion sort15,874O(n²)
selection sort32,640O(n²), on every input
quicksort2,325O(n log n) on average
heap sort3,323O(n log n)
merge sort1,731O(n log n)
Algorithms · Merge sort
0
comparisons
of 123 total
0
swaps
of 0 total
0
writes
array elements
0
reads
array elements
comparedswapped / writtenin final placeactive range

Initial array.

step 0 / 315

Merge sort — Top-down: sort each half, then merge the two sorted runs through an auxiliary buffer. Best O(n log n), average O(n log n), worst O(n log n).

Cost vs n· comparisons, random input
05001k1.5k8163264128256n →nn log₂ nn²n = 8: 15 comparisonsn = 16: 43 comparisonsn = 32: 123 comparisonsn = 64: 307 comparisonsn = 128: 736 comparisonsn = 256: 1731 comparisons
measuredreference curves (unscaled: n, n log₂ n, n²)
comparisons at n = 256 — click to switch input

Merge sort splits the list in halves down to single elements, then merges sorted halves back together. There are log₂ n levels of splitting, and each level does about n work, so the total is O(n log n).

No comparison-based sort can do fundamentally better. There are n! possible orders, and each comparison has only two outcomes, so telling them apart takes at least log₂(n!) ≈ n log₂ n comparisons in the worst case. For 256 elements that's about 1,684 — and merge sort's 1,731 is close to it.

Where the real machine bends the rules

Big-O is the starting point, not the whole story:

  • Constants matter for small n. An O(n²) algorithm with a tiny constant beats an O(n log n) one on a few dozen elements. That's why real sorts — Timsort in Python and Java, introsort in C++ — switch to insertion sort on small ranges.
  • Memory access patterns matter. Two algorithms with the same count can differ hugely in time because one walks memory in order and the other jumps around. The caches chapter shows a 75% versus 0% hit rate on identical work.
  • Branches matter. A comparison whose outcome is random costs a misprediction about half the time; the same comparison on sorted data costs almost nothing.
  • Space is a cost too. Merge sort needs O(n) extra memory for merging. Heap sort and insertion sort sort in place, using O(1) extra.

Takeaways

  • Compare algorithms by counting basic steps as a function of the input size n, not by timing them.
  • Big-O keeps the fastest-growing term: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ).
  • The same algorithm has a best, worst and average case: insertion sort is O(n) on sorted input and O(n²) on reversed input.
  • Comparison sorting can't beat about n log₂ n comparisons, and merge sort, heap sort and (on average) quicksort reach that bound.
  • Constants, caches and branch prediction decide between algorithms of the same class.

In this level

  1. 1.1What is a computation?Planned
  2. 1.2Algorithms and complexity (big-O)
  3. 1.3Data structures: arrays, lists, trees, hash tables
  4. 1.4Searching and sorting
  5. 1.5From algorithm to codePlanned