Introduction
The search results and file lists on your screen were sorted somewhere along the way. Sorting is still being improved: Go switched to pdqsort in 2022, and Rust switched to driftsort and ipnsort in 2024. Why is something as old as sorting still getting better?
Start from the beginning What is sorting?Common questions
USE Does my language's standard sort keep equal elements in their original order?
This property is called stability, and it depends on the language and the API. Java's Arrays.sort for object arrays is a stable sort. C++'s std::sort does not guarantee stability, but C++'s std::stable_sort is a stable sort.
USE What should I check before calling a standard sort?
There are five things to check before the call, such as whether you need everything sorted, whether the input may be modified, and whether equal keys must keep their order.
What to Consider When Calling a Sorting API Sorting basicsUSE What happens if the comparator is wrong?
Most sorts decide the order from comparison results. So a wrong comparator can scramble the order, and some implementations even throw an exception.
What to Consider When Calling a Sorting API Sorting basicsSPEED Are all O(n log n) sorts equally fast?
O(n log n) describes how the work grows as the input grows. Even with the same O(n log n), the number of comparisons, reads, and writes per element differs from sort to sort, so the speed differs too.
Same complexity, different speeds Implementation and optimizationSPEED Can data that is already in order be sorted faster?
Some sorts finish an already sorted input in n−1 comparisons. Others compare and rearrange a sorted input just as they would a shuffled one.
Pruning and Early Termination Implementation and optimizationSPEED How fast can a comparison sort get?
A sort that decides order only by comparing elements needs, in the worst case, a number of comparisons at least proportional to n log n.
The O(n log n) Barrier Sorting basicsSPEED Does sorting with several cores or a GPU always make it faster?
Handing out the work and waiting for it to finish take time, so a small input can even get slower.
Sharing the Work Across Cores or a GPU Implementation and optimizationTRACE Why do standard sorts keep getting replaced?
New ways keep being found to do less work by using input patterns and CPU behavior, and to make sorts easier to prove correct. A language switches only when the new sort still meets what it requires, such as its API guarantees, memory, and code size.
Modern Standard Sorts and How They Change Standard libraries and recent implementationsTRACE What did pdqsort and ipnsort improve?
They are implementations and techniques adopted into standard sorts since 2017. They keep stability and worst-case bounds while using input patterns and CPU behavior to do less work.
Notable Algorithms Standard libraries and recent implementationsLEARN Can I learn sorting theory from the basics, in order?
Yes. The first page starts with arrays and the basic operations of sorting.
Arrays and Sorting Moves Sorting basicsWhat is sorting?
Putting mixed-up things back into a chosen order is sorting.
Each circle moving above is one of the things being put in order. Here, we call these circles marbles.
Sorting is not only smallest to largest. Largest first, alphabetical order, and newest first are sorting too. We sort people by height, books by title, and photos by date every day.
Line up by height
short → tall
Sort books by title
A → Z
Sort photos by date
newest → oldest
If things are already in the order you search by, you can skip places where the item cannot be. Computers sort data when that makes things easier to find or display.
The finished order can be the same even when the marbles get there in different ways. The next examples show how their movements differ.
There is more than one way to put things back in order. Let's see how different sorts make progress.
One step deeper: Why sort?
Sorting data before you search it can narrow the range you have to check. Ordering can also group related items. Binary search expects sorted data, databases use ordering, and file systems organize contents by name or date.
Binary search
One comparison against the middle value 5 rules out the whole left half. When the values are scrambled you cannot know that 5 sits in the middle, so binary search does not apply.
Database index (B-tree)
The root holds 30 and 60, so a lookup for 42 only has to follow the middle branch, the one between them. The other two are never opened. An index is this same step stacked many levels deep.
File listing
When you look at a file listing, switching between name order and date order makes different things easy to find. By name you get "the ones starting with m"; by date you get "the newest one".
The value that decides the ordering here (the file name, or the date modified) is called the key. Each thing being put in order is called an element. The marbles at the top of this page are elements, and the number written on each one is its key.
What to Consider When Calling a Sorting API
Most languages ship a sorting API in their standard library. You can leave the algorithm to the API, but the caller still decides what order to sort by, whether the original array may change, and whether equal keys keep their order. These five checks tell you which API to call and how.
Do you need a complete order of every item?
If you do not need every element in sorted order, consider an API or data structure that fits the goal instead of a full sort.
- One minimum or maximum → min / max
- Only the highest or lowest k → top-k (select only the k items you need)
- Only a median or a boundary such as the top 10% → selection (find the item at a chosen rank)
- Repeated lookup over the same data → an index or ordered data structure
- A database query already guarantees the required order → do not sort it again into the same order
Have you decided how null, NaN, text, and ties are ordered?
Decide whether
nullcomes first or last, howNaNbehaves, and which language or region (locale) supplies the rules for ordering text. Decide as well whether items with equal keys stay tied, or whether a second key settles their order.May the original data be changed?
If other code refers to the same array, an API that rewrites its input also changes the order that code sees. JavaScript’s
items.sort(compare)reordersitemsitself and returns that same array, soconst ordered = items.sort(compare)still loses the original order. To keep it, use an API that returns a new array (toSortedin JavaScript, added in ES2023, orsorted()in Python).Must equal keys keep their original order?
Requirements such as showing tied scores in signup order, or preserving an earlier stage of a multi-key sort, need a stability guarantee. Whether an API gives one varies: C++'s
std::sortdoes not, whilestd::stable_sortdoes. Read the guarantee from the API instead of inferring it from an algorithm name.To see the guarantee in each language: Standard Sorts Across Languages
Can you afford the working memory and the comparison work?
Some sorting APIs allocate a work array (out-of-place). Where memory is tight, as on embedded devices, an in-place sort helps, but few in-place sorts are also stable.
To see how stability and extra memory trade off: Stability and extra memory
The comparator usually runs many times more often than there are elements (about 10,000 times for 1,000 elements in random order). If each comparison converts strings or parses dates, that work repeats just as often. Compute an expensive key once per element before sorting, for example by building key–element pairs first.
Choosing your own order
Many sorting APIs take a comparator that decides the order. By passing just the comparison you would write in a for loop as a function, you avoid writing the whole sorting procedure yourself. Here are the two ways in JavaScript.
Numbers, smallest first
Sorting with for loops
const nums = [5, 2, 9, 1];
for (let i = 0; i < nums.length - 1; i++) {
for (let j = 0; j < nums.length - 1 - i; j++) {
const a = nums[j], b = nums[j + 1];
if (a > b) {
nums[j] = b;
nums[j + 1] = a;
}
}
}
// [1, 2, 5, 9]Passing a comparator to sort
const nums = [5, 2, 9, 1];
nums.sort((a, b) => a - b);
// [1, 2, 5, 9]Players, highest score first
Sorting with for loops
for (let i = 0; i < players.length - 1; i++) {
for (let j = 0; j < players.length - 1 - i; j++) {
const a = players[j], b = players[j + 1];
if (a.score < b.score) {
players[j] = b;
players[j + 1] = a;
}
}
}Passing a comparator to sort
players.sort((a, b) => b.score - a.score);A comparator takes two elements, a and b, and returns a negative number if a comes first, a positive number if b comes first, or 0 if they tie. a - b is positive when a is larger, so it gives the same order as swapping in the loop when a > b.
How you pass a comparator depends on the language. Some APIs, such as C#'s OrderBy, even take a key instead of a comparator.
Keep the comparator consistent
When you write a comparator yourself, follow these three rules.
- Keep answers consistent
- Comparing the same two elements always gives the same answer. Do not mix in random numbers, the current time, or values that change during the sort. If you swap
aandb, the answer must flip too. - No cycles in the order
- If a comes before b and b comes before c, then a comes before c. A comparator that switches criteria depending on the pair (by score when both have one, otherwise by name, for example) can go round in a circle: a before b, b before c, and c before a.
- Don't break chains of ties
- Two elements where neither comes first are tied. If a ties with b and b ties with c, then a ties with c. Treating close values as equal (equal when
|a - b|is below a threshold) breaks this rule.
Here is a comparator that breaks each rule, next to a fixed one.
Keep answers consistent
Inconsistent
players.sort((a, b) =>
a.score > b.score ? -1 : 1);Fixed
players.sort((a, b) => b.score - a.score);When two scores are equal, swapping a and b still returns 1, so each element claims the other comes first. The fix returns 0 for equal scores.
No cycles in the order
Inconsistent
players.sort((a, b) =>
a.score != null && b.score != null
? a.score - b.score
: a.name.localeCompare(b.name));Fixed
const noScore = (p) => p.score == null;
players.sort((a, b) =>
noScore(a) - noScore(b)
|| (a.score ?? 0) - (b.score ?? 0)
|| a.name.localeCompare(b.name));Once some players have no score, the comparator uses scores for some pairs and names for others, so the order can go round in a circle. The fix puts players without a score last, then compares by score and then by name.
Don't break chains of ties
Inconsistent
values.sort((a, b) =>
Math.abs(a - b) < 10 ? 0 : a - b);Fixed
values.sort((a, b) =>
Math.floor(a / 10) - Math.floor(b / 10));100 ties with 108 and 108 ties with 116, but 100 does not tie with 116. The fix compares in buckets of 10, so every value in the same bucket ties.
Checks when calling a sorting API
Decide from the required result and the API contract, not an algorithm name. After answering, compare the source and result as marble rows.
Try the questions Four questions about top-k, mutation, stability, and comparators.
A Row of Boxes, and Sorting Moves
Arrays and Sorting Moves
Where do the marbles go?
A computer remembers the marbles in a row of numbered boxes. This connected row of boxes is called an array.
The numbers make it easy to say “look at box 2” or “switch boxes 1 and 3.” The computer can also pass around the whole row or visit each box in order.
Compare, then move
Many sorts compare marbles and move them when their order is wrong. Repeating this slowly turns a mixed-up row into an ordered one.
Compare
Ask, “Which comes first, 3 or 1?”
Move
If the order is wrong, move the marbles to new places.
Operations to watch in a sort
Sorts use operations such as read, write, comparison, swap, and move. Some algorithms use other operations too, but for most sorts, the number of these operations is a good guide to the cost.
In the figures below, [0] [1] [2] are slot positions. Arrays in most languages count from zero, so the slot called "number 1" in the figure above is [0] inside a program.
Read
Taking the value that sits at a given position
Write
Putting a value into a given position
Compare
Asking which of two keys comes first
Swap
Exchanging where two values sit, leaving the positions themselves untouched
Move
Shifting the values in between aside, then placing the taken value into the gap
How does each sort make progress?
Each way of sorting moves the marbles differently and leaves the row in a different state after each move.
How it makes progress
Fix two neighbors in the wrong order
Compare neighbors and exchange them when the larger one is on the left.
What changed? That neighboring pair is now in the right order.
Sorts that progress this way:Bubble sort
How it makes progress
Put the next marble into the ordered part
Take the next marble, make room for it in the part that is already in order, and put it there.
What changed? The ordered part is one marble longer. The marble at the end is still waiting.
Sorts that progress this way:Insertion sort
How it makes progress
Choose what belongs next
Search the unfinished part for its smallest marble and place it at the front.
What changed? The smallest marble is now first. We will not move it again.
Sorts that progress this way:Selection sort
How it makes progress
Join two ordered rows
First, put two short rows in order. Compare the front marbles and take the smaller one until the two rows become one.
What changed? Two ordered rows became one ordered row. Only those two rows were joined, not the whole row.
Sorts that progress this way:Merge sort
How it makes progress
Separate around a guide
Choose one marble as the guide, then move values smaller than it to one side and larger values to the other.
The marble chosen as the guide is called the pivot.
What changed? Values are on the correct side of the guide; the two sides are not necessarily sorted yet.
Sorts that progress this way:Quicksort
How it makes progressEXPERT
Build a structure for extraction
Maintain a heap so an extreme value can be found and removed repeatedly.
What changed? The largest value is ready at the root; the rest is only heap-ordered.
Sorts that progress this way:Heapsort
How it makes progressEXPERT
Use the key to choose a destination
A value or digit sends each item to a bucket instead of deciding every position by comparison.
What changed? Items with the same key part are grouped in the same destination. The order inside each destination is not decided yet.
Sorts that progress this way:Bucket sort, LSD Radix sort (b=10)
EXPERT · What insertion sort preserves midway
Focus on the ordered range at the start of the row during insertion sort.
Compare the methods further Carry two ways of making progress to the end, or put a different row in order yourself.
What happens when the step is repeated?
Each card above shows a single step. The following examples repeat two of those steps until the mixed-up row is fully ordered.
Green marks the marbles that are in order so far. They can still move later.
-
At the start, only the marble on the left is in the ordered part.
Check it yourself
Use different numbers to check the same move: put the next marble into the ordered part. Choose where each marble belongs.
-
At the start, only the marble on the left is in the ordered part.
Where does 2 go?
Fast Sorts, Slow Sorts
What happens when we add more marbles?
We counted how many times “Choose what belongs next” and “Join two ordered rows” from the previous page compare marbles before they finish. Switch the number of marbles to compare the two counts.
“Choose what belongs next” always makes the same number of comparisons, whatever the order. The count for “Join two ordered rows” depends on the order, so it shows the average over shuffled marbles.
How work such as the number of comparisons grows when we add more marbles is called time complexity.
How work such as the number of comparisons grows when we add more marbles is called time complexity. These growth rates are written with the symbols O, Θ and Ω.
Why do we use complexity?
Time complexity expresses the difference we just saw as a relationship between the input size n and the number of operations. For sorting, the operation usually counted is comparisons. Asymptotic notation does not give the actual number of seconds. It helps us compare the broad trend as more marbles are added.
Dominant Operations
The overall time complexity of an algorithm is set by whichever operation grows fastest as the input size grows. That operation is called the dominant operation, or the bottleneck. Because complexity can vary depending on the input, it is usually described in terms of best, average, and worst cases.
For example, even if the number of comparisons grows in proportion to the input size n, if the number of shifting writes grows in proportion to n², those writes decide the total as n grows. In that case, we keep only the n² that decides the growth and write the overall complexity as O(n²). O(n²) means “grows at most in proportion to n².”
- Best case: performance on the most favorable input for a given sort, such as an already-sorted array.
- Average case: the expected performance under a stated input distribution. Sorting analyses often assume that every input permutation is equally likely.
- Worst case: performance on the most unfavorable input for a given sort.
Three Ways to Describe Complexity
How do O, Θ, and Ω differ? O is an upper bound, Θ is a matching bound, and Ω is a lower bound.
O (Big-O)
Upper boundFor sufficiently large inputs, the complexity does not grow faster than this upper bound. Saying quicksort’s worst case is O(n²) means its cost does not asymptotically grow faster than n².
Θ (Theta)
Tight boundThe growth is pinned from above and from below by the same shape. Merge sort is barely affected by input order: its best, average, and worst cases are all Θ(n log n).
Ω (Omega)
Lower boundΩ describes a growth rate the cost does not fall below on large inputs. Comparison sorts need Ω(n log n) comparisons in the worst case. Insertion sort checks every element even on ordered input, so its best case is Θ(n). Ω is not reserved for best cases.
Why Big-O Is Used Most Often
Many sorting resources conventionally use Big-O even when describing a tight growth rate. What users usually care about is whether the number of operations balloons beyond what they expected as the input grows. Big-O states that ceiling, “at most about this much,” in a short form. For example, “Quicksort is O(n log n)” may be shorthand for saying its average case is Θ(n log n).
- Θ(1) Constant time: the counted operation occurs a fixed number of times regardless of input size, as when reading the first array element once.
- Θ(n) Linear time: the count grows in proportion to the input size, as when scanning an array once from beginning to end.
- Θ(n log n) Linearithmic time: greater than linear but far less than quadratic, as in a sort that splits the array and processes all elements at each level.
- Θ(n²) Quadratic time: the count grows with the square of the input size, as when a double loop examines every pair of elements.
Growth Rate Comparison
A general sort must examine every element, so it requires Ω(n) time. Furthermore, a general sort that determines order through comparisons alone requires Ω(n log n) comparisons in the worst case. Counting operations directly, n² is about 750 times n log₂ n when n = 10,000.
| n | n | n log₂ n | n² |
|---|---|---|---|
| 10 | 10 | ~33 | 100 |
| 100 | 100 | ~664 | 10,000 |
| 1,000 | 1,000 | ~9,966 | 1,000,000 |
| 10,000 | 10,000 | ~132,877 | 100,000,000 |
The table shows the values of the functions themselves, which differ from actual comparison counts by a constant factor. With n = 1,000, selection sort makes n(n − 1)/2 = 499,500 comparisons (about half of n²), and merge sort makes about 9,400 on average for shuffled input (close to n log₂ n).
Why Constants Disappear
Asymptotic notation focuses on the growth rate as input size n increases, so it drops constant multipliers. Counts of 2n and 5n are both tightly Θ(n), and both also fit the same O(n) upper bound. This helps compare broad trends, but it does not capture their difference in actual speed.
Same Big-O Does Not Mean Same Performance
Heapsort and quicksort are both described as O(n log n). Heapsort is Θ(n log n) for every input, while quicksort averages Θ(n log n) and has a Θ(n²) worst case. Even so, quicksort can finish sooner because some implementations use the cache better. Big-O does not show this difference.
The O(n log n) Barrier
General sorting algorithms that determine order through comparisons alone require Ω(n log n) comparisons in the worst case. Therefore, a comparison sort with O(n log n) worst-case time is asymptotically optimal. The stable merge sort and the unstable heapsort are equally optimal in this sense.
Start with a decision tree for three elements
Sort three distinct elements A, B, and C using only less-than comparisons. Each answer is Yes or No, and each leaf is the completed order reached by that path.
In general, n distinct elements have n! possible orders. A binary decision tree of height h has at most 2ʰ leaves, so distinguishing every order requires 2ʰ ≥ n!, or h ≥ ⌈log₂(n!)⌉. This is Θ(n log n), giving a worst-case lower bound of Ω(n log n) comparisons.
To break through this wall, you need information beyond comparisons. Distribution sorts use the values themselves or the shape of the keys to place elements, freeing them from the comparison-based lower bound and enabling sorting faster than O(n log n) under the right conditions. Counting sort and radix sort are the prime examples.
Speed also depends on the shape of the keys. Counting sort is Θ(n + k) for key range k, and LSD Radix Sort is Θ(d(n + b)) for d digits in base b (the number of values one digit can take). Both use keys directly, so they count different work from comparison sorts. A large key range or many digits can make them slower.
The worst-case lower bound for comparison sorting does not change. However, if the input already has parts that are in order, a sort may use them to reduce comparisons and movement. A sort that finishes in linear time on sorted input, and in nearly linear time when only a few elements are out of place, is called an adaptive sort.
Stability and extra memory
Speed is not the only thing to weigh when choosing a sort. How much memory it uses, and whether it keeps the order of equal keys, matter too.
In-place sorting and extra memory
The memory used in addition to the input is the sort's auxiliary space complexity. An in-place sort orders elements by moving them within the original array. That does not mean it uses no extra memory at all. Most need only O(1) working storage, although some implementations use about O(log n) to record recursive calls. An out-of-place sort works through a temporary array, so it needs extra memory, typically O(n).
Embedded devices and other environments with little memory are a typical place where in-place sorts are chosen. There, O(n) extra memory is hard to come by, and sorts that use a temporary array may have to be avoided.
Stability
A stable sort keeps elements with equal keys in the order they started in. When sorting students by score, a stable sort keeps “Tom” and “Sam” (both 90) in their original order.
This matters when the key is only part of each element, rather than the whole value. With a stable sort, if you sort by name first and then by score, the names stay in order within each score. With an unstable sort, sorting by score may not keep the names in order within each score. When the original order of equal keys matters, you need stability.
Stable Sort
Equal keys (90) keep their original order: Tom stays before Sam.
Unstable Sort
Equal keys (90) may swap: Sam moved before Tom.
How to Achieve Stability
Stability is not just a property of an algorithm. It can be built in through design. There are two main strategies.
- Strategy 1: Use a stable algorithm. The simplest approach: choose an implementation that is inherently stable (Merge sort, Timsort, Powersort). No extra effort needed.
- Strategy 2: Break ties by original position. Before sorting, record each element’s original position, then compare by key first and, if the keys are equal, by original position. Since no two elements then tie, the original order is preserved. This does not rely on the algorithm being stable, but recording the positions takes extra memory proportional to the number of elements.
C#’s OrderBy / ThenBy are a clear example of strategy 2. C#’s Array.Sort is an unstable sort, but OrderBy / ThenBy build an array of keys and an array of original positions, and compare equal keys by original position, which guarantees stability.
Choose a Sort by Extra Memory and Stability
Does it keep the order of equal keys? Does it sort within the original array? Together, the answers divide the implementation variants into four groups.
Block sort (WikiSort) is stable and in-place, has O(n log n) worst-case time, and uses O(1) extra memory. Timsort is stable but not in-place and uses O(n) extra memory.
To see the strategy in each language: Standard Sorts Across Languages
| In-place (O(1) to O(log n)) | Not in-place | |
|---|---|---|
| Stable |
|
|
| Unstable |
|
|
How algorithms are built
Many fast sorts split the array into small parts, sort them, and combine the results into the whole. The split can be top-down, dividing the whole recursively, or bottom-up, building up from small units. The flow also changes between merge-based designs, which settle the order when combining, and partition-based designs, which settle it when splitting.
Fundamentals Whether code divides top-down or builds bottom-up, and how it merges ordered runs. Most implementations differ along these two choices.
Top-down and Bottom-up
Top-down splits recursively, so the code mirrors the problem structure and handles uneven sizes naturally. Bottom-up starts from small units and merges upward with loops. It avoids both recursive calls and the call stack that records unfinished function calls.
Merge
A merge combines two sorted arrays into one sorted array. By repeatedly taking the smaller of the two front elements, it preserves order throughout. It is a fundamental operation used in merge sort and many other algorithms.
Divide and Conquer Merge-based and partition-based forms share a strategy, but place their comparison, movement, and memory costs differently.
Divide-and-conquer is a design method: split the problem, solve each part, and combine the results. In sorting, the code splits into two forms, merge-based and partition-based.
Merge-based
Sort the parts and merge them to form the whole. Implementations may split recursively from the top down, merge small units from the bottom up, or use existing runs in a natural merge strategy.
Merge sort, Timsort, PowersortPartition-based
Split the input by a pivot or key, then sort each part independently. A 3-way partition groups equal keys and avoids recursing over them. MSD Radix Sort also divides from higher digits downward. LSD Radix Sort distributes from lower digits upward, so it is not divide-and-conquer.
Quicksort, Samplesort, MSD Radix sortSame complexity, different speeds
Quicksort, merge sort, and heapsort all have an average complexity of O(n log n). Even so, they do not finish in the same time. Complexity describes how the cost grows; it does not show how long one operation takes or differences by a constant factor.
Two sorts with the same average O(n log n) can still run at different speeds. Complexity describes how the work grows as the input grows; it does not say how many comparisons, reads, and writes each element costs.
Why the same O(n log n) can run at different speeds
Quicksort and heapsort are both O(n log n) on average. Predict which one finishes first.
Fewer Calls and Copies
The same algorithm can run faster depending on how it is put together. Some techniques replace recursion with a loop to make fewer function calls; others cut down the reads and writes to a buffer.
Ways to cut calls and copies
Recursion → Iteration Replacing recursion with a loop and an explicit stack removes function calls and gives the implementation direct control of pending ranges.
Iteration alone does not determine stack use or running time. Quicksort can bound the number of saved ranges to O(log n) by processing the smaller partition first and saving the larger one.
Implementation examples: Bottom-up merge sortTail Recursion Elimination Recurse only on the smaller side and loop over the larger side to bound stack depth even under skewed partitions.
This keeps stack depth at O(log n), even when partitions are badly unbalanced.
Implementation examples: Quicksort, Quicksort (3-way), Quicksort (Median3), Quicksort (Median9), Dual-Pivot Quicksort, Quicksort (Stable), Quicksort (Bidirectional Stable), Quicksort (Destswap Stable), BlockQuicksort, Introsort and 5 morePing-Pong Buffering Alternate source and destination at each level to avoid copying every completed level back to the original array.
Writes between buffers still occur. If the final result lands outside the original array, one final copy is required.
Implementation examples: Pingpong merge sort, Bottom-up merge sort, std::stable_sort (LLVM), Spinsort, Flat stable sort, Quicksort (Destswap Stable)Copy-Smaller Strategy Buffer only the smaller run, so a merge needs temporary storage for at most half the elements.
Example: the optimization that turns recursion into a loop
What changes when recursion becomes a loop?
One way to optimize quicksort is to manage the pending ranges with a loop and an explicit stack, instead of recursing into both sides. Why does this help?
1. Compare the postponed work
While sorting an array of 128 elements, compare how many postponed pieces of work each approach has to remember at once. Recursion remembers them as functions still in progress; the loop remembers them as ranges saved on an explicit stack. Choose an input to play.
Pruning and Early Termination
An input may already contain sections that are in order. If a sort can find such a section, it can skip sorting it again. Checking whether a section is in order also takes comparisons, so implementations keep the check cheap and stop it as soon as it looks unpromising.
Ways to skip work
Sortedness Detection When the input is already ordered, detecting that fact lets the algorithm skip the remaining heavy path.
Nearly ordered input can also avoid work when only the disordering elements need repair.
Implementation examples: Bubble sort, Cocktail shaker sort, Odd-even sort, Comb sort, Circle sort, ShiftSort, Spinsort, Flat stable sort, Smoothsort, Introsort and 4 morePartial Insertion Sort Cutoff Try insertion only while a small move budget can repair the range, then return to the main strategy when it cannot.
Count moves and treat the input as not nearly sorted when the limit is exceeded.
Implementation examples: Introsort, pdqsort, pdqsort (branchless)Range Reduction Find edges already in place and narrow the range that must be merged.
Before merging, use exponential search, which doubles the search distance as 1, 2, 4, 8, and so on, to skip elements already in their final positions. On nearly sorted data, this can greatly reduce the merge range.
Implementation examples: Pingpong merge sort, Bottom-up merge sort, Block sort (WikiSort), TimsortExample: stopping a partial insertion sort early
Why stop the insertion sort partway?
If partitioning a range needed no swaps and did not leave one side much smaller than the other, pdqsort guesses that the range is nearly sorted and tries to finish it with insertion sort. Once it has moved elements more than 8 times, it stops the insertion sort and goes back to partitioning. Why stop partway?
1. Try the insertion sort
On a range of 32 elements, try an insertion sort that may move elements at most 8 times. Choose a range to play it.
Pattern Adaptation and Strategy Switching
Real data can contain ascending or descending stretches, or many repeated values. When a sort finds such a feature, switching to a method that suits it reduces comparisons and moves. Some techniques also switch to another method as soon as the input turns out to be a poor fit for the current one. Switching is a way of building a sort, separate from the property “adaptive”: a sort that switches methods but does not finish in nearly linear time when only a few elements are out of place (pdqsort, for example) is not called adaptive.
Ways to match the input
Run Detection Reuse ascending and descending runs already present in the input instead of rebuilding every ordered stretch.
Descending runs are reversed, short runs are extended with insertion sort, and the runs are then merged. Only strictly descending runs may be reversed. Reversing across equal values swaps their order and breaks stability.
Implementation examples: Natural merge sort, Strand sort, Timsort, Powersort, ShiftSort, Spinsort, Glidesort, DriftsortDrop-Based Techniques Set aside a small number of disordering elements, preserve the main ordered flow, and merge them back later.
Only the buffered elements are processed later, then merged back into the main sequence.
Implementation examples: Drop-Merge sortHybrid Switching Hand work to a suitable sub-algorithm according to size, recursion depth, and the observed input pattern.
For example, introsort normally uses quicksort, switches to heapsort when recursion becomes deep, and uses insertion sort for small arrays.
Implementation examples: Introsort, std::stable_sort (LLVM), Block sort (WikiSort), Timsort, Powersort, Spinsort, Flat stable sort, Glidesort, Driftsort, Quicksort (3-way) and 10 moreEarly-Out Heuristic When the observed input is a poor fit, switch to a general path before the current strategy degrades further.
The algorithm can monitor disorder or partition imbalance to decide when to switch.
Implementation examples: Drop-Merge sort, Introsort3-Way Partitioning Collect values equal to the pivot and avoid extra comparisons and recursion.
The benefit grows with the number of duplicates. When all elements are equal, the work is O(n).
Implementation examples: Quicksort (3-way), Quicksort (Median3), Quicksort (Median9), Dual-Pivot Quicksort, Quicksort (Stable), pdqsort, pdqsort (branchless)Adaptive Pivot Selection Use a larger pivot sample as inputs grow to reduce comparisons from unbalanced partitions.
For example, small inputs may choose the median of three candidates, while larger inputs may sample nine. A larger sample reduces the chance of a badly unbalanced partition.
Implementation examples: Driftsort, Quicksort (3-way), Quicksort (Median3), Quicksort (Median9), Dual-Pivot Quicksort, Quicksort (Stable), Quicksort (Bidirectional Stable), BlockQuicksort, Introsort, Introsort (.NET) and 4 moreMerge Order Optimization With the same runs, choosing which adjacent pair to merge first changes the total comparisons and movement.
Galloping (Exponential Search) When one run keeps winning, exponential search skips repeated element-by-element comparisons.
On nearly ordered data, one run can often provide a whole block of values, greatly reducing comparisons.
Implementation examples: TimsortExample: Timsort reusing ordered runs
Timsort and merge sort on an array with ordered runs
Timsort finds the ascending and descending runs already in the array and merges them as they are. Merge sort keeps splitting the array in half at fixed positions.
Fewer Hard-to-Predict Branches
Fewer comparisons do not always produce a shorter running time. A CPU also waits on unpredictable branches and distant memory accesses, so two implementations of the same algorithm can run at different speeds.
A CPU keeps recently used data in a fast cache, predicts which way each conditional branch will go, and overlaps instructions that do not wait for one another. So it stalls when a branch prediction is wrong, when an instruction needs the result of the one before it, and when data is not in the cache. Changing how branches are written, how instructions are ordered, and where data is kept reduces this waiting.
When irregular conditional branches follow one another, the CPU has trouble predicting where to go next and waits each time a prediction is wrong. Removing branches also removes those failed predictions. Using the result of a comparison in a calculation instead of a jump, so that the branch disappears, is called branchless code.
Ways to remove branches
Code without branches (branchless) Use the comparison result as the number 0 or 1 in a calculation, removing the branch whose direction depends on the comparison.
For example, to count values smaller than the pivot, write count += (value < pivot) instead of an if, adding 1 when smaller and 0 otherwise. To choose one of two values, use an instruction that selects by condition (such as cmov on x86). Mispredictions disappear, but the same instructions run either way, so on data that is easy to predict, such as sorted data, the gain over a branch is small.
Implementation examples: BlockQuicksort, pdqsort (branchless)Block Partitioning An application of branchless code to partitioning. Separate comparison from exchange to reduce unpredictable branches and scattered movement.
Read a block, collect the positions to exchange, then perform those exchanges together. Collecting the positions adds each comparison result to a count, so the path does not depend on the values. Sequential reads also make better use of the cache.
Implementation examples: BlockQuicksort, pdqsort (branchless)Sorting Networks for Small Arrays An ordering that is easy to write branchless. For a small fixed size, a fixed comparison schedule removes loops and data-dependent branches.
For arrays of about 2 to 5 elements, the comparisons can be written directly in code, removing the loop test as well. Because the comparison order does not depend on the values, writing each compare-and-exchange with instructions that pick the smaller and the larger value leaves no branch.
Implementation examples: Block sort (WikiSort), Glidesort, Driftsort, Introsort, std::sort (LLVM), ipnsortSentinels (Unguarded Insertion) Not branchless, but a way to reduce the boundary-check branches. Place a value that must stop the scan so the inner loop no longer checks the boundary on every move.
A normal insertion loop checks both the array boundary and the value on every move. In a non-leftmost partition, the preceding pivot can stop the scan, leaving only the value comparison. The branch that compares values remains.
Implementation examples: Pair insertion sort, Introsort, pdqsort, pdqsort (branchless), std::sort (LLVM)Example: a partition written with a branch and a branchless partition
The data changes how often a branch is mispredicted
Before a comparison result is known, the CPU predicts which way the branch will go and carries on ahead. Running the predicted path before the result is known like this is called speculative execution. When the prediction is wrong, it redoes the work it did ahead.
1. Run quicksort
Quicksort repeatedly moves the values smaller than a reference value (the pivot) to the left and the rest to the right. Suppose we sort 64 elements and the pivot is 33. Written with a branch, it looks like this.
if (value < 33) put it on the left;
else put it on the right;
Here we simplify and assume the CPU predicts the same result as last time. It starts by predicting “smaller”.
On the random array the prediction keeps missing; on the sorted array it misses only where the smaller values turn into larger ones.
Real CPUs predict more cleverly, but when the pivot is the middle value, whether a value in random order is smaller than the pivot is guessed right only about half the time, whatever the method.
Letting the CPU Overlap Instructions
Even within a single core, a CPU works on several instructions at once when they do not use each other’s results. When each instruction needs the result of the one before it, though, the CPU can only run them one at a time. Placing pieces of work that do not use each other’s results side by side lets the CPU overlap them. Running instructions that do not use each other’s results at the same time is called instruction-level parallelism (ILP).
Ways to make work independent
Bidirectional Merging Resolve the front minimum and back maximum independently, creating instruction streams the CPU can overlap.
Write the minimum to the front of the output and the maximum to the back. The two operations do not wait on each other's result, so the CPU can overlap them.
Implementation examples: Glidesort, Driftsort, Quicksort (Bidirectional Stable), ipnsortLoop Unrolling Expand several iterations into one body to reduce loop tests and expose more independent instructions.
The expanded operations are often independent, so the CPU can overlap them. pdqsort’s branchless variant processes eight iterations of its offset-generation loop at once.
Implementation examples: Driftsort, pdqsort (branchless), ipnsortExample: a front-to-back merge and a bidirectional merge
A merge that works from both ends at once
A merge combines two ordered runs into one ordered run.
1. Run an ordinary merge
Merge two ordered runs of 8 values each into one run of 16. Compare the heads of the two runs and write the smaller one to the next spot from the front of the output. Each comparison uses the positions the previous comparison moved, so it cannot start until the previous result is known.
Here we simplify and assume the CPU can run up to two pieces of work at once when they do not use each other’s results.
The output fills only one cell at a time from the front, taking 16 steps in sequence.
Steps in sequence: 0 Comparisons: 0
Keeping Data Small and Reading It in Order
When a CPU reads data from memory, it brings nearby data into the cache along with it. Reading neighbouring locations one after another uses all of that data. Keeping the working data small makes it more likely to fit in the cache. Block partitioning also reads elements a block at a time, which helps here too.
Ways to arrange and read data
Bit Tricks & Bit Packing Express key extraction and flags at bit granularity to reduce instructions and the size of working data.
With a power-of-two radix such as 4, 16, or 256, digit extraction can shift the bit pattern left or right and mask off every part except the needed digit. Packing one flag per element from one byte into one bit reduces that storage by about eight times.
Implementation examples: Weak-heap sort, LSD Radix sort (b=4), LSD Radix sort (b=256), MSD Radix sort (b=4), American flag sort, SpreadsortExample: reading in order and keeping values small
How the reading order changes the loads from memory
When a CPU reads one value from memory, it loads that value and its neighbours into the cache together, 64 bytes at a time on many CPUs. The cache is a fast store close to the CPU, and the chunk loaded at once is called a cache line.
Comparing Several Values with One Instruction
SIMD (Single Instruction, Multiple Data) is a CPU feature that applies one instruction to several values at once. A 128-bit register holds four 4-byte values (such as int). Comparing two such registers performs four comparisons with one instruction. Because SIMD runs inside one core and mixes with ordinary instructions, an algorithm can use it for just one part, such as the comparisons in a partition or the sorting of a small range. It works when the keys have a fixed size and the CPU can compare them directly, such as integers and floating-point numbers.
Sorting with SIMD
SIMD Fixed-size keys can be compared several at a time, but arbitrary types and caller comparators are a poor fit.
Google Highway’s VQSort and Intel’s x86-simd-sort are production examples. VQSort sorts small inputs with sorting networks. NumPy 1.25 (2023) sped up np.sort and np.argsort on AVX-512 processors. NumPy 2.0 (2024) adopted x86-simd-sort and Google Highway to accelerate four ordering APIs. Standard sorts accept arbitrary types and comparators, so SIMD is mainly a fast path for fixed-size keys.
NumPy 1.25 release notes
NumPy 2.0 release notes
VQSort README
Example: comparing one pair at a time versus four pairs at once
A sorting network that compares four pairs at once
A sorting network is a way of sorting in which the pairs to compare and their order do not depend on the values. Here, 8 values are sorted with the bitonic sort network.
1. Compare one pair at a time
The 8 values are sorted by a fixed set of 24 comparisons. Of the two values compared, the smaller goes to the left and the larger to the right.
The dots below stand for the 24 comparisons, arranged in groups of 4 pairs that compare different values, in 6 columns.
Comparing one pair per instruction, the dots fill one at a time, taking 24 steps in sequence.
Steps in sequence: 0 Pairs compared: 0
Sharing the Work Across Cores or a GPU
Besides techniques inside a single core, the work of sorting can be divided across several CPU cores or a GPU and run at the same time. This is called parallelism.
It does not speed up every input, though. Handing out the work and waiting for it to finish take time, so a small input can even get slower. Parallelism pays off on large inputs, where the time saved by sharing the work exceeds that cost.
CPU cores and GPUs
Sharing the Work Across CPU Cores On a large enough input, the time saved by sharing the work exceeds the time spent handing it out and waiting. It does not fit the default path of a general sort that also handles small inputs.
Java’s Arrays.parallelSort and C++ std::sort with an execution policy make parallelism explicit. In OpenJDK, an int array of 4096 elements or fewer is sorted on one thread without being divided. On small inputs, the time spent handing work to threads and waiting for them cannot be recovered.
OpenJDK source (JDK 25)
Sharing the Work with a GPU A GPU applies the same instruction to thousands of values at once. Copying the data into GPU memory also takes time, so it suits very large inputs.
A GPU is a device that applies the same instruction to many values at once. Sorting networks such as Bitonic Sort, whose pairs and order of comparison do not depend on the values, fit this design. NVIDIA’s Thrust uses radix sort when it sorts an array of numbers in ascending or descending order, and merge sort when given any other comparator. On a GPU with memory separate from the CPU’s, copying the data to GPU memory and the results back also takes time. Thrust source (CCCL 3.5.0)
Bitonic sort, Radix sort (Thrust / CUB)Example: sorting with one core and with four cores
How sharing the work across four cores depends on array size
The work of a merge sort is divided across four cores and compared with sorting on one core, by the time it takes to finish.
1. Sort 32 values
The same 32 values are merge-sorted on one core and on four cores. Each of the four cores sorts 8 values, then runs are merged two at a time, and one core performs the final merge.
Here we simplify by counting time in comparisons and assuming that handing the work to four cores and starting them takes the time of 200 comparisons. The 200 is a number chosen for this model, not a measurement.
Four cores need setup time, so they take longer than one core to finish sorting.
Comparing and Classifying Sorts
When thinking about how sorts differ, it helps to look separately at what decides the order, how the sort proceeds, how much extra memory it uses, and which properties hold. Two comparison sorts, for example, can still differ in how they use extra memory and in whether they keep elements with equal keys in their original order.
What decides the order
Whether the order comes from comparing two elements, or from using values or digits directly. Sorts that decide by comparing are called comparison sorts.
How it sorts
Which operation grows the sorted part: insertion, selection, exchange, merging, partitioning, distribution, and so on. Practical sorts combine several of these.
Extra memory
Whether the sort works only by moving elements within the array (in place), or uses space outside the array.
Properties
Whether it is stable (keeps elements with equal keys in their original order), how long it takes in the worst case, and whether it is adaptive (finishes in linear time on input that is already sorted, and in nearly linear time when only a few elements are out of place).
Example: comparing Introsort and Timsort
Introsort and Timsort are both comparison sorts. Are they the same in the other aspects as well?
| Aspect | Introsort | Timsort |
|---|---|---|
| What decides the order | Comparison | Comparison |
| How it sorts | Partitioning. Split the range around a pivot, then sort each part (top-down). Short ranges switch to insertion sort, and splitting that goes too deep switches to heapsort | Merging. Find ordered runs already in the input and join them into longer ones (bottom-up). Short runs are extended with binary insertion sort |
| Extra memory | In place, plus O(log n) for recursion | Up to half the number of elements, for merging |
| Properties | Unstable. O(n log n) in the worst case | Stable. O(n log n) in the worst case |
| Ordered input | Not in the original design | Uses the ordered runs already in the input as they are. n − 1 comparisons if already sorted |
What they share is that comparisons decide the order and that both finish in O(n log n) in the worst case. They differ in how they sort, in extra memory, in stability, and in how they handle ordered input. The name “comparison sort” alone does not show these differences.
Classifying sorts
A sort has several features at once: how it sorts, how much extra memory it uses, its properties, and more. So rather than placing a sort in a single class, recent approaches give it several classifications.
The classic classification
Donald Knuth, sometimes called the “father of the analysis of algorithms”, is a computer scientist who received the Turing Award in 1974. The Art of Computer Programming, which he has been writing since 1968, is a series of books that sets out the central algorithms of programming systematically, together with their mathematical analysis.
Its third volume, Sorting and Searching, divides internal sorting, which sorts data that fits in memory, into five kinds: insertion, exchanging, selection, merging, and distribution. Under this scheme, quicksort is an exchange sort and heapsort is a selection sort. These names are still common in books and papers, but a modern practical sort that combines several methods may not fit any single one.
Reference: Donald E. Knuth, The Art of Computer Programming, Volume 3, Section 5.2.
Classification today
Recent textbooks and curricula do not settle a sort into a single class. They classify it along several aspects at once.
Classification by whether a sort compares elements or uses values or digits directly. A comparison sort needs, in the worst case, a number of comparisons at least proportional to n log n. Counting sort and radix sort use values or digits directly, so this lower bound does not apply to them.
Comparison sorts
Sorts that use values or digits directly
Classification by design strategy. CS2023 is a set of guidelines, produced jointly by three societies (ACM, the IEEE Computer Society and AAAI), on the knowledge and skills students should gain in a computer science degree. It lists selection sort as an example of brute force, insertion sort of decrease-and-conquer, quicksort and merge sort of divide-and-conquer, and heapsort of transform-and-conquer.
Brute force
Decrease-and-conquer
Divide-and-conquer
Transform-and-conquer
Classification by properties. It looks at points such as whether a sort is stable, how much extra memory it uses, and whether it can use parts of the input that are already in order to do less work. CS2023 lists the distinction between stable and unstable sorts among the topics that every computer science graduate must know (the CS Core).
| Algorithm | Stability | Extra memory | Time on already sorted input |
|---|---|---|---|
| Insertion sort | Stable | In place | O(n) |
| Selection sort | Unstable | In place | O(n²) |
| Merge sort | Stable | As many as the elements | O(n log n) |
| Heapsort | Unstable | In place | O(n log n) |
| Timsort | Stable | Up to half the elements | O(n) |
A single sort belongs to several of these classes at once. Timsort, for example, is a comparison sort, is stable, and is adaptive. CS2023 also lists Timsort alongside quicksort and merge sort as an example of an O(n log n) sort.
References: Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms, 4th ed., Chapter 8; ACM/IEEE-CS/AAAI, CS2023 Body of Knowledge, Algorithmic Foundations (AL). CS2023 Body of Knowledge
SortVivo’s categories
To make algorithms easier to find, SortVivo gives each algorithm one category. Seven of its ten categories are ways of sorting: the five classic kinds plus partition and heap. Quicksort, an exchange sort in the classic scheme, is PARTITION here, and heapsort, a selection sort there, is HEAP. Algorithms that do not fit a way of sorting are grouped by structure or as other. A sort that combines several methods goes in the category of its main method, and the other methods it uses are listed in its description under “Combined with”: introsort is PARTITION, and Timsort is MERGE. Properties such as being stable or adaptive are not categories; you find them with the filters.
Ways of sorting
Structure
Other
Modern Standard Sorts and How They Change
Why do standard sorts get replaced? In 2022, Go moved the sort that does not promise to preserve equal-key order to pdqsort. In 2024, Rust replaced both its stable sort, which preserves that order, and its unstable sort, which does not promise to preserve it. Each language weighs API needs, speed, memory, and robustness differently.
What matters when choosing a standard sort?
A faster implementation does not automatically become the standard sort. Besides speed, a standard sort is chosen by these conditions.
Compatibility
Does it keep the behavior that existing programs rely on, such as stability and the order of equal values?
Example:In 2022, .NET considered replacing its unstable sort with pdqsort. The proposal was declined because the order of equal values would change and many projects would have to fix their tests.
Worst-case speed
Does it avoid becoming extremely slow on skewed or deliberately crafted input? For services that accept outside input, this also protects against attacks that try to slow them down.
Example:C++11 requires std::sort to make at most O(n log n) comparisons even in the worst case. Quicksort takes quadratic time on some inputs, but introsort protects the worst case without slowing down the average case.
Speed on common input
Can it do less work by using traits of the data that a language often sorts, such as parts that are already in order, many equal values, or slow comparisons?
Example:In Python, comparing two elements can run arbitrary Python code, so each comparison is expensive. Timsort was chosen on the assumption that reducing comparisons almost always pays off.
Memory and code size
Do the extra working memory and the code generated for each element type stay within acceptable limits?
Example:Rust adopted driftsort, a trimmed-down design, because glidesort was fast but produced too much code. In the authors' measurements, glidesort generated more than 10 times as much code as the previous stable sort.
Tuning for the CPU and the type
Can it take advantage of CPU behavior such as branch prediction and caches, and switch to a faster dedicated method when the element type is known, as with integers?
Example:Orson Peters’s C++ implementation of pdqsort (pdqsort.h) automatically uses branchless partitioning (pdqsort_branchless) under C++11 or later when the elements are of an arithmetic type and no comparator, std::less, or std::greater is given.
Correctness and maintainability
A standard library is used for a long time, so its sort must be easy to verify and simple enough to maintain. It must also behave the same on every supported platform.
Example:Go uses SymMerge for its stable sort. The source comments explain that it is a simple method whose correctness can be proven for any input.
What is adaptivity?
An input may already contain ordered stretches or many duplicate keys. A sort can use those features to reduce comparisons and movement.
Adaptivity means using order already present in the input to do less work. An adaptive sort finishes in linear time on sorted input, and in nearly linear time when only a few elements are out of place. Grouping duplicate values changes the processing to suit repeated values rather than order, so it is treated separately from adaptivity. API guarantees, memory, and portability are decided separately as well.
Stable and unstable sorts sit behind separate APIs so that callers can choose between different guarantees and costs.
How do you investigate difficult inputs and updated implementations?
An implementation that is fast on average can still degrade on deliberately difficult input. Before adopting it, and again after upgrading it, you need different evidence for guarantees and current behavior.
Try the questions Two questions about protection from difficult inputs and API guarantees versus internal implementation.
Standard Sorts Across Languages
Languages are listed alphabetically. The badge beside each language shows its stability guarantee. Select a language for its API and implementation details, or an algorithm for an explanation of how it works.
| Language | Algorithm | Version / year |
|---|---|---|
| C No guarantee | Implementation-defined | — |
| C# / .NET Chosen by API | Introsort (.NET)No guaranteeIntrosort (ties by original index)Stable | .NET Core 1.0+ .NET Framework 4.5+ |
| C++ Chosen by API | IntrosortNo guaranteestd::sort (LLVM)libc++Merge sort familyStable | — |
| C++ (Boost) Chosen by API | pdqsortNo guaranteeSpinsortStableFlat stable sortStable | — |
| Dart No guarantee | Dual-Pivot Quicksort | — |
| Go Chosen by API | pdqsortNo guaranteeSymMerge sortStable | 1.19+ (2022) |
| Java Chosen by API | TimsortStableDual-Pivot QuicksortNot applicable | JDK 7+ |
| JavaScript Stable by default | TimsortV8Merge sortSpiderMonkeyPowersortJavaScriptCore | Stable sort since ES2019 |
| Kotlin Stable by default | Same as JavaJVMImplementation-definedJS / Native | — |
| PHP 8+ Stable by default | Quicksortwith insertion sort | Stable sort since 8.0 |
| Python Stable by default | TimsortPowersortmerge order | 2.3+ / 3.11+ |
| Ruby No guarantee | OS qsort_rif availableQuicksortotherwise | — |
| Rust Chosen by API | ipnsortNo guaranteeDriftsortStable | 1.81+ (2024) |
| Swift Stable by default | Timsort | 5.0+ |
| Zig Chosen by API | pdqsortNo guaranteeBlock sort (WikiSort)Stable | 0.11+ |
As of 2026-03. API details come from official specifications and documentation. Implementation details come from released source code and release notes. Internal implementations may change.
How standard sorts were adopted over time
- Adopted as the stable sort
- Unstable or unspecified
- Start year not traced (shown from the earliest confirmed year)
- Changed this year
Shows how each language's standard sort has changed over the years.
APIs, implementations, and reasons by language
Languages weigh API guarantees, compatibility, speed, and memory differently. “Stable” promises to preserve equal-key order; “No guarantee” means callers cannot rely on that order. “Not applicable” marks primitive arrays whose equal values cannot be distinguished. Sources appear at the end of each card.
C
No guarantee- No guarantee
qsort()
- Implementation
The internal algorithm is left to each implementation of the C standard library (libc).
qsort_r()was a glibc/BSD extension until POSIX.1-2024 standardized it; the standardized argument order differs from the older BSD one.- Why this sort
C does not explain the background of
qsort(). Neither the C nor the POSIX standard names an algorithm or guarantees complexity or stability, and the order of elements that compare as equal is unspecified.
C# / .NET
Chosen by API- No guarantee
Array.Sort() - Stable
Enumerable.OrderBy()/ThenBy()
- Implementation
Array.Sort()uses introsort, switching to heapsort when partitioning gets too deep to keep worst-case time within O(n log n). This implementation dates from .NET Core 1.0 and .NET Framework 4.5.OrderBy/ThenBysort element positions and break key ties by original position to preserve stability. When followed bySkip/Take, a partial quicksort sorts only the required range.- Why this sort
.NET keeps introsort for compatibility. A 2022 proposal to replace it with pdqsort was not merged: swapping one unstable sort for another changes the order of equal values, a semi-breaking change that would make many projects update their test baselines.
The discussion also noted that performance work would be easier to continue without breaking anyone once the default is a stable sort.
C++
Chosen by API- No guarantee
std::sort - Stable
std::stable_sort
- Implementation
Generally
std::sortis introsort-based (libc++ has used an implementation with pdqsort's techniques since LLVM 16),std::stable_sortis merge-based. Details are implementation-defined.- Why this sort
The C++ standard names no algorithm; it sets requirements such as comparison counts.
std::sortmust use O(n log n) comparisons, a requirement tightened in C++11: the earlier wording, about n log n on average, was meant to allow quicksort, which can be quadratic for pathological inputs. Matt Austern pointed out that introsort guarantees O(n log n) in the worst case without extra average cost and that most implementers already did this, and the committee changed the requirement (LWG 713).std::stable_sortis allowed n log n comparisons with enough extra memory and up to n log² n without it, which admits both a buffered merge sort and an in-place merge.
C++ (Boost)
Chosen by API- No guarantee
boost::sort::pdqsort - Stable
spinsort/flat_stable_sort
- Implementation
Offers
pdqsort,spinsort, andflat_stable_sortunder their algorithm names. Boost.Sort is a separate library from the C++ standard library.- Why this sort
Boost.Sort states its goal as modern, fast, memory-efficient sorting in both stable and unstable versions.
Its documentation lays the three out by stability, extra memory, and speed: pdqsort is an unstable sort with log n extra memory; spinsort is a stable sort with n/2 extra memory that is fast on random or nearly sorted data; flat_stable_sort is a stable sort with about 1% of the data size as extra memory at 80–90% of spinsort's speed.
Dart
No guarantee- No guarantee
List.sort()
- Implementation
Implements the Dual-Pivot Quicksort from Yaroslavskiy's paper, with some improvements copied from Android's implementation. Ranges under 32 elements use insertion sort. The stable sort is left to
mergeSortinpackage:collection.- Why this sort
Dart's documentation does not say why this sort was chosen. What it does say is that
List.sort()is not guaranteed to be stable, so elements that compare as equal may end up in any order. The stable sort lives outside the core library, inmergeSortfrompackage:collection, whose documentation notes that it needs extra space of about the size of the list.
Go
Chosen by API- No guarantee
slices.Sort()/sort.Sort() - Stable
slices.SortStableFunc()/sort.SliceStable()
- Implementation
Go 1.19 (2022) rewrote the
sortalgorithm to pdqsort. Theslicespackage became standard in Go 1.21 (2023). Stable sort uses SymMerge.- Why this sort
For the unstable sort, the Go 1.19 release notes give the reason for pdqsort as being faster for several common scenarios.
For the stable sort, the notes in the source explain SymMerge as simple and provably correct on all input, needing only logarithmic extra stack space, and performing well in experiments against other stable in-place sorts.
Java
Chosen by API- Stable
Arrays.sort(Object[])/List.sort()/Collections.sort() - Not applicable
Arrays.sort(int[])/Arrays.sort(double[])/ …
- Implementation
Object arrays: the Implementation Note states Timsort. Primitive arrays: Dual-Pivot Quicksort, also stated in the Implementation Note. JDK 14 rewrote that implementation; when recursion gets too deep, it now switches to heap sort, so every input stays within O(n log n).
- Why this sort
JDK 7 brought Timsort to object arrays (JDK-6804124). The old modified merge sort always needed n extra references and beat n log n only on nearly sorted input. According to the request, Timsort is a stable, adaptive sort that needs far fewer than n log n comparisons on partially sorted arrays, ran up to 25 times faster on highly ordered data, and was comparable on random data.
For primitive arrays, the documentation says Dual-Pivot Quicksort offers O(n log n) performance on all data sets and is typically faster than traditional one-pivot quicksort.
JavaScript
Stable by default- Stable
Array.prototype.sort()
- Implementation
Engine-dependent. V8 (Chrome) uses Timsort. SpiderMonkey (Firefox) uses a merge sort. JavaScriptCore (Safari) chooses the merge order with Powersort and uses Timsort's galloping merge.
- Why this sort
ECMAScript does not choose an algorithm; it specifies what the result must satisfy (sorted order and, since ES2019, a stable sort). Because the major engines already had stable sorts, a spec change proposal made stability a requirement in ES2019.
V8's blog explains its choice of Timsort: it is stable, handles already-sorted input in O(n) with an O(n log n) worst case, and gives predictable performance where quicksort could degrade to O(n²). A stable
Array#sortwas among the oldest requests in V8's bug tracker.
Kotlin
Stable by default- Stable
sort()/sortedArray()/sortedBy()
- Implementation
On JVM, sorting delegates to
java.util.Arrays.sort. On JS, it checks at run time whether the engine’sArray.prototype.sortis stable and uses its own merge sort if needed.- Why this sort
Kotlin's documentation does not say why the implementations were chosen. What it states is that
sort()on object arrays and lists is a stable sort; primitive arrays carry no such promise. Each target delegates to its platform's standard facility, and only where stability depends on the environment, as on JS, does Kotlin fall back to its own stable sort.
PHP 8+
Stable by default- Stable
sort()/usort()
- Implementation
Quicksort-based with insertion sort for 16 elements or fewer. An original-order fallback key makes it a stable sort.
- Why this sort
PHP 8.0 (RFC “Make sorting stable”) made every sort function a stable sort. The RFC gives the reasons: a stable sort is useful when sorting complex data by only part of it, and emulating stability in userland is highly inefficient and sharply increases memory use.
For the implementation it kept
zend_sort, an unstable hybrid quicksort, and records the original order to break ties; switching to Timsort was considered as the alternative, and the chosen approach measured essentially unchanged performance when the array has no duplicates.
Python
Stable by default- Stable
list.sort()/sorted()
- Implementation
A merge sort derived from Timsort that uses the runs already in the input. Since Python 3.11, merge order is chosen with the Powersort strategy.
- Why this sort
Python adopted Timsort to use existing order and reduce comparisons. A comparison can run arbitrary Python code, so comparisons can be expensive. Tim Peters, who introduced it in Python 2.3, reported as few as n−1 comparisons on highly ordered input and similar speed to the previous samplesort on random input.
Python 3.11 changed the merge order to Powersort. The old rules were difficult to reason about, and their intended conditions had been found not to hold. In Peters’ modeling, Powersort had a lower merge cost in every case tested.
Ruby
No guarantee- No guarantee
Array#sort
- Implementation
Delegates to the platform's GNU-style
qsort_r(orqsort_s) when available; otherwise uses the bundled quicksort (qs6, 1995). The delegation dates from a 2014 change.- Why this sort
Ruby's documentation and change history do not say why this setup was chosen. What the documentation does say is that for elements that compare as equal, the order from
Array#sortis indeterminate and may be unstable. The 2014 change that routed sorting toqsort_ronly describes how configure detects the GNU version.A consequence shows up in a 2015 report (Bug #11379): equal values came out in different orders on Linux and OS X, and nobu answered that glibc's
qsort_r()is used instead ofruby_qsort(), advising that sincesort_byis not a stable sort, the original value should be part of the key.
Rust
Chosen by API- No guarantee
slice::sort_unstable() - Stable
slice::sort()
- Implementation
Since Rust 1.81 (2024), the unstable sort uses ipnsort and the stable sort uses driftsort. ipnsort is a quicksort-based sort built on pdqsort, Rust's previous unstable sort. driftsort finds runs that are already in order and merges them, and sorts the other parts with quicksort-style partitioning.
- Why this sort
Rust 1.81 replaced both sorts to improve runtime performance and compilation time. The change aimed to balance execution time, compilation time, and generated code size.
The new implementations may panic when they detect inconsistent comparisons. They do not guarantee detection of every inconsistency; even on normal return, the resulting order is unspecified. Callers must correctly implement
Ord, the trait that defines the ordering.
Swift
Stable by default- Stable
Array.sort()/sorted()
- Implementation
Timsort-based implementation.
- Why this sort
Swift 5 replaced introsort with a modified Timsort to preserve equal-key order and use existing structure in the input. CI measurements for Nate Cook’s change showed about a 33-fold speedup on an integer pyramid and a 2.3-fold speedup on sorted strings. Some cases, including random strings, became slower.
Although the implementation was stable, the documentation initially did not guarantee stability. SE-0372 in Swift 5.8 documented it as an API guarantee that callers could rely on, since every Swift runtime already had a stable sort.
Zig
Chosen by API- No guarantee
std.sort.pdq/std.mem.sortUnstable - Stable
std.sort.block/std.mem.sort
- Implementation
Per release notes: the unstable sort is pdqsort, the stable sort is block sort.
- Why this sort
Zig 0.11 (2023) split sorting into a stable sort and an unstable sort. The issue that started it (#15388) showed that Zig's
sort, a stable in-place sort, took about 2.7 times as long as Rust's unstable sort on the same input, and argued that since Zig strives for optimality, users should be able to choose better performance when stability is not a requirement.The added pdqsort combines the fast average case of randomized quicksort with the fast worst case of heapsort. The stable sort stayed block sort; Andrew Kelley held that the default sort should be stable, and the release notes advise using the unstable sort when you can, with the stable sort as the more conservative choice.
Notable Algorithms
pdqsort, Powersort, ipnsort, and driftsort are implementations or techniques adopted by standard sorts since 2017. They reduce work by using input structure and CPU behavior while preserving the required stability and worst-case bounds.
Unstable sorts: faster quicksort with a worst-case bound
pdqsort (Orson Peters, 2015): An improved introsort. Introsort switches to heapsort when recursion gets too deep, which keeps the worst case at O(n log n), but it can spend a long time before it switches. When a partition is badly unbalanced, pdqsort swaps a few elements to break up the pattern that caused it. It switches to heapsort only if bad partitions continue. It also sorts ascending, descending, and all-equal input in linear time. The original C++ implementation uses BlockQuicksort's branchless partitioning when sorting numbers with the standard comparison. Rust 1.20, Boost 1.67, Go 1.19, and Zig 0.11 adopted it. paper (arXiv) Implementation source Rust 1.20 Boost 1.67 Go 1.19 Zig 0.11
See it in SortVivo:pdqsort, pdqsort (branchless), BlockQuicksort
ipnsort (Lukas Bergdoll and Orson Peters, 2024): This unstable sort builds on Rust’s pdqsort, reworking partitioning and the sorting of short ranges. It removes pattern-breaking swaps and, like introsort, uses recursion depth to decide when to switch to heapsort. Improved pivot selection and faster processing of small types and short ranges compensate for that simpler fallback strategy. It also improves compilation time in Rust, which generates code for each element type. Rust 1.81 adopted it. ipnsort writeup Rust 1.81
See it in SortVivo:ipnsort
Stable sorts: merge sort with better merging, helped by quicksort
Powersort (Munro and Wild, 2018): Its rule chooses a nearly optimal order for merging sorted runs with little overhead. Timsort had chosen the order using conditions on the lengths of runs waiting to be merged. Verification work in 2015 showed that these conditions did not always hold. Powersort uses a simpler rule that is easier to verify, with a proof that its merge cost is nearly optimal. CPython 3.11 and JavaScriptCore adopted it. paper (arXiv) ERCIM News (2015) CPython listsort.txt CPython 3.11 JavaScriptCore
glidesort (Orson Peters, 2023): It combines Powersort’s use of existing sorted runs with a stable quicksort that benefits from repeated values. It merges sorted runs and uses quicksort on unordered parts. Grouping equal values gives average running time of O(n log k) for k distinct values. It became the basis for driftsort. Implementation source
See it in SortVivo:Glidesort
driftsort (Lukas Bergdoll and Orson Peters, 2024): glidesort is fast, but its code was too large for a standard library. In the authors' measurements, it took about 4 times as long to compile as Rust's previous stable sort and produced more than 10 times as much code. driftsort keeps glidesort's approach of merging sorted runs and sorting the rest with quicksort, along with its Powersort merge order. It drops the parts that are rarely needed or that make the code larger, which keeps compile time about the same as the previous stable sort. Rust 1.81 adopted it together with ipnsort. driftsort writeup Rust 1.81
See it in SortVivo:Driftsort
Notable techniques
Pivot selection, the sorting of short ranges, and merge order all affect overall performance. Different sorts can benefit from the same technique.
Block Partitioning: When quicksort partitions, it branches on how each element compares with the pivot. On shuffled input, the CPU cannot predict these branches, and every wrong guess makes it redo work. BlockQuicksort (Edelkamp and Weiss, 2016) records the comparison results without branching, then swaps the elements in batches. In the paper, it sorted random integers more than 80% faster than GCC's std::sort.paper (ESA 2016)
Used by:pdqsort (when sorting numbers with the standard comparison) and Rust's unstable sort from 1.20 to 1.80
Adaptive Pivot Selection: A poor pivot gives an unbalanced partition and more comparisons. The more pivot candidates a sort looks at, the less likely this becomes, so ipnsort raised the number of candidates to about √n. As a result, it stays fast even without pdqsort's pattern-breaking swaps.
Used by:pdqsort (3 candidates, or 9 for large arrays); ipnsort and driftsort (a median chosen recursively from about √n candidates)
Grouping equal values: Keys such as statuses or categories often have few distinct values. When a new pivot equals the previous pivot, equal values can be moved to one side and excluded from further sorting. With k distinct values, the pdqsort paper gives a worst-case time bound of O(nk). Rust’s API documentation describes expected running time of O(n log k) for ipnsort and driftsort. An expected time is different from a worst-case bound over all inputs.Rust API
Used by:pdqsort, ipnsort, driftsort
Sorting Networks for Small Arrays: Repeated partitioning leaves many short ranges at the end. pdqsort and introsort sort these short ranges with insertion sort. Insertion sort compares elements while it searches for where each one goes, so on shuffled input the CPU cannot predict where the search stops, and its guesses often fail. A sorting network compares a fixed set of pairs in a fixed order, whatever the input, so it can be written without branches and avoids those wrong guesses. In the ipnsort authors' measurements on random 64-bit integers, sorting short ranges became 2 to 3 times faster.
Used by:ipnsort (small types such as integers)
Bidirectional Merging: An ordinary merge checks the result of one comparison before it takes the next step, so it moves forward one step at a time. A merge from the left end and a merge from the right end do not depend on each other, so running them in turns lets the CPU work on both at once.
Used by:glidesort, driftsort (short ranges), ipnsort (short ranges of some types)
Merge Order Optimization: The more sorted runs an input has, the more the merge order changes the number of comparisons and moves. Powersort's rule finds a nearly optimal order with little extra work.
Used by:CPython 3.11, JavaScriptCore, glidesort, driftsort
Where to Go Next
What you have seen here carries over to the rest of SortVivo. Run sorts yourself, play guessing games, or trace how sorts developed.
The ways of sorting you saw have names
These are the names you will see in the Visualizer and Unsorted. Pick one to follow its movement step by step.
- Fix two neighbors in the wrong order Bubble sort
- Put the next marble into the ordered part Insertion sort
- Choose what belongs next Selection sort
- Join two ordered rows Merge sort
- Separate around a guide Quicksort
Try it hands-on
- Visualizer See the differences for yourself Pick a sort and an input order (random, nearly sorted, reversed, and more), then play it. Slow it down or pause to follow each move. Run several sorts on the same input to compare their comparison and swap counts side by side.
- Unsorted Name the sort from how it moves Each sort moves in its own way. Watch the animation and guess which sort it is. Answer questions across the map to put a scrambled world back in order.
- Reveal The further it sorts, the more of the painting appears A famous painting is cut into thin vertical strips and shuffled, and a sort puts them back in order. Name the painting before the sort finishes.
Dig deeper
- How It Works Follow each step with marbles Step forward and back through each comparison and swap, and watch the marbles move. Each sort also explains its key points and what to watch for.
- History Trace sorts in the order they appeared A timeline places each sort in the year it appeared, from the 1940s to today. Lines show which sorts grew out of which.
- Lineage See how sorts connect A relationship map, colored by kind of sort, shows which sorts were derived from which. Select a sort to see its details.