Sort Basics

Short pages explain the sorting.

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.

Standard Sorts Across Languages Standard libraries and recent implementations
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 basics
USE 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 basics
SPEED 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 optimization
SPEED 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 optimization
SPEED 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 basics
SPEED 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 optimization
TRACE 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 implementations
TRACE 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 implementations
LEARN 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 basics

What 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.

EXPERT

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.

Check your understanding

You want to order photos from oldest to newest. Which clue should decide their order?

EXPERT · Think from the conditions

The same product data is sorted by price on screen A and by product name on screen B. What changes between the screens?

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.

  1. 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
  2. Have you decided how null, NaN, text, and ties are ordered?

    Decide whether null comes first or last, how NaN behaves, 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.

  3. 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) reorders items itself and returns that same array, so const ordered = items.sort(compare) still loses the original order. To keep it, use an API that returns a new array (toSorted in JavaScript, added in ES2023, or sorted() in Python).

  4. 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::sort does not, while std::stable_sort does. 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

  5. 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 a and b, 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.
1. Inspect every record, but do not sort them all

You need the 20 highest scores from one million records. Break ties by smaller ID. There is no next page or full export. If an unseen record could rank first, how many records must you inspect, and how many must you put in order? The marbles shrink the same decision to a top three.

2. Preserve the source row

A preview and an audit log share arrival-ordered orders. Only the preview should be amount-ordered. What preserves the audit order?

3. Carry input order through stability

The API guarantees rows arrive in time order. Group by severity while preserving time order within each severity. What is the minimum sufficient key specification?

4. Fix comparator equality

A stable sort still scrambles equal-priority 2a and 2b. The comparator returns -1 even for equal priority. What should be fixed first?

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.

EXPERT

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

Check your understanding

Two marbles are 3, 1. After comparing them, you know 3 is larger. What should happen next to put them in increasing order?

EXPERT · Think from the conditions

You wanted scores in ascending order, but wrote the comparator `a.Score > b.Score`. The result is descending order. What should you fix first?

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.

Choose one answer

During insertion sort, what can you say about the leading range?

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.

  1. 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.

  1. 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 Ω.

EXPERT

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 bound f(n) c·g(n) n₀ n

For 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 bound f(n) c₁·g(n) c₂·g(n) n₀ n

The 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 f(n) c·g(n) n₀ n

Ω 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²
1010~33100
100100~66410,000
1,0001,000~9,9661,000,000
10,00010,000~132,877100,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.

Check your understanding

Going from 10 to 100 marbles, “Choose what belongs next” went from 45 to 4,950 comparisons, and “Join two ordered rows” went from about 28 to about 600. What happens to the gap between the two counts with 1,000 marbles?

EXPERT · Think from the conditions

Let the input size be n. An algorithm performs Θ(n²) comparisons and Θ(n log n) writes. Each operation costs Θ(1), and no other work grows faster. What is its total time complexity?

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.

Three elements have 3! = 6 possible orders, so the decision tree needs six leaves. Two comparisons can tell apart at most 2 × 2 = 4 orders. Telling all six apart therefore takes three comparisons in the worst case.

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.

EXPERT · Think from the conditions

There are three distinct values and six possible orders. Can one less-than comparison always identify the right order?

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.

Implementation variants grouped by stability and whether they are in-place
In-place (O(1) to O(log n)) Not in-place
Stable
  • Bubble sort Extra memory O(1)Worst-case time O(n²)

    It swaps only neighbors, so equal keys keep their order.

  • Insertion sort Extra memory O(1)Worst-case time O(n²)

    On nearly sorted input, it needs very few comparisons or moves.

  • Block sort (WikiSort) Extra memory O(1)Worst-case time O(n log n)

    It merges using only a small working area whose size does not depend on the number of elements (512 elements in SortVivo’s implementation). Its author reports that it is generally about as fast as a standard merge sort.

  • Merge sort Extra memory O(n)Worst-case time O(n log n)

    It merges the sorted halves by copying them through a work array.

  • Timsort Extra memory O(n)Worst-case time O(n log n)

    It finds runs that are already in order and merges them, so nearly sorted input is fast.

  • Powersort Extra memory O(n)Worst-case time O(n log n)

    It merges runs like Timsort, with a better rule for choosing which runs to merge next.

  • LSD Radix sort (b=10) Extra memory O(n+k)

    It uses no comparisons and distributes keys digit by digit, starting from the lowest. The k in its extra memory is the number of values one digit can take (10 when b=10). Each pass must be stable, or the order from earlier digits is lost.

Unstable
  • Quicksort Extra memory O(log n)Worst-case time O(n²)

    Its average is O(n log n). It handles the smaller part first, so its extra memory stays within O(log n) (a naive recursion can need O(n) in the worst case). It swaps distant elements, so equal keys can change order.

  • Heapsort Extra memory O(1)Worst-case time O(n log n)

    It swaps the heap's maximum with the last element, so equal keys can change order.

  • Tournament sort Extra memory O(n)Worst-case time O(n log n)

    It picks the minimum with a tournament tree, which also helps merge many sorted runs.

  • Patience sort Extra memory O(n)Worst-case time O(n log n)

    It deals elements onto piles, then merges them. The number of piles equals the length of the longest increasing subsequence.

EXPERT · Think from the conditions

You confirmed that one quicksort implementation is stable. Can you conclude that another quicksort implementation is also stable?

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, Powersort

Partition-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 sort
EXPERT · Think from the conditions

How does merge sort divide the problem and combine the results?

Same 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.

1. Predict

Given the same random array, which one do you think 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 sort
Tail 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 more
Ping-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.

The larger run stays in the original array while the merge proceeds. Each merge buffers only the shorter run, so the temporary storage never exceeds half the elements being merged.

Implementation examples: Timsort, Powersort, ShiftSort, Spinsort, Flat stable sort, Glidesort, Driftsort

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.

EXPERT · Think from the conditions

Two merge sorts make the same number of comparisons. One copies back to the original array at every level; the other alternates the roles of two buffers. Which can more readily reduce writes?

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 more
Partial 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), Timsort

Example: 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.

EXPERT · Think from the conditions

An implementation scans from the start and returns immediately when the input is already sorted. What work remains on a sorted input?

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, Driftsort
Drop-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 sort
Hybrid 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 more
Early-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, Introsort
3-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 more
Merge Order Optimization With the same runs, choosing which adjacent pair to merge first changes the total comparisons and movement.

Powersort derives a priority from the boundary between adjacent runs and avoids merging runs of very different sizes too early. This makes the merge cost (the total number of elements involved across all merges) nearly minimal.

Implementation examples: Powersort, Glidesort, Driftsort
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: Timsort

Example: 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.

1. Predict

If both sort an array made of an ascending run, a descending run, and another ascending run, which one do you think finishes first?

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), ipnsort
Sentinels (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.

EXPERT · Think from the conditions

Implementation A makes fewer comparisons than B but takes longer on the same input. Is this result possible?

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), ipnsort
Loop 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), ipnsort

Example: 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

EXPERT · Think from the conditions

A computes x = a + b and then y = c + d. B computes x = a + b and then y = x + d. In which can the CPU work on the two additions at the same time?

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, Spreadsort

Example: 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.

1. Predict

You will read 128 four-byte values (such as int). How does the number of loads from memory compare between reading from the start in order and jumping around?

EXPERT · Think from the conditions

A two-dimensional array such as int[,] in C#, [[i32; 4]; 3] in Rust, or [3][4]int in Go stores all the values of row 1 in memory, then all the values of row 2, and so on. To add up all the values, which order is more likely to need fewer loads from memory?

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

VQSort, x86-simd-sort

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

EXPERT · Think from the conditions

Which is easier to speed up with SIMD?

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)

Java Arrays.parallelSort, C++ std::sort (std::execution::par)
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.

EXPERT · Think from the conditions

Which is more likely to sort faster on a GPU?

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.

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
EXCHANGE SELECTION INSERTION MERGE HEAP PARTITION DISTRIBUTION
Structure
NETWORK TREE
Other
JOKE
EXPERT · Think from the conditions

Merge sort and heapsort are both comparison sorts that finish in O(n log n) in the worst case. What else can be said to be the same?

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.
1. Choose a defended path from requirements

Stability is unnecessary, memory should stay small, and externally supplied patterns must not cause extreme latency. What strategy should you investigate first?

2. Separate contract from current implementation

A source says version X adopted ipnsort. Which design note is safe for callers?

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.

1998 2000 2002 2004 2006 2008 2010 2012 2014 2016 2018 2020 2022 2024 2026
2026 DartDual-Pivot Quicksort · Confirmed in Dart 3.11.4 (2026); adoption year not verified

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 / ThenBy sort element positions and break key ties by original position to preserve stability. When followed by Skip / 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::sort is introsort-based (libc++ has used an implementation with pdqsort's techniques since LLVM 16), std::stable_sort is merge-based. Details are implementation-defined.

Why this sort

The C++ standard names no algorithm; it sets requirements such as comparison counts. std::sort must 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_sort is 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, and flat_stable_sort under 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 mergeSort in package: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, in mergeSort from package: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 sort algorithm to pdqsort. The slices package 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()Required by the spec
  • Not applicable Arrays.sort(int[]) / Arrays.sort(double[]) / …Equal primitives are indistinguishable
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()Since ES2019
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#sort was among the oldest requests in V8's bug tracker.

Kotlin

Stable by default
  • Stable sort() / sortedArray() / sortedBy()Object arrays and lists
Implementation

On JVM, sorting delegates to java.util.Arrays.sort. On JS, it checks at run time whether the engine’s Array.prototype.sort is 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()Since PHP 8.0
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 (or qsort_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#sort is indeterminate and may be unstable. The 2014 change that routed sorting to qsort_r only 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 of ruby_qsort(), advising that since sort_by is 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()Documented by SE-0372 (Swift 5.8)
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.

EXPERT · Think from the conditions

A runtime update changes the internal sorting implementation. Your application requires equal-key elements to keep their original order. What should you check first?

EXPERT · Think from the conditions

The official release note says the new implementation is faster on common inputs. You measured one input yourself, and it got faster. What can you conclude?

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

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

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

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

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

EXPERT · Think from the conditions

Orders must be sorted by amount, preserving arrival order when amounts are equal. Extra working memory is available. Which of Rust 1.81’s sorts, ipnsort or driftsort, meets this requirement?

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.

Sorting is
a spectacle.
Algorithms at work, in a daily vertical feed. Watch, play, challenge — and go deeper when you're ready.
Loading 0%
An unhandled error has occurred. Reload 🗙