Counting

One run, four counts, four answers

The question “how many operations” has no answer until the operation is named. Selection sort makes more comparisons than any other algorithm here and fewer writes than almost all of them; bubble sort matches its comparisons and does 124 times the swapping. The ranking depends entirely on which count is chosen, and the choice needs justifying.

Selection sort and bubble sort perform almost exactly the same number of comparisons. At n = 512 on random input the counts are 130,816 and 129,688 — within one percent, and they stay within one percent at every size. By the measure that every introductory course uses to rank sorting algorithms, they are the same algorithm.

They are not the same algorithm. Bubble sort performs 62,563 swaps in that run. Selection sort performs 504.

That is a factor of 124, it is not visible in the comparison count, and if the things being sorted are anything larger than a machine integer it is the number that decides which algorithm was the right one.

Comparisons and swaps at n = 512Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort63,071 / 0Selection sort130,816 / 504Bubble sort129,688 / 62,563Merge sort3,964 / 0Heapsort7,653 / 4,170Quicksort5,049 / 2,380n = 512, random inputcounted in the same run
Fig. 1 Comparisons and swaps at n = 512, from the same runs. The two bars in each row are the same algorithm measured two ways. Ordering by the blue bar and ordering by the orange bar produce different orders — which is the entire content of this essay, and the reason the site records four counts rather than one.

The four counts

Every algorithm here runs against an instrumented array whose primitives tally four things:

  • comparisons — how many times two elements were asked which was larger
  • swaps — how many times two elements exchanged places
  • reads — how many times an element was fetched
  • writes — how many times an element was stored

At n = 512 on random input, in full:

algorithm comparisons swaps reads writes
insertion sort 63,071 0 126,145 63,074
selection sort 130,816 504 262,640 1,008
bubble sort 129,688 62,563 384,502 125,126
merge sort 3,964 0 12,536 4,608
heapsort 7,653 4,170 23,646 8,340
quicksort, median of three 5,049 2,380 10,997 4,760
Shellsort 5,840 0 11,928 6,088
merge sort with cutoff 4,794 0 12,221 4,920

Several things in that table are worth more than a glance.

Insertion sort records zero swaps. It is not standing still: it moves a great deal of data — 63,074 writes — but it does so by shifting elements one position with get and set rather than by exchanging pairs. The swap counter is measuring a particular way of moving data, not movement in general, and an algorithm that avoids the primitive shows up as zero.

That is a real limitation of the count and it is worth stating rather than papering over. To compare data movement across algorithms that move data differently, the honest column is writes, not swaps. By that column insertion sort at 63,074 is one of the heaviest movers here, and selection sort at 1,008 is by far the lightest.

Selection sort’s write count is the whole point of the algorithm. n1n - 1 swaps at most, so at most 2(n1)2(n-1) writes, regardless of the input. Nothing else here comes close. Selection sort is the algorithm for the case where writes are catastrophically expensive and reads are cheap — sorting records on media where a write costs orders of magnitude more than a read, which is not the situation most people are in but is exactly the situation the algorithm was designed for.

Merge sort’s reads exceed its writes by nearly three to one. 12,536 against 4,608. Each merge reads both halves and writes the result once, but the comparison primitive charges two reads per comparison, and the copy back from the buffer costs a read and a write each. The asymmetry is structural rather than incidental.

Which count is the right one

There is no answer to that in general, which is the useful thing to know. There is an answer for each situation, and it comes from what the elements are.

Machine integers, in memory. Comparisons and writes cost about the same — a few cycles each — and both are dominated by whether the data is in cache. Here the comparison count is a reasonable proxy for total work and the ranking it gives is roughly right. This is the case most textbooks silently assume.

Large records moved by value. A comparison touches one field; a write moves the whole record. If a record is 200 bytes and a key is 4, a write is fifty times a comparison and the write column is the one that matters. Selection sort’s 1,008 writes at n = 512 start to look extremely attractive against insertion sort’s 63,074, despite selection sort making twice as many comparisons.

Expensive comparisons. Sorting strings by a locale-aware collation, or sorting objects by a computed key, or sorting anything where the comparison is a function call into user code. Now a comparison may cost hundreds of times a write, and the comparison column is not merely a proxy — it is the answer, and everything else is noise. This is why the standard library sorts of most languages work hard to minimise comparisons specifically, and why the information-theoretic floor on comparisons is a bound anyone in this situation genuinely cares about.

Data on disk or across a network. None of these counts is the right one. What matters is the number of blocks transferred, which is a different quantity again, and which the cache model is a small-scale analogue of.

The pattern is that the choice of cost model determines the answer, the choice is a modelling decision rather than a measurement, and it is almost always made implicitly. A benchmark that reports “algorithm A is better” has chosen a cost model — usually by choosing what to sort — and the choice is rarely stated.

The counts rank differently, and it is not a small effect

Take the eight algorithms in the table and rank them by each count.

By comparisons, best to worst: merge with cutoff, merge, quicksort, Shellsort, heapsort, insertion, bubble, selection.

By writes, best to worst: selection, merge, quicksort, merge with cutoff, Shellsort, heapsort, insertion, bubble.

Selection sort goes from last place to first. That is not a marginal reordering caused by measurement noise — it is a move across the entire field, produced by changing which exact, deterministic, reproducible number is being looked at.

The same effect appears within a single algorithm’s design decisions. Merge sort with an insertion cutoff performs 4,794 comparisons where plain merge sort performs 3,964: the hybrid does 21% more comparing. It also does 6.8% more writing. On this measure the cutoff is a straightforward loss, and yet every production sorting routine implements one, for reasons that turn out to have nothing to do with either count.

A case where the counts disagree about which algorithm is broken

The clearest demonstration is an input that most comparisons never use: an array with only a handful of distinct values. Sorting a million records by a status field with eight possible values is an entirely ordinary thing to want to do, and it is where several respectable algorithms come apart.

At n = 512 with eight distinct values, the comparison counts are:

algorithm random input few distinct values
insertion sort 63,071 55,019
merge sort 3,964 3,837
quicksort, median of three 5,049 19,718
quicksort, random pivot 4,921 17,855
quicksort, first element 4,887 17,990
Shellsort 5,840 3,980

Merge sort barely notices. Shellsort gets slightly faster. Every quicksort variant on the site gets roughly four times slower, and the fitted class moves from nlognn \log n to n2n^2 — which the site’s machinery detects and refuses to paper over, so none of these algorithms carries a declared class on this input.

The reason is that a two-way partition around a pivot equal to many other elements puts all those equal elements on one side. The recursion is unbalanced not because the pivot was unlucky but because the data has few distinct values, and choosing the pivot better — median of three, at random, any of it — does not help at all. Randomising the pivot defends against an adversarial arrangement of the input, and few-distinct-values is not an arrangement. It is a property of the values themselves, and no pivot rule can escape it.

The fix is a three-way partition, which puts elements equal to the pivot in a middle band and recurses on neither side of it. That is what production quicksorts do, and it costs comparisons on ordinary input to buy safety on this one — a trade that is invisible to anyone who only ever measures random arrays.

The two counts, four ways

The disagreement between the two counts is the claim, so the pair is drawn again at another size and on two more inputs.

Comparisons and swaps at n = 128Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort3,890 / 0Selection sort8,128 / 120Bubble sort8,083 / 3,766Merge sort735 / 0Heapsort1,402 / 796Quicksort964 / 490n = 128, random inputcounted in the same run
Fig. 2 A quarter of the array. Both counts fall and they do not fall by the same factor for the same algorithm.
Comparisons and swaps at n = 1024Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort264,216 / 0Selection sort523,776 / 1,014Bubble sort522,786 / 263,197Merge sort8,936 / 0Heapsort17,313 / 9,333Quicksort11,589 / 5,278n = 1024, random inputcounted in the same run
Fig. 3 And twice it. The ordering by comparisons and the ordering by swaps disagree at both sizes, about different algorithms.

Both of those hold the input fixed, and the input is what a count is most sensitive to — far more so than the size, which moves both counters together. The two plates below hold the size at five hundred and twelve and change the order the elements arrive in.

Comparisons and swaps at n = 512Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort574 / 0Selection sort130,816 / 25Bubble sort2,545 / 63Merge sort2,326 / 0Heapsort7,949 / 4,441Quicksort4,419 / 549n = 512, nearly sorted inputcounted in the same run
Fig. 4 Nearly sorted input at the original size, where insertion sort’s comparison count collapses and its swap count collapses further.

Reversed is the other end of that axis and the worst case for the algorithm the previous plate flattered, which makes the pair the sharpest version of the comparison available.

Comparisons and swaps at n = 512Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort130,816 / 0Selection sort130,816 / 256Bubble sort130,816 / 130,816Merge sort2,304 / 0Heapsort7,203 / 3,772Quicksort7,039 / 3,959n = 512, reversed inputcounted in the same run
Fig. 5 And reversed, its worst case. Four plates, two counters, and no plate on which one ordering is the other.

What none of these counts can see

Ordered by how much of the truth they capture, the counts form a ladder — and the top rung is not on it.

Comparisons capture the information-theoretic work. Writes capture the data movement. Reads capture the total traffic. And all three are blind to where the accesses go, which on real hardware is most of the cost.

Where each algorithm looks, and whenEvery array access from one run of each algorithm on 256 random elements: time along the horizontal axis, array index up the vertical. Merge sort makes 49% of its accesses to the next element or the same one; Heapsort makes 15%. That difference is invisible in the comparison count and is most of what the machine feels.Merge sort49% sequential · 7,540 accesses2560Heapsort15% sequential · 14,044 accesses2560time (accesses, left to right) · index (bottom to top)one run each, n = 256every access plotted
Fig. 6 Merge sort and heapsort, every array access from one run, plotted as index against time. Merge sort’s accesses sweep; heapsort’s scatter. Every count in this essay’s table is blind to this difference, and on a real machine it is the difference that decides which one finishes first.

Merge sort and heapsort are both Θ(nlogn)\Theta(n \log n). Merge sort does about half the comparisons. But the interesting number is neither: it is that merge sort makes 49% of its accesses to the next element or the same one, and heapsort makes 14%. Heapsort’s whole method is jumping between a node and its children at indices 2i+12i+1 and 2i+22i+2, which for large heaps are nowhere near each other in memory, and no amount of counting comparisons reveals that.

This is why the site carries a fifth quantity that is not in the table above — the complete access trace, which becomes a modelled miss count when replayed through a cache. That count is genuinely independent of the four here, and the site asserts that it is: if the miss count ever became predictable from the comparison count, the second quantity would be carrying no information and the assertion would fail the build.

The same test applied to the swap column gives the complementary answer: swaps predict nothing that writes do not, since every swap is two writes, and the column is retained because it distinguishes exchanging from shifting rather than because it measures a separate resource.

Two properties that no count sees at all

Two further things distinguish sorting algorithms and neither is a number.

Stability. A stable sort preserves the relative order of elements that compare equal. Merge sort as written here is stable; heapsort and quicksort are not. Stability decides whether sorting by one key and then by another gives a sensible result, which is how most multi-key sorting is done in practice. It costs nothing in comparisons and is invisible in every column above.

Auxiliary space. Merge sort needs a buffer the size of the input. Heapsort, insertion sort, selection sort and quicksort’s partition need a constant amount. That is a factor-of-n difference in memory, it appears in none of the four counts, and it is frequently the constraint that decides the choice. The site does not currently measure it, which is an honest gap: an in-place sort and an out-of-place one with identical counts are different algorithms and nothing here says so.

Both belong on the list of reasons that “which sorting algorithm is best” is not a question with a number for an answer.

What a good comparison looks like

If the counts disagree and there is no universally right one, comparisons between algorithms are not impossible. They just have to say what they are about.

A defensible statement looks like: merge sort performs 0.838 · n log₂ n comparisons on random input, against heapsort’s 1.613 — measured from n = 32 to n = 4,096, averaged over sixteen seeds. Every part of that is checkable, the cost model is named, the range is named, and the sample is named.

An indefensible statement looks like: merge sort is faster than heapsort. It might be true on some data on some machine, and nothing in it says which, so there is no way to find out whether it holds anywhere else.

This site tries to make only the first kind of statement. Where an essay says one algorithm does less work than another, the work is named. Where it says one is faster, something has been measured that entitles it to the word — and given that nothing here is timed, that word is used very sparingly indeed.

The counts are not independent, and the identity says something

Four columns look like four degrees of freedom and they are not. Three of them are tied together by an exact identity, and working it out turns the table into a description of how each algorithm is written.

Every read in these runs comes from one of two places: a comparison, which fetches its operands, or a swap, which fetches the two elements it is about to exchange. Every write comes from a swap or a shift. So

reads  =  ccomparisons  +  writes\text{reads} \;=\; c \cdot \text{comparisons} \;+\; \text{writes}

where cc is the number of array elements a comparison fetches — two if it compares two positions of the array, one if it compares a position against a value the algorithm is already holding.

Checked against the table, at n=512n = 512, it closes exactly on five of the eight rows:

algorithm cc c×c \times comparisons + writes reads
selection sort 2 261,632 + 1,008 262,640
bubble sort 2 259,376 + 125,126 384,502
merge sort 2 7,928 + 4,608 12,536
heapsort 2 15,306 + 8,340 23,646
insertion sort 1 63,071 + 63,074 126,145
Shellsort 1 5,840 + 6,088 11,928

Not approximately. To the unit, on every row.

And the two rows with c=1c = 1 are the two insertion-based algorithms, which is not a coincidence: insertion sort lifts the element being placed into a local variable and then compares that held value against array positions as it shifts them along. One array read per comparison, because the other operand is not in the array any more. Shellsort is insertion sort at several gap sizes and inherits the property exactly.

So the identity is a fingerprint of an implementation detail. A reader who had only the four counts and none of the code could recover, from arithmetic alone, which algorithms hold an operand in a register — and that detail is one of the standard hand-optimisations, usually described as saving a memory access and here visible as a coefficient.

The two rows where it does not close are the more interesting ones.

Quicksort with median-of-three leaves a residual of 1,188 reads over c=1c = 1. That number is exactly divisible by three, and three is how many elements the median-of-three rule samples per partition — so the residual is 396 partition calls at three reads each, and the identity closes once the pivot selection is accounted for. The extra reads are not comparisons and not swaps; they are the algorithm looking at candidates before deciding what to compare.

Merge sort with a cutoff sits between the two coefficients, because it is literally between the two algorithms: the merges compare two array positions and the insertion-sorted runs compare against a held key, so its reads are consistent with a mixture and with no single cc.

The general point is worth more than the arithmetic. A set of counters can look like several independent measurements and be two measurements plus bookkeeping, and the way to find out is to try to predict one column from the others. Where the prediction closes, the extra column is carrying no information and is a convenience. Where it fails — as it does for the modelled miss count, which no combination of these four predicts — the extra column is a genuinely new quantity, and that failure is the reason it was added.

The one thing that survives all of it

Amid all this relativity there is a fixed point, and it is worth ending on because it is the reason the comparison count keeps its privileged position despite everything above.

The comparison count is the only one of the four with a proved floor. No comparison sort averages fewer than log2(n!)\log_2(n!) comparisons, ever, for any input distribution, by any technique anyone will ever invent. There is no corresponding theorem about writes, or reads, or swaps: an algorithm that performs zero writes is conceivable if it is allowed to return a permutation rather than rearrange the array, and the write count therefore has no interesting lower bound at all.

So the comparison count is not merely one arbitrary choice among four. It is the count that connects to a theorem, and that is why measuring how close each algorithm gets to its floor is a more informative exercise than measuring the others. It is still not the running time, and it is still not the right count for every situation — but it is the one where the question “could anything do better?” has an answer.

There are now six

This essay reports four counts. The site has since added two more — modelled cache misses, and peak auxiliary space — and the pattern it describes holds across all six rather than being a curiosity of the original four.

Ranking the ten sorts by each of the six and counting the pairs ordered differently, comparisons and peak space disagree about 91% of pairs, which is close to a reversal. The full table is a separate essay, and its conclusion is this one’s, arrived at with a larger vocabulary: there is no ranking of sorting algorithms, there are six, and choosing between them is a statement about the data.

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 51 that link here.

The objects this essay names

Each one links to every other essay that touches it.

CacheComparison countCost modelCutoffData movementPartitionPivotQuicksortRankingStabilitySwapsTrade off