This site has eleven comparison sorts now — Insertion Sort, Bubble Sort, Selection Sort, Shell Sort, Heap Sort, Quicksort, Merge Sort, Timsort, Introsort, Bitonic Sort, and Cycle Sort — the largest single category on the site, and the one where "which one do I use" is the most ordinary of questions: every one of these sorts an array by comparing pairs of elements, and none of them needs its own page to justify existing before a reader picks. Seven of the eleven are the classic textbook sorts, compared below by the questions that actually decide between them; Timsort, Introsort, Bitonic Sort, and Cycle Sort are the odd ones out. The first two are real production hybrids built from ingredients this page already covers rather than a competing approach in their own right; the third isn't a hybrid at all, it's a different paradigm — a fixed comparator schedule instead of anything the other ten would recognize as an algorithm's control flow. The fourth trades away speed entirely for a property none of the other ten optimize for: the fewest possible writes to the array. All four get their own section instead of being forced into the same funnel.
In order, cheapest to check first: Is n small, or already nearly
sorted? If so, this settles it before anything else below matters. Does the
relative order of equal elements need to survive the sort (stability)? Some real sorts —
sorting rows by one column while a prior sort already ordered them by another — depend on this and
break silently if it's missing. If stability doesn't force the answer, is a worst-case
guarantee required, or does typical-case speed matter more? This is the classic
heap sort vs. quicksort trade, and it's the last question, not the first, because it only matters
once the first two have already been ruled out.
Insertion sort is O(n²) worst and
average case, but its inner loop bails out the moment it finds the right slot — on already-sorted
input that's O(n), and the same adaptivity pays off on any array that's only slightly
out of order, no matter how large n is. That's not a special mode you opt into: it
falls out of the algorithm's own structure, which is exactly why several production sort
implementations (V8's Array.prototype.sort, Timsort) switch to insertion sort for
small sub-arrays regardless of what the outer algorithm is.
Bubble sort looks like the same trade at first glance
— also O(n²) worst and average, also stable — but its O(n) best case only
exists with an explicit early-exit flag bolted on; bubble sort's own pitfalls confirm that without it,
an already-sorted 7-element array still runs all 6 passes and 21 comparisons, the same as a
worst-case input. Insertion sort gets its adaptivity for free from the inner loop's own condition;
bubble sort has to be told to look for it. There's no input where bubble sort is the better choice
over insertion sort — its page exists to make that contrast concrete, not because it's a live
candidate.
n isn't small: merge sortStability rules out more of the seven than it might seem. Selection sort's long-range swap can jump one equal
element clean over another — confirmed on the tagged pair [3♥, 5, 3♦, 1], which comes
out [1, 5, 3♦, 3♥], order flipped. Shell
sort loses stability the moment a gap exceeds 1, since it compares elements that aren't
adjacent and can leapfrog an equal one sitting between them. Heap sort's sift-down swaps and quicksort's partition swaps both move elements long distances
with no tie-breaking rule. That leaves insertion sort, bubble sort, and merge sort — and once
n is past the range where insertion sort's quadratic worst case is a real cost, merge sort is the one built for scale: a deliberate
<= in its merge step keeps left-half elements ahead of equal right-half elements,
preserving original order, while still guaranteeing O(n log n) in every case — best,
average, and worst alike. The cost is O(n) extra space for the merge buffers, unlike
every other sort on this page. That combination — stable, guaranteed n log n, no
quadratic worst case — is why merge sort (or a hybrid built on it, like Timsort, covered in its own section below) backs the stable
sort in most standard libraries.
Once stability is off the table, the choice comes down to a guarantee against typical-case
speed. Heap sort is O(n log n) in every
case — no adversarial input pushes it worse, because extracting from a heap costs
O(log n) regardless of the array's original order — at O(1) extra space.
Quicksort is O(n log n) only on average; this
site's last-element pivot rule degrades to O(n²) on an already-sorted or
reverse-sorted array, confirmed directly by pasting 1,2,3,4,5,6,7,8 into its own demo.
Randomized or median-of-three pivots make that worst case unlikely to hit by accident, but neither
eliminates it — quicksort's own pitfalls are
explicit that "strong practical default" and "asymptotically solved" are different claims. In
exchange, quicksort partitions in place with better cache locality than heap sort's
far-apart sift-down jumps, and is typically faster in practice despite the weaker guarantee.
Real standard-library sorts mostly refuse to pick just one of these three. Introsort — behind C++'s std::sort — runs
quicksort by default, falls back to heap sort only once recursion depth signals a bad pivot
pattern, and switches to insertion sort below some small n for the same reason as
the previous section. The three sections above aren't really competing answers; they're the
ingredients a hybrid picks between at different sizes and different moments, and this site's own
pages implement each ingredient standalone specifically so each one can be read and verified on
its own. Introsort's own page is where that recursion-depth switch is built and made to actually
fire, rather than just described; Timsort, covered next,
is this site's other worked example of the same pattern — a hybrid built from ingredients this
guide already covers.
Selection sort always makes exactly
n(n-1)/2 comparisons no matter the input — no adaptive best case at all — but at most
n - 1 swaps total, one per pass, and often fewer. That's the one real reason to reach
for it: when writes are far more expensive than comparisons (flash memory wear leveling, large
records with a cheap-to-compare key), selection sort's write count is bounded by how many elements
are actually out of place, a guarantee none of the other six on this page make.
Shell sort generalizes insertion sort by shifting at
wide gaps before narrowing to a final gap-1 pass, trading some of insertion sort's quadratic risk
for a sequence-dependent worst case — this site's halving sequence still degrades to
O(n²) on an adversarial input, and no known gap sequence has a proven
O(n log n) bound, only better empirical or amortized ones (Hibbard's reaches
O(n^1.5)). It sorts in place at the same O(1) space as insertion and
heap sort, without heap sort's cache-unfriendly jumps or merge sort's buffer — a real middle
ground, but an unproven one, which is exactly why shell
sort's own page describes its reputation as "good enough, rarely the right choice when
something else is available."
Timsort isn't a candidate answer to the three questions
at the top of this page — it's what you get by taking "stability required, n isn't
small" seriously and then also checking whether the input has real-world structure before doing
merge sort's full O(n log n) work regardless. It detects existing ascending or
descending stretches (natural runs), extends short ones with binary-insertion sort, and merges the
rest with the same balance-preserving discipline that gives merge sort its guarantee. On an
already-sorted 1,000-element array it does the whole job in 1,000 comparisons to merge sort's 4,932, measured,
not estimated — and on data with no structure at all, or disorder scattered too thin for run
detection to notice, it's measurably no better than merge sort, sometimes very slightly worse. It's
the site's one production-grade hybrid with its own page instead of just a mention, exactly because
that adaptivity is a real, checkable claim rather than a description of one.
Introsort — behind C++'s std::sort — is
also not a candidate answer to the three questions above; it's what "typical-case speed matters
more than a worst-case guarantee," the answer the unstable section gives
quicksort, looks like once someone also insists on a guarantee for the rare case quicksort gets
unlucky. It runs quicksort by default, counts its own recursion depth as it goes, and falls back to
heap sort — for whichever range triggered it, not the
whole array — the moment that count passes 2 · floor(log2(n)), a threshold that scales
with input size rather than a fixed constant. Below a small range size it switches to insertion sort instead, for the unrelated reason
the niche-picks section already gives it: recursion overhead costs more than a
few shifts once a range is small enough. Where Timsort combines two ingredients toward one goal
(stability, kept cheap when the input already has structure), Introsort combines three toward a
different one: quicksort's average speed, with heap sort's guarantee as a safety net that measurably
almost never fires — zero times in 1,000 seeded trials on shuffled data, but reliably once on the
exact adversarial input that breaks plain quicksort, both measured on Introsort's own page.
Bitonic Sort doesn't answer any of the three
questions above either, but for a different reason than Timsort or Introsort: it isn't trying to
win on typical-case speed, an adaptive best case, or even a matching worst-case bound — the five
entries built for exactly that (heap sort, quicksort, merge sort, Timsort, Introsort) all reach
O(n log n) or better on average, and Bitonic Sort's O(n log² n)
total comparator count is strictly worse than any of them. What it buys instead is a property none
of the other ten have: the exact sequence of index pairs it compares is fixed by the array's
length alone, computed before a single element is inspected, and measured identical — not just
similar — across sorted, reverse-sorted, and random input of the same size (24 comparators at
n=8, every time, on Bitonic Sort's
own page). That's the one property every other sort on this page actively works against:
quicksort's partition, Timsort's run detection, and insertion sort's adaptive inner loop all
change their behavior based on what they find, which is exactly what makes them unsuitable for
hardware where every parallel lane has to execute the same instruction stream regardless of the
data it's holding.
Cycle Sort shares Bitonic Sort's shape of answer —
it isn't competing in the funnel above either — but optimizes for a completely different resource.
Where Bitonic Sort spends extra comparisons to buy a fixed schedule, Cycle Sort spends nothing
extra on comparisons at all (it's O(n²), the same floor selection sort sits at) and instead minimizes
writes: every element lands in its final array slot at most once, the provable minimum
for any in-place comparison sort, measured directly against selection sort on the same inputs on
Cycle Sort's own page (selection sort wrote
strictly more in 98.2% of 3,000 trials, never fewer). None of the other ten entries on this page
even track writes as a separate cost from comparisons — Cycle Sort is the only one for which the
distinction is the entire point.
| Entry | Time (worst) | Space | Stable? | Reach for it when |
|---|---|---|---|---|
| Insertion Sort | O(n²), O(n) best case | O(1) | yes | small n, or already nearly sorted |
| Bubble Sort | O(n²), O(n) best case with early exit | O(1) | yes | teaching the contrast with insertion sort — not a live candidate |
| Selection Sort | O(n²), no best case | O(1) | no | writes are much more expensive than comparisons |
| Shell Sort | O(n²) with the halving sequence, no sequence has a proven O(n log n) bound | O(1) | no | O(1) space needed, insertion sort's worst case is too risky, heap sort's cache pattern isn't wanted |
| Heap Sort | O(n log n), guaranteed | O(1) | no | a worst-case bound is required and O(1) space matters |
| Quicksort | O(n²) (average case O(n log n)) | O(log n) average | no | typical-case speed matters more than a worst-case guarantee |
| Merge Sort | O(n log n), guaranteed | O(n) | yes | stability is required and n isn't small |
| Timsort | O(n log n), guaranteed | O(n) | yes | same guarantee as merge sort, plus real-world data that's already partly sorted |
| Introsort | O(n log n), guaranteed | O(log n) | no | same speed as quicksort, plus a worst-case guarantee, stability not required |
| Bitonic Sort | O(n log² n), always — same for every input of a given size | O(n) | no | parallel/hardware execution needs a fixed, data-independent comparator schedule |
| Cycle Sort | O(n²), always | O(1) | no | writes are much more expensive than comparisons, and the fewest-writes guarantee must be exact |
The table above is asymptotic notation — worth seeing the actual numbers behind it. This runs
the exact reference implementation from each of the eleven pages above, unmodified, in your own
browser, on one randomly generated array shared across all eleven, timed with
performance.now(). It's not a rigorous benchmark — JIT warmup, garbage collection,
and whatever else your browser is doing all add noise — but the gap between O(n log
n) and O(n²) shows up clearly enough at a few thousand elements that the noise
doesn't matter. Watch Bitonic Sort specifically: it registers as the slowest or near-slowest entry
at every size and every data pattern, not a fluke but the direct, measured consequence of the
extra total comparators the section above already names — and it's the one
entry whose time doesn't move at all when you switch data to already
sorted or reverse sorted, for the same reason. Cycle Sort's time moves the opposite
way selection sort's does: selection sort's cost is a function of n alone, flat
across every pattern, while Cycle Sort gets measurably slower from sorted to reverse-sorted to
random — sorted data needs zero of the position-recomputing rescans that fire on every displaced
element, so it's the one pattern where Cycle Sort's own extra bookkeeping mostly disappears.
Switch data to already sorted at 5,000 elements and watch quicksort
specifically: its reference implementation uses a
last-element pivot with no randomization, so on that adversarial input its recursion depth runs
n deep instead of log n — the exact pitfall the
section above describes, now timed instead of just claimed. Introsort runs the identical partition scheme on the same
input but doesn't melt down with it: at n = 5,000 its depth counter reaches zero
after only 24 levels (2 · floor(log2(5000))), at which point the remaining
~4,976-element range gets handed to heap sort instead of another partition — visibly the fastest
or near-fastest entry on this exact input that breaks quicksort, not by luck but by the same
mechanism Introsort's own Pitfalls section
measures directly. Insertion sort and bubble sort do
the opposite: both drop to sub-millisecond, insertion sort's adaptive inner loop and bubble sort's
early-exit flag each paying off at once. Switch to reverse sorted and insertion and bubble
sort become the slow ones instead — every element is as far from its final slot as it can be, so
neither algorithm's best case triggers. Timsort matches insertion and bubble sort's speed on both
sorted patterns for a different reason than either of them — not an adaptive inner loop or an
early-exit flag, but run detection finding the whole array is already one run and skipping the
merge phase entirely, exactly as measured on Timsort's own page.
None of these eleven beat O(n log n) — they can't (or, for Bitonic Sort and Cycle
Sort, don't even try to), because every one of them is built entirely out of "is A bigger than
B?" comparisons, and
that's a proven floor for any sort that only asks that question. Non-Comparison Sorts escapes the bound instead of beating it,
by never comparing elements at all — counting occurrences or reading digits instead — worth a look
once this page's guarantee starts to feel like a hard floor rather than one family's tradeoff.
This started as the site's third guide, comparing seven textbook comparison sorts by the questions that decide between them; Timsort joined later as an eighth entry and a different kind of answer — not one more competing option in the funnel above, but a real hybrid built from two of the other seven, with its own page and its own measured numbers rather than just a mention. Introsort joined as a ninth entry the same way — a different hybrid, built from three ingredients instead of two, aimed at a guarantee rather than adaptivity. Bitonic Sort joined as a tenth entry and a different kind of answer again — not a hybrid of anything already here, but a fixed comparator network with no data-dependent branching at all, trading total work for exactly the property every other entry on this page treats as an optimization opportunity. Cycle Sort joined as an eleventh entry and yet another different kind of answer — not a hybrid, not a fixed network, but a different metric entirely: the fewest possible writes instead of the fewest possible comparisons. See the journal for session notes on all five.