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 you search using the same order, 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. Watch how they move.
There is more than one way to put things back in order. Let's see how different sorts make progress.
One step deeper: Why do computers sort?
Using the order to search can narrow the range it has 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.
Before You Call a Sorting API
We have seen sorting as putting elements into an order that fits a purpose. Now let us look at what to decide before calling a sorting API in application code.
Before choosing an algorithm name, check the result the application needs and the contract of the API. Decide these six items in order to tell whether a standard full sort fits or another tool is the better match.
Do you need a complete order of every item?
If the result does not need to decide which of every pair of elements comes first, a full sort can be unnecessary work. Look first for the API or data structure that directly matches the result.
- 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
Does the key or comparator define a consistent order?
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 changes the input also changes the order that code sees. Check the signature and documentation to learn whether the input changes or the API returns a new sequence.
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. Read that guarantee from the API instead of inferring it from an algorithm name.
Are memory, comparisons, or key extraction constrained?
Check not only whether a temporary array fits, but also whether text ordering or key calculation is expensive. An expensive key can be better extracted and stored once instead of recomputed on every comparison.
Are you relying on an API guarantee or today's implementation?
Correctness properties such as stability, worst-case complexity, and mutation must come from the API contract. An internal implementation such as pdqsort or ipnsort can explain performance, but it is not necessarily a promise that survives future versions.
See the error-prone boundary in code
Which API keeps the input unchanged? Compare JavaScript's `sort` and `toSorted`.
const ordered = items.toSorted(compare);
items.sort(compare);
toSorted returns a new array, while sort rearranges items itself. Looking only at the returned value hides this difference.
What does a sorting API promise?
When choosing a sorting API, check these four parts of its contract. Some APIs leave the order of equal keys unspecified. Others, including APIs named stable_sort, explicitly promise to preserve that order.
- Order
- A key or comparator defines which item comes first.
- Same elements
- Nothing is lost or invented; only the order changes.
- Equal keys
- Stability says whether items with equal keys keep their original relative order.
- Result shape
- Some APIs mutate the input, while others return a new ordered sequence. One language often has both, as with
sortandtoSortedin JavaScript orlist.sort()andsorted()in Python.
What a comparator must guarantee
Some languages let callers supply the ordering. A key function extracts a value, as in Python's sorted(key=...) or C#'s OrderBy(x => x.Score). A comparator receives two elements and reports which comes first. JavaScript's sort accepts a comparator.
players.sort((a, b) => a.score - b.score);
The language also decides what happens when you pass nothing. JavaScript's Array.prototype.sort converts elements to strings before ordering them when the comparator is omitted, so [1, 2, 10] comes back as [1, 10, 2]. For when you want numeric order, the language lets you pass a comparator.
[1, 2, 10].sort();
[1, 2, 10].sort((a, b) => a - b);
A comparator must give a consistent answer about which element comes first. Check these three rules.
- Consistent
- Comparing the same two items always gives the same answer. No randomness, no clock, no state that changes while the sort is running. Swapping the arguments has to agree too: it must never report that a precedes b and that b precedes a.
- Transitive
- If a comes before b and b comes before c, then a comes before c. Floating point error and rounded scores are common ways this breaks.
- Ties stay consistent
- Two items where neither precedes the other are tied. If a ties b and b ties c, then a must tie c. Comparing with a tolerance (equal when the difference is below a threshold) is the classic way to break this.
A wrong comparator does not only fail to put things in order; it can raise an exception. Java's Arrays.sort and Collections.sort, for example, throw IllegalArgumentException (Comparison method violates its general contract!) when they detect a violation.
Most languages ship a sorting algorithm in their standard library. What stays on the calling side is which key to order by, and whether the comparator you hand over holds to the three rules above.
A 30-second check before calling sort
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.
What SortVivo observes
SortVivo mainly observes read, write, comparison, swap, and move events as a sort progresses. Some algorithms also use other kinds of work. Counting these operations is how the cost of a sort is measured.
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
The same algorithm may use fewer comparisons but more movement. Its measured cost depends on what you count.
How does each sort make progress?
Different sorts put different parts of the row in order after each step. Watch what falls into place, not only which marble moves.
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 more (challenge) Carry two ways of making progress to the end, or put a different row in order yourself.
What happens if you keep going?
Every card above showed a single step. Repeat that one step, and how far does a mixed-up row get? Here are two of them, each carried all the way to the end.
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.
Your turn
Now you decide. Use the same move with different numbers: put the next marble into the ordered part. You will choose where it goes.
-
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?
With only a few marbles, every sort finishes quickly. With many marbles, the number of comparisons and moves can become very different.
A few marbles
The counts are close, so both finish quickly
Many marbles
More marbles create a much bigger gap in the counts
How the number of comparisons and moves grows when we add more marbles is called time complexity.
Each way of growing has its own name. Check EXPERT if you're curious.
These growth rates are written with the symbols O, Θ and Ω.
Compare growth with numbers
Time complexity expresses the difference we just saw as a relationship between the input size n and the number of operations. Asymptotic notation does not give the actual number of seconds. It helps us compare the broad trend as more marbles are added.
Which operation you count is decided up front. For sorting it is usually either the number of comparisons or the number of element moves, and the same algorithm can lead to different conclusions depending on which one you counted. Below, comparisons are what is counted unless stated otherwise.
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.
Big-O Is Used Most Often
Many sorting resources conventionally use Big-O for brevity even when describing a tight growth rate. 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 |
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.
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 comparisons are O(n), if shift writes are O(n²), the overall complexity is O(n²). As the input grows, the O(n²) work dominates the total cost.
- 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.
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 Θ(n · d) for d digits. 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, a sort can reduce comparisons and movement by using order already present in the input. Adaptive sorts use this idea.
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 moves elements within the original array instead of allocating a temporary array proportional to the input size. It usually needs O(1) working storage, although some implementations use about O(log n) memory to record recursive calls. A sort with a temporary array may need O(n) extra memory.
On embedded devices or in memory-constrained environments, allocating O(n) extra memory is often impractical. In such cases, in-place sorts become a strong choice.
Stability
A stable sort preserves the relative order of elements with equal keys. When sorting students by score, a stable sort keeps “Tom” and “Sam” (both 90) in their original order.
This is what pays off when you want to order by two keys. Sort by name first, then sort by score with a stable sort, and within each score the names stay in order. Stacking stable sorts is enough to build a multi-level ordering. An unstable sort loses the first pass, so the two keys have to be handled together inside the comparator instead.
Stable Sort
Equal keys (90) keep their original order: Tom stays before Sam.
Unstable Sort
Equal keys (90) may swap: Sam moved before Tom.
Compare Implementation Variants with Two Questions
Does it keep the order of equal keys? Does it sort without extra memory? Those two answers split the implementation variants into four groups. The amount of extra memory varies by implementation.
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.
| In-place (O(1) to O(log n)) | Not in-place (see exact amount below) | |
|---|---|---|
| Stable | ||
| Unstable |
| Algorithm / variant | Stable | In-place | Extra memory | Notes |
|---|---|---|---|---|
| Bubble sort | ✅ | ✅ | O(1) | Stable & in-place, but slow |
| Insertion sort | ✅ | ✅ | O(1) | Fast on small inputs |
| Block sort (WikiSort) | ✅ | ✅ | O(1) | Stable, in-place, and O(n log n), with larger constant costs |
| Merge sort | ✅ | ❌ | O(n) | O(n) extra memory |
| Quicksort | ❌ | ✅ | O(log n) | Low extra memory but unstable |
| Heapsort | ❌ | ✅ | O(1) | O(n log n) worst-case |
| Timsort | ✅ | ❌ | O(n) | Adaptive + O(n) memory |
| Powersort | ✅ | ❌ | O(n) | Improves the merge order used by Timsort-style sorts |
| LSD Radix sort (b=10) | ✅ | ❌ | O(n+k) | Stability required for correctness |
| Tournament sort | ❌ | ❌ | O(n) | O(n) tournament tree; the tree itself is the point |
| Patience sort | ❌ | ❌ | O(n) | O(n) piles; also yields the longest increasing subsequence |
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: Embed the original position in the comparator. Layer stability on top of an unstable sort. Design the comparator as follows: compare by key first; if the keys are equal, compare by original position. This imposes a total order so the algorithm never sees tied keys. Stability is guaranteed by the comparison specification, not the algorithm.
.NET's OrderBy / ThenBy follow strategy 2. Rather than using a stable sort, .NET embeds the original position into the comparison function so the order is fully decided. The sort inside can change, and stability still holds as an API promise.
To see which of these two strategies your own language takes: Standard Sorts Across Languages
How algorithms are built
A sort is built from choices about how to divide a problem and combine the results. Top-down and bottom-up processing, and merge-based and partition-based designs, follow different flows.
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.
MergeSort, 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 sortWhy implementation changes speed
Algorithms with the same average O(n log n) complexity can run at different speeds. Recursion means that a function calls itself on a smaller problem. A buffer is temporary storage for values used during processing. Changing recursion, buffers, and copying can reduce calls and memory use.
QuickSort and HeapSort both average O(n log n). Given the same random input, which one finishes first? The Sortube race scales each lane by its measured time, so the lane that reaches the end first is the one that actually ran faster. Make your prediction, then compare the two measured times. The result holds for this input, this implementation, and this machine.
Open the QuickSort vs HeapSort race in Sortube
Recursion → Iteration Replacing recursion with a loop can reduce function calls and stack use.
Stack use depends on the implementation. Recursive Merge Sort normally uses O(log n), while a naive QuickSort can reach O(n) after badly unbalanced partitions.
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), DualPivot 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 merging needs at most about half an array of temporary storage.
Adapting to the Input
Pruning and Early Termination
Find processing that is not needed and skip it.
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, Pattern-defeating quicksort, PDQ sort (branchless)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: Introsort, Drop-Merge sortRange 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), TimsortPattern Adaptation and Strategy Switching
Techniques that detect structure in the input and choose a strategy accordingly.
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, Timsort, Powersort, ShiftSort, Spinsort, Glidesort, Driftsort, Strand sortDrop-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 InsertionSort 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 more3-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), DualPivot Quicksort, Quicksort (Stable), Pattern-defeating quicksort, PDQ sort (branchless)Merge Order Optimization With the same runs, choosing which adjacent pair to merge first changes the total comparisons and movement.
Low-Level Optimization
A CPU keeps recently used data in fast caches, predicts which way a branch will go, and overlaps instructions that do not depend on one another. Low-level optimization arranges work to use these caches, predictions, and instruction pipelines well.
Block Partitioning Separate sequential comparison from batched exchange to reduce unpredictable branches and scattered movement.
Read a block, collect the positions to exchange, then perform those exchanges together. Sequential reads also make better use of the cache.
Implementation examples: BlockQuickSort, PDQ sort (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), DualPivot Quicksort, Quicksort (Stable), Quicksort (Bidirectional Stable), BlockQuickSort, Introsort, Introsort (.NET) and 4 moreGalloping (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: TimsortSorting Networks for Small Arrays 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.
Implementation examples: Block sort (WikiSort), Glidesort, Driftsort, Introsort, std::sort (LLVM), IpnsortBidirectional 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), IpnsortSentinels (Unguarded Insertion) 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.
Implementation examples: Pair insertion sort, Introsort, Pattern-defeating quicksort, PDQ sort (branchless), std::sort (LLVM)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. A pdqsort variant that reduces branches processes eight iterations of its offset-generation loop at once.
Implementation examples: Driftsort, PDQ sort (branchless), IpnsortBit 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 heapSort, LSD Radix sort (b=4), LSD Radix sort (b=256), MSD Radix sort (b=4), American flag sort, SpreadsortCPU-Friendly Design The same comparison count can take different time because branches and memory access differ.
Comparison count is not the only source of speed. Small-range code, fewer branches, and sequential memory access also matter. Pivot selection is different: it prevents skewed partitions and reduces comparisons. Practical sorts combine several of these techniques.
Specialise small ranges
Because simple routines outperform the general algorithm at small scale, dedicated fast paths reduce constant-factor overhead. (→ Sorting Networks, Hybrid Switching)
Implementation examples: Block sort (WikiSort), Glidesort, Driftsort, Introsort, std::sort (LLVM), Ipnsort, std::stable_sort (LLVM), Timsort, Powersort, Spinsort and 10 moreReduce branch mispredictions
Irregular branches stall the CPU pipeline. Separating comparison and swap phases and stabilising branch patterns reduces mispredictions. (→ Block Partitioning, Sentinels)
Implementation examples: BlockQuickSort, PDQ sort (branchless), Pair insertion sort, Introsort, Pattern-defeating quicksort, std::sort (LLVM)Distribute dependencies
Long dependency chains prevent the CPU from filling its pipeline. Multiple cursors and bidirectional processing keep chains short. (→ Bidirectional Merging, Loop Unrolling)
Implementation examples: Glidesort, Driftsort, Quicksort (Bidirectional Stable), Ipnsort, PDQ sort (branchless)Improve memory locality
Accessing nearby memory together maximises cache utilisation. Block processing and half-array copies keep working data in cache. (→ Block Partitioning, Galloping, Bit Packing)
Implementation examples: BlockQuickSort, PDQ sort (branchless), Timsort, Weak heapSort, LSD Radix sort (b=4), LSD Radix sort (b=256), MSD Radix sort (b=4), American flag sort, SpreadsortLimited but Practical Techniques
Parallelism divides work across CPU cores or a GPU. SIMD applies one instruction to several values at once. Both work best on large inputs or keys the CPU can compare directly.
Parallelism via GPU and Threads Large inputs can repay startup and synchronization costs, but that tradeoff does not fit a general sort's default path.
Sorting networks such as Bitonic Sort divide well across workers and are also used on GPUs. Java's Arrays.parallelSort and C++ std::sort with an execution policy make parallelism explicit. Small inputs cannot repay the cost of starting and synchronizing workers.
SIMD Fixed-width keys can be processed several at once, but arbitrary types and caller comparators are a poor fit.
Google Highway's VQSort and Intel's x86-simd-sort are production examples. 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-width keys.
NumPy 1.25 release notes
NumPy 2.0 release notes
Six ways to compare sorts
Compare a sort by the information it uses, how it makes progress, its design strategy, data and execution structure, guaranteed properties, and implementation techniques. These views reveal differences that one label cannot.
Decision information
Does it learn order through comparisons, or use values, digits, or keys directly? The former is called a comparison sort.
How it makes progress
Does it insert, select, merge, partition, extract from a structure, or distribute?
Design strategy
How the problem becomes smaller. Divide-and-conquer splits it, decrease-and-conquer removes one part at a time, and transform-and-conquer changes its form first.
Data and execution
Array, linked structure, heap, network, in-place storage, or a separate buffer.
Guaranteed properties
Stable (elements with equal keys keep their order), adaptive (the more ordered the input already is, the sooner it finishes), worst-case bounded, fixed schedule, or hybrid.
Implementation techniques
Pivot selection, detecting runs (stretches already in order in the input), small-sort fallback, block processing, and other engineering choices.
Merge and partition are divide-and-conquer examples. Insertion and selection are decrease-and-conquer examples. A heap is a transform-and-conquer example. Recursion versus iteration is a separate choice.
Compare QuickSort and Timsort
QuickSort and Timsort are both comparison sorts. Are they alike in other ways? Compare them across the six dimensions.
| Dimension | Quicksort | Timsort |
|---|---|---|
| Decision information | Comparison | Comparison |
| How it makes progress | Partition | Merge |
| Design strategy | Divide and conquer (split in half from the top) | Iterative (combine runs from the bottom; there is no dividing step) |
| Data and execution | Array, in place | Array with a side buffer (at most half the elements) |
| Guaranteed properties | Unstable. A naive implementation is O(n²) in the worst case | Stable and adaptive. O(n log n) even in the worst case |
| Implementation techniques | Pivot selection, insertion sort on short stretches, heapsort fallback when recursion runs deep | Run detection, galloping, insertion sort to extend short runs, merge order tuning |
They share only one point: both use comparisons to decide order. Their progress, strategy, memory use, and guarantees differ. The label "comparison sort" does not show those differences.
Reading SortVivo's categories
Sorts can be classified by how they make progress, the data they use, or the properties they guarantee.
How the sort progresses
Data or execution model
Property
Other
A traditional vocabulary
Exchange, selection, insertion, merging, and distribution are names you still meet in books and papers. Modern production sorts combine several of them, so one name is often not enough to describe an implementation.
Reference: Donald E. Knuth, The Art of Computer Programming, Volume 3, Section 5.2.
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 new implementation may not be better in every way. Standard sorts balance these needs.
- API contract and compatibility Whether it preserves stability, avoids a breaking change, retains observable equal-key order, and respects behavior existing code depends on.
- Worst case and robustness Whether complexity stays bounded on skewed or deliberately constructed inputs and has a predictable limit. This also matters for denial-of-service resistance when an attacker controls the input.
- Real workloads Whether it saves work on ordered stretches already present in the input (runs), duplicate keys, skew, or elements that are expensive to compare in that language.
- Memory and code size Whether temporary storage, records of recursive calls, and executable size added when generic code is specialized for each type fit the target environment.
- CPU and specialization Whether it benefits from caches, branches, data movement, fast paths for keys stored in a fixed number of bits such as integers, and types or comparisons known at compile time.
- Portability and maintenance Whether it works across targets, is tractable to verify, and remains maintainable at standard-library scale.
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 detecting features of the input and changing the processing. A sort may reuse ordered runs or group duplicate values. API guarantees, memory, and portability are decided separately.
Stable and unstable sorts sit behind separate APIs for a different reason: they let callers choose between different guarantees and costs.
Explain a surprising Sortube result
Compare the input, implementation, and measurements from this race to explain why the times differ. Keep operating requirements and API guarantees separate from that measurement.
Try the questions Four questions about result scope, three observations, deliberately difficult inputs, and API versus implementation.
Notable Algorithms
Recent sorts each solve a different problem. Read them by the problem each one set out to solve.
ipnsort and driftsort became Rust's standard sorts in version 1.81. ipnsort is unstable and resists partition imbalance. driftsort is stable and combines run detection, merging, and divide-and-conquer. Rust 1.81 release ipnsort writeup driftsort writeup
PowerSort (Munro and Wild, 2018) chooses the order for merging existing runs. It reduces comparisons while preserving stability and adaptivity. CPython 3.11 adopted it. paper (arXiv) CPython listsort.txt
Glidesort (Orson Peters, 2023) combines run-detecting merges with QuickSort-style partitioning. It is stable, handles duplicate-heavy input and tight memory, and became a basis for driftsort. Implementation source
Run it in SortVivo:Glidesort
pdqsort (Orson Peters, 2015) is an unstable sort that changes strategy when partitions stay skewed. The fallback keeps its worst case at O(n log n). Boost, Go, and Zig use it. paper (arXiv) Implementation source
Run it in SortVivo:Pattern-defeating quicksort, PDQ sort (branchless)
Standard Sorts Across Languages
| Language | UnstableSort API | StableSort API | Guarantee | Implementation |
|---|---|---|---|---|
| C | qsort() |
- | Not specified | 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. Official documentation POSIX.1-2024 qsort_r |
| C++ | std::sort |
std::stable_sort |
std::sort unspecified; std::stable_sort stable |
Generally std::sort is introsort-based, std::stable_sort is merge-based. Details are implementation-defined. Official documentation (sort) Official documentation (stable_sort) |
| C++ (Boost) | boost::sort::pdqsort |
spinsort / flat_stable_sort |
Chosen by which API you call | Offers pdqsort, spinsort, and flat_stable_sort under their algorithm names. Implementation source |
| Rust | slice::sort_unstable() |
slice::sort() |
sort_unstable unstable; sort stable | Since Rust 1.81 (2024), unstable uses ipnsort and stable uses driftsort. Release notes Implementation source (ipnsort) Implementation source (driftsort) |
| Go | slices.Sort() / sort.Sort() |
slices.SortStableFunc() / sort.SliceStable() |
Chosen by which API you call | Go 1.19 (2022) rewrote the sort algorithm to pdqsort. The slices package became standard in Go 1.21 (2023). Stable sort uses SymMerge. Release notes Implementation source (pdqsort) Implementation source (SymMerge) |
| Zig | std.sort.pdq / std.mem.sortUnstable |
std.sort.block / std.mem.sort |
Chosen by which API you call | Per release notes: unstable is pdqsort, stable is block sort. Release notes Implementation source (pdqsort) Implementation source (Block Sort) |
| Java (primitives) | Arrays.sort(...) |
- | Not applicable; equal primitives are indistinguishable | Dual-Pivot Quicksort, stated in the Implementation Note. Official documentation Implementation source |
| Java (objects) | - | Arrays.sort(Object[]) |
Stable, required by the spec | Implementation Note states TimSort. Official documentation Implementation source |
| .NET / C# | Array.Sort() |
Enumerable.OrderBy() / ThenBy() |
Array.Sort unstable; OrderBy / ThenBy stable |
Array.Sort() is an introspective sort. OrderBy / ThenBy is QuickSort, deciding equal keys by their original position. sort src LINQ src |
| Python | - | list.sort() / sorted() |
Stable | 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. Official documentation Design discussion Code change Implementation source (Timsort 3.10) Implementation source (PowerSort 3.14) |
| JavaScript | - | Array.prototype.sort() |
Stable since ES2019 | Engine-dependent; V8 uses TimSort. MDN V8 Implementation source (V8) |
| Swift | - | Array.sort() / sorted() |
Stable, documented by SE-0372 in Swift 5.8 | TimSort-based implementation. Official documentation Implementation source |
| Kotlin | - | sort() / sortedArray() / sortedBy() |
Stable | Varies by target. On JVM, delegates to java.util.Arrays.sort. Official documentation Implementation source (JavaScript) |
| Dart | List.sort() |
- | Not guaranteed | Dual-Pivot Quicksort. package:collection also provides Merge Sort. Official documentation Implementation source |
| PHP 8+ | - | sort() / usort() |
Stable since PHP 8.0 | QuickSort-based with InsertionSort for 16 elements or fewer, stabilized via an original-order fallback key. Official documentation Implementation source |
| Ruby | Array#sort |
- | Not guaranteed | Delegates to the platform's qsort_r / qsort_s when available; otherwise uses a built-in QuickSort. Official documentation Implementation source |
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.
Why Each Language Chose This Sort
Languages give different priority to API guarantees, compatibility, speed, and memory.
- C
- C standardizes a portable
qsort()interface and leaves the implementation to each platform. It does not specify the internal algorithm or the order of equal values. Official documentation - C++
- C++ specifies complexity and results, then leaves the concrete algorithm to each implementation. Implementations can change while preserving the API contract. Official documentation (
sort) Official documentation (stable_sort) - C++ (Boost)
- Boost.Sort exposes APIs named after concrete algorithms. Callers can select an implementation from properties such as stability and resistance to difficult inputs. Implementation source
- Rust
- Rust 1.81 changed sorting to improve standard-library speed and quality. Its release notes report substantial gains for sorting and partial selection, which finds only the elements at needed ranks. Release notes Official documentation
- Go
- Go's unstable path keeps O(n log n) even on skewed inputs. Its stable path uses SymMerge to limit extra memory while remaining straightforward to verify. Implementation source Implementation notes
- Zig
- Zig added a faster API for callers that do not need stability while preserving the existing stable, in-place guarantee. Separate APIs let callers choose between guarantees and speed. Release notes Design discussion #15388 Design discussion #11117
- Java (primitives)
- Primitive arrays do not distinguish equal values, so Java can prioritize speed over stability. Its documentation describes Dual-Pivot Quicksort as generally faster than the older one-pivot form. Official documentation
- Java (objects)
- Java guarantees stability for object arrays. TimSort fits that contract and can reduce comparisons on partially ordered input. Official documentation
- .NET / C#
- Some code depends on the deterministic order of equal values produced by
.NETArray.Sort(). Changing to another unstable implementation can therefore break compatibility. Design discussion Code change - Python
- Python 3.11 kept stability and adaptivity but changed the merge order to PowerSort's policy. The goal was fewer comparisons. Implementation notes Design discussion
- JavaScript
- ECMAScript specifies observable results and leaves the algorithm to each engine. When ES2019 required stability, engines such as V8 moved to stable implementations. Official documentation Implementation notes
- Swift
- Swift turned the already stable behavior of
sort()into a documented API guarantee. Callers can now rely on the order of equal keys. Specification proposal SE-0372 Developer discussion - Kotlin
- Kotlin keeps one stability guarantee while delegating implementation to JVM, JavaScript, and Native. This reuses each platform's standard facilities. Official documentation
- Dart
- Dart's
List.sort()does not guarantee stability and uses an in-place Dual-Pivot Quicksort. Callers that need stability use a different API. Implementation source - PHP 8+
- PHP 8.0 kept its existing sort and broke ties by original position. This added stability with a small implementation change. Change proposal
- Ruby
- Ruby uses an operating-system sort function when available and a built-in implementation otherwise. Portability takes priority over one uniform algorithm. Implementation source
Explore More
The ways of sorting you saw have names
These are the names you will see in the Visualizer and the quiz. 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
Move the marbles and watch how each sort's speed and motion are different.
Run one input through several sorts at once and compare their movement and operation counts side by side.
QuizTry a quiz to check what you discovered. You can try again as many times as you like.
Test whether you can identify how a sort makes progress from its movement, and whether the complexity and stability calls come naturally.
How It WorksPick a way of moving the marbles that caught your eye and follow it one step at a time.
Follow a single implementation step by step and see where pivot selection and run detection actually pay off.
HistoryFollow the new ideas that grew from the earliest sorts to today's sorts.
Trace when each technique appeared and how it carried over into today's implementations.