Counting

Fitting a class to measurements

A complexity class is normally read off the shape of the loops and written down. Here it is fitted to counts taken across three orders of magnitude, and an algorithm is granted a class only if the fit holds — which turns a statement about code into a statement that can fail.

Ask how anyone knows that merge sort is Θ(nlogn)\Theta(n \log n) and the answer is the recurrence. T(n)=2T(n/2)+nT(n) = 2T(n/2) + n, apply the master theorem, out comes nlognn \log n. It is correct, it is a proof, and it is about the recurrence rather than about the code that will run.

The gap between those two things is where the bugs live. A merge sort with an off-by-one in its merge loop has the same recurrence and the same proof and does not sort. A merge sort that accidentally copies the whole buffer on every recursive call has the same top-line class and does far more work. Nothing about reading the loops distinguishes these from the correct implementation, because reading the loops is how the class was arrived at in the first place.

So on this site the class is not read off the code. It is fitted to the counts the code produces.

Comparison counts against n, random inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10010³10⁴10⁵10⁶ncomparisonsInsertionMergeHeapsortQuicksorta power law is a straight line herecomparisons, counted exactly
Fig. 1 Measured comparison counts on logarithmic axes from n = 32 to n = 4,096. A power law is a straight line here and its exponent is the slope, so the shape of the plot is the class. The quadratic algorithm climbs at roughly twice the gradient of the other three, and the vertical gaps between the parallel lines are the constants.

Why a slope is not enough

The obvious method is to take logarithms of both axes and fit a straight line. The slope is the exponent, and the exponent names the class.

It half works. Fitted this way across n from 32 to 4,096 on random input, the site’s algorithms report:

algorithm fitted exponent
merge sort with cutoff 1.161
quicksort, median of three 1.189
merge sort 1.213
heapsort 1.221
quicksort, random pivot 1.244
Shellsort 1.249
insertion sort 2.004
selection sort 2.005
bubble sort 2.015

The quadratic algorithms come out at 2.00, which is exactly right and satisfying — and worth pausing on, because nothing about being quadratic makes three algorithms interchangeable. The linearithmic ones come out around 1.2, which is not 1 and not any other round number, and that is the problem.

nlognn \log n is not a power law. It has no exponent. Fitting a straight line to it on log–log axes produces the average slope over the fitted range, which is 1+1lnn1 + \frac{1}{\ln n}-ish and therefore drifts downwards as the range moves right. Fitted from 32 to 4,096 merge sort gives 1.213; fitted from 4,096 to 262,144 it gives something smaller. The number is real, it is reproducible, and it is not a property of the algorithm.

So the exponent is worth reporting — it cleanly separates the two families, and the gap between 1.25 and 2.00 is a chasm nothing falls into — but it cannot be the test.

Where the measurements are taken

Two decisions have to be made before any fitting happens, and both affect the answer.

The spacing. Sizes are spaced evenly on a logarithmic scale — 32, 54, 91, 152, 256, and so on — rather than evenly on a linear one. Linear spacing puts almost all the measurements at the large end, where the curve is nearly straight on log–log axes and every candidate class looks alike; log spacing puts equal weight on each decade, which is where the classes actually separate. The same reasoning is why the plots have logarithmic axes in the first place.

The range. Three orders of magnitude is the standard used here, and it is a compromise. Too narrow and everything fits everything: over a single doubling, nn, nlognn \log n and n1.2n^{1.2} are indistinguishable to any tolerance worth having. Too wide and the quadratic algorithms become unaffordable — selection sort at n = 100,000 is five billion comparisons, which is a minute of build time for one point on one curve.

Comparison counts against n, random inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10³10⁴10⁵10⁶10⁷ncomparisonsSelectionBubbleShellsortMergea power law is a straight line herecomparisons, counted exactly
Fig. 2 The same range, four different algorithms. Two quadratic and two linearithmic, and the separation is obvious by eye at these axes — which is the point of choosing them. Note that the two quadratic lines are nearly on top of each other: selection sort and bubble sort perform almost identical numbers of comparisons, and everything that distinguishes them lives in other counts entirely.

The ratio test

The test used here is simpler and stronger. For a candidate class f(n)f(n), compute ci=count(ni)f(ni)c_i = \frac{\text{count}(n_i)}{f(n_i)} at every measured size, and look at how much cc varies.

If the algorithm really is Θ(f)\Theta(f), then cc is bounded above and below by constants — that is precisely what Θ\Theta means — and across a range of sizes it should be roughly flat. If it is not, the ratio will drift monotonically in one direction and keep drifting.

The measure used is the spread: the largest cc divided by the smallest. A perfect fit has spread 1. The tolerance is 1.6, meaning the ratio may vary by up to 60% across three orders of magnitude before the class is refused.

Applied to merge sort’s counts against the six candidate classes:

candidate spread verdict
nlognn \log n 1.21 fits
nn 2.91 refused
n1.5n^{1.5} 3.89 refused
n2n^2 43.96 refused
logn\log n 155 refused
11 373 refused

That is not a close call. The correct class sits at 1.21 and the nearest wrong one at 2.91, a margin of nearly two and a half, and against the class it is most often confused with — quadratic — the margin is a factor of thirty-six.

Choosing the tolerance honestly

A tolerance chosen to make the tests pass is not a test. The number has to sit between two things that are themselves measured: the noise, and the smallest real failure.

The noise. A correct fit is not exactly flat, for reasons that have nothing to do with being wrong. Lower-order terms are real: merge sort’s cost is nlognn \log n plus terms in nn, and at n = 32 the lower-order terms are a substantial fraction of the total while at n = 4,096 they are not. Recursion base cases, integer rounding when a range is split, and the specific input all contribute. Across every class this site grants, the observed spreads run from 1.02 to 1.45.

The smallest real failure. The tightest non-fit available is not a wildly wrong class — those come in at three, forty, hundreds — but one of the two claims the fit refused. Bubble sort’s counts on nearly sorted input miss the linear class at 1.79, and the hybrid’s counts on reversed input miss nlognn \log n at 1.99.

The tolerance is 1.6, and it sits between 1.45 and 1.79. Those margins are not large, and pretending otherwise would be the same failure this whole method exists to avoid: the honest description is that the tolerance separates the cases measured here with a little room on each side, and that a genuinely marginal case would come down to a judgement the number cannot make on anyone’s behalf. Both margins are checked on every build, so loosening the tolerance enough to grant bubble sort linearity would break the check rather than quietly pass it.

The fit has to refuse things

An acceptance test that accepts everything is not a test, and this is the failure mode that hides best: everything downstream is written as though the check were working.

So the site’s gate does not only require the correct classes to be granted. It requires the wrong ones to be refused, explicitly and by name:

  • selection sort’s counts must fail the nlognn \log n fit
  • merge sort’s counts must fail the linear fit
  • insertion sort’s claim of linearity, true on sorted input, must fail on random input

If any of those started passing, every caption on the site would be unearned and nothing else would notice, because every figure would still draw a perfectly plausible curve.

Every sort measured on random input, and its claim testedThe exponent fitted to each algorithm's comparison count across n from 32 to 4096, beside the class it claims. The two groups separate cleanly — nothing measures between 1.3 and 1.9 — and every claim on this input is the class that actually fits. The fitted exponent for a linearithmic algorithm sits near 1.2 rather than 1.0 because n log n is not a power law.fitted exponent of the comparison count1.01.52.0Merge sort with a cutoff1.16n log nQuicksort, median of three1.19n log nQuicksort, first-element1.21n log nMerge sort1.21n log nHeapsort1.22n log nQuicksort, random pivot1.24n log nShellsort1.25n log nInsertion sort2.00n^2Selection sort2.01n^2Bubble sort2.01n^2n from 32 to 4096comparisons, counted exactly · random input
Fig. 3 Every sort put through the fit at once. The bar is the fitted exponent; the label beside it is the class the ratio test actually granted. Nothing measures between 1.3 and 1.9 — the two families separate with a gap wider than either family’s internal variation — and every claim on random input survives.

Two claims the fit refused

The mechanism only earns its place if it is capable of contradicting the person who built it. During construction it did so twice, and neither refusal was a bug.

Bubble sort on nearly sorted input. Bubble sort with the early-exit optimisation is widely described as linear on nearly sorted data, and the site’s algorithm table originally declared it quadratic there on the grounds that the optimisation cannot help enough. Measured, neither is right. Across n from 64 to 65,536 the counts divided by nn vary by a factor of 1.84 and divided by n2n^2 by a factor of 747. Linear is much closer and still outside tolerance.

The reason is structural, and it is the same reason adaptivity resists a clean classification generally. Bubble sort’s cost is roughly nn times the number of passes, and the number of passes needed depends on how far the most displaced element has to travel. In the site’s nearly-sorted generator the number of displaced elements is proportional to n, so the maximum displacement grows slowly with n too, and the cost is nn times something that creeps upward. No clean class describes it, and the fit says so.

The hybrid on reversed input. This one is sharper and gets its own essay. Merge sort with an insertion-sort cutoff, measured from n = 64 to n = 4,096 on reversed input, fits linear better than nlognn \log n — spread 1.69 against 1.92. Measured out to n = 65,536, the ranking reverses. Nothing changed but the range.

Comparison counts against n, reversed inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10⁴10³10⁴10⁵10⁶ncomparisonsMergeMergea power law is a straight line herecomparisons, counted exactly
Fig. 4 The refused claim, drawn over the range that settles it. The hybrid’s counts on reversed input are visibly not parallel to merge sort’s: the insertion-sort phase contributes a term proportional to n which dominates below a few thousand elements, and only past that does the merging start to show. Over the left two-thirds of this plot the line looks straight with slope 1. It is not.

Both claims were withdrawn from the algorithm table. That is what the machinery is for, and the alternative — adjusting the declaration to whatever the audience expects — would have made every other declaration on the site worthless.

It is worth noticing what each refusal cost. Neither algorithm changed. Neither is worse than it was. All that happened is that the site now declines to attach a two-symbol summary to two of the roughly forty algorithm-and-input pairs it measures, and says instead what the counts do. That is a small price, and the alternative price — every other declaration being a matter of taste — is not small at all.

Two more sweeps say what the fit is a statement about. Change the input and the same four algorithms move class; change the range and they do not.

Comparison counts against n, nearly sorted inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10010³10⁴10⁵ncomparisonsInsertionMergeHeapsortQuicksorta power law is a straight line herecomparisons, counted exactly
Fig. 5 The same four algorithms over the same range on nearly sorted input. Insertion sort’s line has flattened towards nn and the other three have not moved at all, because their comparison counts do not read the data. A fitted class is a class for an input distribution, and the notation carries no slot for one.
Comparison counts against n, random inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10⁴10010³10⁴10⁵10⁶10⁷10⁸ncomparisonsInsertionMergeHeapsortQuicksorta power law is a straight line herecomparisons, counted exactly
Fig. 6 And the original comparison taken eight doublings further. Every slope is where it was at n = 4,096 and the vertical gaps between the parallel lines are unchanged — which is what makes the fit a class rather than a local reading, and is the only evidence available that it will keep holding.

What the fit does not do

It does not prove anything.

This bears repeating in the middle of an essay about fitting, because the machinery is persuasive and the persuasion outruns the logic. A fit over three orders of magnitude establishes that the counts are consistent with a class over that range. Infinitely many functions are consistent with any finite set of measurements and diverge from each other beyond the last data point. No amount of measuring settles the question of what happens at infinity, which is the only question Θ\Theta is about.

What the fit can do is refuse. If an algorithm claims nlognn \log n and its counts fit n2n^2 with a spread of 1.03 across three decades, something is wrong — the claim, or the implementation, or the input assumptions — and finding out which is worth the trouble. Refutation is logically available to finite measurement in a way that confirmation is not, and the site’s machinery is built to exploit exactly that asymmetry.

There is a second thing it can do, which is to catch the discrepancy between what an implementation is supposed to be and what it is. This is the case the proof genuinely cannot reach. A proof about a recurrence establishes something about the recurrence; the fit is measuring the code that will actually run, and where those two diverge — because of a bug, an unintended copy, a base case that fires more often than expected — the measurement notices and the proof does not. That is not a criticism of proofs. It is a description of what they are about, and what the floors field does with the ones that are genuinely about every possible algorithm is a different and stronger kind of argument again.

Quicksort, first-element pivot on four kinds of inputThe same algorithm, the same range of n, four input distributions. The best and worst differ by a factor of 83 at n = 2048, so a single complexity class describes this algorithm only if the input is also stated.10010³10³10⁴10⁵10⁶ncomparisonsrandomnearly sortedalready sortedreversedthe textbook trap: quadratic on exactly the input people test withcomparisons, counted exactly
Fig. 7 Quicksort taking the first element as its pivot, on four inputs. On random data it fits n log n comfortably. On sorted or reversed data it fits n² — the partition is maximally unbalanced every time, and the recursion degenerates into a loop. One algorithm, two classes, and the difference is entirely the input.

The constant comes free

A useful side effect: once a class has been granted, the ratio cc is not just flat, it is a number. That number is the constant the notation throws away.

Merge sort’s comparison count divided by nlog2nn \log_2 n settles at 0.838. Heapsort’s settles at 1.613. Both are Θ(nlogn)\Theta(n \log n), and heapsort does 1.9 times as much comparing — while ranking the other way round on some counts and not others. That factor is invisible in the classification and is often the whole of what matters, which is why it gets an essay of its own.

The method in six lines

  1. Run the algorithm at sizes spaced evenly on a log scale, spanning at least three orders of magnitude.
  2. For each candidate class ff, compute count/f(n)\text{count}/f(n) at every size.
  3. Take the spread — largest divided by smallest.
  4. The best-fitting class is the one with the smallest spread.
  5. Grant the class only if that spread is under tolerance, and the tolerance sits demonstrably between the observed noise and the tightest real failure.
  6. Report the constant, because it was computed on the way.

None of this is difficult, and none of it is standard. The reason seems to be that the class is usually known before anyone thinks to measure — and once the answer is known, measuring feels like a formality. It is a formality right up until the moment something disagrees, which is exactly when it was needed — and the disagreements worth having are rarely about whether an algorithm is quadratic. They are about how close to the floor it gets, and about the constant nobody wrote down.

A drift is not a wobble

The spread is a range and a range throws away the one thing that separates a wrong class from a noisy right one: which way the ratio is moving.

A genuinely wrong class produces a ratio that drifts in one direction and keeps drifting, because the two functions have different growth and the gap between them widens without limit. Noise produces a ratio that moves up and down with no direction. Those look identical to a largest-over-smallest statistic and completely different on the plot.

So a sharper test is available: measure the trend rather than the range. Correlate the ratio against logn\log n and ask whether the relationship is monotone. A class whose ratio wobbles by a factor of 1.7 with no direction is a class the range test refuses and the trend test grants; one whose ratio climbs steadily by 1.3 is the reverse.

That would tighten both of the marginal cases this essay reports. Bubble sort’s ratio on nearly-sorted input at 1.84 is not noise — it climbs at every step, because the number of passes creeps up with nn — and a trend test would refuse it more decisively than a range test at a threshold of 1.6.

There is a difficulty and it is the reason the site uses the blunter instrument. A correct class also produces a trend, because lower-order terms are real: merge sort’s cost is nlognn\log n plus terms in nn, so its ratio against nlognn\log n falls monotonically as the lower-order terms become a smaller share. The trend is genuine and it is not evidence of a wrong class.

What distinguishes the two is whether the trend decays. A lower-order term’s contribution to the ratio shrinks like 1/logn1/\log n and the ratio approaches a constant; a wrong class’s contribution grows without bound. So the honest version of the trend test is a test on the second difference — is the drift flattening or is it not — and that needs more measured sizes than three orders of magnitude comfortably supply.

Which is why the site reports a range and states its margins rather than computing a trend and stating a p-value. The blunter statistic is defensible with the data available; the sharper one is the right instrument and would want a wider sweep than the quadratic algorithms can afford.

The best fit is best among the offered

One more property of the procedure deserves stating, because step four says the best-fitting class is the one with the smallest spread and that sentence has a silent qualifier in it.

There are six candidates. The winner is the best of six, not the best of all functions, and if the true behaviour is not in the list then the procedure returns whichever of the six is least wrong — with a spread that may or may not clear the tolerance, and with no indication that the vocabulary was the problem.

That is not hypothetical on this site. Randomised selection’s consumption of random bits grows like (logn)2(\log n)^2, which is not among the six; against the nearest offered class it lands at a spread of 1.98 and is refused, while against (log2n)2(\log_2 n)^2 it is flat to 1.14. The measurement is clean and the vocabulary cannot express it, so the class was withdrawn rather than granted — which is the right outcome and required somebody to notice that the refusal meant not in the list rather than not a class.

Two habits follow. A refusal is ambiguous — it says the counts do not fit any offered class, and the two reasons for that are a bad implementation and a short list. And the list should be reported alongside the verdict, because a reader who knows only that the fit refused everything cannot tell which of the two happened.

Enlarging the list is not free, either, and that is the reason it stays at six. Every extra candidate is another chance for a wrong class to fit by accident over a finite range, and the classes that are near-neighbours — nlognn\log n against n1.2n^{1.2}, say — are exactly the ones a three-decade sweep cannot separate. A small vocabulary of well-separated classes refuses more and mistakes less, and the price is a withdrawn claim now and then.

The same machinery, two variables

The procedure above fits a class in one size parameter, and a graph has two. The extension is small and it needs one thing the one-variable version does not: a claim is granted only when the sweep can also be shown to distinguish it from a named alternative.

The reason is that with two parameters, a single sweep collapses candidates that are genuinely different. Sweep the number of vertices at a fixed average degree and EE is proportional to VV, so VV, EE and V+EV + E become one curve and all three “fit” at a spread of 1.00 — which is a fact about the experiment rather than about the algorithm. The graph field is built on that distinction, and the assertion that enforces it refuses a real case rather than an imagined one.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 37 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Complexity classCurve fittingFalsificationPower lawQuicksortTolerance