Skip to main content

Choosing a Sort

Overviewโ€‹

In almost every situation the correct answer is call your language's built-in sort. Those implementations are hybrids refined over decades, and they beat hand-written sorts on nearly every input. This page is about knowing what you are calling, and recognising the rare cases where the default is wrong.

What the standard libraries actually doโ€‹

LanguageFunctionAlgorithmStable
Pythonsorted, list.sortTimsortYes
JavaArrays.sort (objects), Collections.sortTimsortYes
JavaArrays.sort (primitives)Dual-pivot quicksortNo
C++std::sortIntrosortNo
C++std::stable_sortMergesort (or in-place mergesort if memory is tight)Yes
RustsortTimsort-derivedYes
Rustsort_unstablepdqsort (pattern-defeating quicksort)No
Goslices.SortpdqsortNo
Goslices.SortStableInsertion sort + symmergeYes
CqsortImplementation-defined; usually a quicksort hybridNo

Three designs cover almost all of that table.

Timsort โ€” adaptive mergesortโ€‹

Invented by Tim Peters for Python in 2002, and since adopted by Java, Android, Rust and V8. The premise is that real data is rarely random: it arrives partly ordered, appended to, or concatenated from sorted pieces.

Timsort scans for existing sorted runs, extends short ones using insertion sort, and then merges runs under rules that keep the merge tree balanced. On already-sorted input it finds one run and finishes in O(n).

PropertyValue
Best caseO(n) โ€” already sorted, or a handful of runs
Worst caseO(n log n)
SpaceO(n)
StableYes

Introsort โ€” quicksort that cannot degradeโ€‹

C++'s std::sort. Runs quicksort, but:

  • switches to insertion sort for ranges below ~16 elements;
  • switches to heapsort when recursion exceeds 2ยทlogโ‚‚ n levels.

The depth limit is what removes quicksort's O(nยฒ) worst case, without slowing the common path.

pdqsort โ€” pattern-defeating quicksortโ€‹

Rust's sort_unstable and Go's slices.Sort. Introsort plus pattern detection: it recognises already-sorted and reverse-sorted runs, uses three-way partitioning when duplicates are common, and breaks up adversarial patterns by shuffling deterministically when partitions come out badly. The result is O(n) on several common shapes while keeping introsort's guarantees.

How to Chooseโ€‹

SituationUse
Anything, by defaultThe built-in sort
Sorting by a secondary key after a primaryA stable sort
Hard real-time or adversarial inputHeapsort or introsort โ€” bounded worst case
Data larger than RAMExternal mergesort
Integer keys in a small known rangeCounting sort โ€” O(n + k)
Fixed-width keys (integers, dates, strings)Radix sort โ€” O(nw)
Fewer than ~16 elementsInsertion sort
Only need the top kA heap โ€” O(n + k log n)
Only need the median or k-th elementQuickselect โ€” O(n) average
Sort keys, not records

When elements are large, sorting them directly copies a lot of bytes per move. Sort an array of indices or pointers using the record as the comparison key, then permute once at the end. This is also how you sort the same data by several different keys without duplicating it.

The related trick is the decorate-sort-undecorate pattern โ€” Python's key= argument does exactly this, computing each key once instead of on every comparison.

Edge Cases & Pitfallsโ€‹

An inconsistent comparator is undefined behaviour, not a wrong answer

Comparison sorts require a strict weak ordering: if a < b then not b < a, comparison must be transitive, and equivalence must be transitive too. Violating it โ€” return a.score >= b.score instead of >, or a comparator using a mutable field โ€” does not merely produce a wrongly-ordered list. In C++ it is undefined behaviour and routinely reads out of bounds; Java throws IllegalArgumentException: Comparison method violates its general contract!, but only sometimes, depending on input size.

Write <, never <=, in a comparator.

  • Comparing floats with NaN breaks the ordering, since every comparison with NaN is false. Filter or handle NaN explicitly.
  • Sorting a mostly-sorted list with an unstable sort still costs O(n log n) in introsort or pdqsort's non-detected cases. Timsort is the one that exploits it fully.
  • sort() mutates, sorted() copies in Python. The same distinction is sort vs. to_vec then sort in Rust; picking the wrong one is a silent aliasing bug.
  • Do not write your own sort for production. The cases you will get wrong โ€” equal keys, depth limits, comparator contracts โ€” are exactly the ones these implementations spent years on.

Referencesโ€‹

Books & Videosโ€‹

  • Sedgewick & Wayne, Algorithms, 4th ed., ยง2.5 โ€” "Sorting Applications", on choosing among sorts in practice.