More Sorts
Each of these sorts exists because a classic has a weak spot. Insertion sort moves values one slot at a time, so shell sort moves them in strides first and finishes with a gap of one on a nearly sorted array. Quicksort chokes on duplicates, so three-way partitioning gathers every copy of the pivot into a middle zone that is final after one pass. Comparison sorts cannot beat n log n, so bucket sort uses the values themselves to scatter them into ranges and sorts each small bucket. Merge sort ignores order that is already there, so timsort finds the ascending runs in the data, extends short ones with insertion sort, and merges runs instead of single elements. Introsort is what std::sort actually runs: quicksort, but a range of four or fewer values goes to insertion sort, and any stretch that has been partitioned 2 log n times without finishing is handed to heap sort, so bad pivots can slow it down but never make it quadratic. Randomized quicksort takes the other route to a guarantee: choose the pivot at random, and no input can be arranged to be slow on every run. The result compares the same input under thirty seeds, which is what expected running time means: an average over the algorithm's own coin flips, not over inputs.
Introsort, the sort behind C++'s std::sort. It is quicksort with two escape hatches. A range of 4 or fewer values goes to insertion sort, which beats everything at that size. And each recursive call spends one unit of a depth budget, 2 log2 n = 6 here; a range that arrives with the budget spent is handed to heap sort, because that many partitions without finishing means the pivots have been bad and quicksort is sliding towards O(n^2).
Check your understanding
The player pauses before each decision in this run and asks what happens next. Here are all 7, with their answers.
11 values, depth budget 6. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
10 values, depth budget 5. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
9 values, depth budget 4. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
8 values, depth budget 3. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
7 values, depth budget 2. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
6 values, depth budget 1. What does introsort do with this range?
Answer: Partition it. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
5 values, depth budget 0. What does introsort do with this range?
Answer: Heap sort, the depth budget is spent. Size at most 4 means insertion sort; a spent budget means heap sort; otherwise partition.
How it runs, step by step
Introsort, the sort behind C++'s std::sort. It is quicksort with two escape hatches. A range of 4 or fewer values goes to insertion sort, which beats everything at that size. And each recursive call spends one unit of a depth budget, 2 log2 n = 6 here; a range that arrives with the budget spent is handed to heap sort, because that many partitions without finishing means the pivots have been bad and quicksort is sliding towards O(n^2).
Introsort on 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 with a depth budget of 6.
Indices 0 to 10, 11 values, depth budget 6. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 10 at depth 6.
Partition indices 0 to 10 around the last value, 11. i marks where the next smaller value will go; every value at most 11 is swapped there as the scan passes it.
Partition 0 to 10 around 11.
1 is at most 11: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 11: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 11: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 11: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 11: swap it into index 4 and move i to 5.
5 moves to index 4.
6 is at most 11: swap it into index 5 and move i to 6.
6 moves to index 5.
7 is at most 11: swap it into index 6 and move i to 7.
7 moves to index 6.
8 is at most 11: swap it into index 7 and move i to 8.
8 moves to index 7.
9 is at most 11: swap it into index 8 and move i to 9.
9 moves to index 8.
10 is at most 11: swap it into index 9 and move i to 10.
10 moves to index 9.
Swap the pivot into index 10, its final place: 10 values on its left, 0 on its right.
11 placed at index 10.
Indices 0 to 9, 10 values, depth budget 5. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 9 at depth 5.
Partition indices 0 to 9 around the last value, 10. i marks where the next smaller value will go; every value at most 10 is swapped there as the scan passes it.
Partition 0 to 9 around 10.
1 is at most 10: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 10: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 10: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 10: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 10: swap it into index 4 and move i to 5.
5 moves to index 4.
6 is at most 10: swap it into index 5 and move i to 6.
6 moves to index 5.
7 is at most 10: swap it into index 6 and move i to 7.
7 moves to index 6.
8 is at most 10: swap it into index 7 and move i to 8.
8 moves to index 7.
9 is at most 10: swap it into index 8 and move i to 9.
9 moves to index 8.
Swap the pivot into index 9, its final place: 9 values on its left, 0 on its right.
10 placed at index 9.
Indices 0 to 8, 9 values, depth budget 4. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 8 at depth 4.
Partition indices 0 to 8 around the last value, 9. i marks where the next smaller value will go; every value at most 9 is swapped there as the scan passes it.
Partition 0 to 8 around 9.
1 is at most 9: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 9: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 9: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 9: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 9: swap it into index 4 and move i to 5.
5 moves to index 4.
6 is at most 9: swap it into index 5 and move i to 6.
6 moves to index 5.
7 is at most 9: swap it into index 6 and move i to 7.
7 moves to index 6.
8 is at most 9: swap it into index 7 and move i to 8.
8 moves to index 7.
Swap the pivot into index 8, its final place: 8 values on its left, 0 on its right.
9 placed at index 8.
Indices 0 to 7, 8 values, depth budget 3. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 7 at depth 3.
Partition indices 0 to 7 around the last value, 8. i marks where the next smaller value will go; every value at most 8 is swapped there as the scan passes it.
Partition 0 to 7 around 8.
1 is at most 8: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 8: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 8: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 8: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 8: swap it into index 4 and move i to 5.
5 moves to index 4.
6 is at most 8: swap it into index 5 and move i to 6.
6 moves to index 5.
7 is at most 8: swap it into index 6 and move i to 7.
7 moves to index 6.
Swap the pivot into index 7, its final place: 7 values on its left, 0 on its right.
8 placed at index 7.
Indices 0 to 6, 7 values, depth budget 2. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 6 at depth 2.
Partition indices 0 to 6 around the last value, 7. i marks where the next smaller value will go; every value at most 7 is swapped there as the scan passes it.
Partition 0 to 6 around 7.
1 is at most 7: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 7: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 7: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 7: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 7: swap it into index 4 and move i to 5.
5 moves to index 4.
6 is at most 7: swap it into index 5 and move i to 6.
6 moves to index 5.
Swap the pivot into index 6, its final place: 6 values on its left, 0 on its right.
7 placed at index 6.
Indices 0 to 5, 6 values, depth budget 1. Big enough and budget left: partition it and recurse on both sides.
Range 0 to 5 at depth 1.
Partition indices 0 to 5 around the last value, 6. i marks where the next smaller value will go; every value at most 6 is swapped there as the scan passes it.
Partition 0 to 5 around 6.
1 is at most 6: swap it into index 0 and move i to 1.
1 moves to index 0.
2 is at most 6: swap it into index 1 and move i to 2.
2 moves to index 1.
3 is at most 6: swap it into index 2 and move i to 3.
3 moves to index 2.
4 is at most 6: swap it into index 3 and move i to 4.
4 moves to index 3.
5 is at most 6: swap it into index 4 and move i to 5.
5 moves to index 4.
Swap the pivot into index 5, its final place: 5 values on its left, 0 on its right.
6 placed at index 5.
Indices 0 to 4, 5 values, depth budget 0. The budget is spent: 6 partitions have not finished this stretch, so the pivots are degrading. Heap sort finishes it in O(k log k) no matter what.
Range 0 to 4 at depth 0.
Heap sort takes over indices 0 to 4. Treat the range as a binary heap, the value at 0 as the root, and sift every internal node down so the largest value rises to the front.
Heap sort on 5 values.
Build the heap: 2 at index 1 is smaller than its child 5, so they swap and the sift continues from index 4.
Swap 2 and 5.
Build the heap: 1 at index 0 is smaller than its child 5, so they swap and the sift continues from index 1.
Swap 1 and 5.
Build the heap: 1 at index 1 is smaller than its child 4, so they swap and the sift continues from index 3.
Swap 1 and 4.
The root holds the largest remaining value, 5: swap it to index 4, the last unsorted slot, and shrink the heap by one.
5 moves to index 4.
Restore the heap: 2 at index 0 is smaller than its child 4, so they swap and the sift continues from index 1.
Swap 2 and 4.
The root holds the largest remaining value, 4: swap it to index 3, the last unsorted slot, and shrink the heap by one.
4 moves to index 3.
Restore the heap: 1 at index 0 is smaller than its child 3, so they swap and the sift continues from index 2.
Swap 1 and 3.
The root holds the largest remaining value, 3: swap it to index 2, the last unsorted slot, and shrink the heap by one.
3 moves to index 2.
Restore the heap: 1 at index 0 is smaller than its child 2, so they swap and the sift continues from index 1.
Swap 1 and 2.
The root holds the largest remaining value, 2: swap it to index 1, the last unsorted slot, and shrink the heap by one.
2 moves to index 1.
Sorted with 57 comparisons and 61 swaps: 6 partitions, 0 small ranges finished by insertion sort, and heap sort rescued 1 stretch where the budget ran out. Quicksort's speed on average, heap sort's O(n log n) guarantee in the worst case, insertion sort's low overhead on tiny ranges: that combination is why introsort is the library default.
Sorted after 57 comparisons and 61 swaps.
Write it yourself
Define threeWayQuickSort(values) and return the same values in ascending order. It runs in your browser against this lesson's own 2 examples.
// Partition into less than, equal to and greater than the pivot, so runs of duplicates are finished in one pass.function threeWayQuickSort(values) { return values;}
Remember
- Shell sort: insertion sort with gaps n/2, n/4, ..., 1; the big gaps move values far cheaply.
- Three-way quicksort: zones smaller, equal, larger; the equal zone is final, so duplicates cost O(n).
- Bucket sort is linear on evenly spread data; timsort merges natural runs, O(n) on sorted input; introsort is quicksort with insertion sort under 5 values and heap sort once 2 log n partitions have not finished.
Topics covered
Where this is used
Standard librariesstd::sort is introsort
Since C++11 the standard has required O(n log n) worst case from std::sort, which plain quicksort cannot promise, and libstdc++ meets it with introsort. Its constants are worth seeing next to the visualization: median-of-three pivots rather than a fixed position, the heap sort fallback at depth 2 log2 n, and a threshold of 16 rather than 4, below which it does not recurse at all - it leaves those stretches alone and cleans the whole array up with one insertion sort pass at the end, which is cheaper than many small ones. Quicksort stays the main engine because it partitions in place and walks memory sequentially; heap sort is reached only on input that was already defeating it.
Language runtimesTimsort in JavaScript engines
V8 replaced its quicksort for Array.prototype.sort with Timsort in V8 7.0 and Chrome 70, in 2018, and ES2019 then made stability part of the language, which a quicksort cannot provide: sorting a table by one column and then by another has to leave the first order intact inside ties. Run detection is the second payoff, because web data rarely arrives shuffled - arrays built by appending, filtered out of an already sorted list, or re-sorted after a single edit are mostly ordered already, and those are the shapes V8's own benchmarks improved on most.
DataRange partitioning in distributed sorts
Spark's RangePartitioner and Hadoop's TeraSort sort a dataset larger than one machine by choosing boundary keys, shipping each record to the partition whose range covers it, and sorting each partition locally. That is bucket sort with the scatter step turned into a network shuffle, and the payoff is the same: because the buckets are ordered, concatenating the results needs no final merge across machines. The boundaries come from sampling the data rather than cutting the key range into equal slices, since even spread is the one assumption real input does not honour.
SecurityRandom pivots as a denial-of-service defence
A deterministic quicksort picks its pivot by a fixed rule, so anyone who knows the rule can construct the input that drives it quadratic; McIlroy showed in 1999 that an adversarial comparison function can do this to such an implementation without even knowing the values being sorted. A sort behind a public endpoint then becomes a way to burn CPU with a small request. Drawing the pivot from a random source forces the attacker to guess the seed instead, which is why production sorts either randomise or, like introsort, cap the damage with a depth limit.
Why it works this way
Why halving the gap is the worst of the common gap sequences
The last pass of shell sort is a plain insertion sort, so the result is correct whatever gaps came before; the earlier passes exist only to remove distant inversions cheaply so that final pass has almost nothing left to move. That is also why the halving sequence is a poor choice: when n is a power of two every gap except the last is even, so a value at an odd index is never compared with one at an even index until the gap reaches 1, and the worst case stays quadratic. Hibbard's 2^k - 1 removes that by making the gaps odd and gets Theta(n^1.5); Ciura's 1, 4, 10, 23, 57, 132, 301, 701 is the fastest known in practice and was found by search rather than proof. No optimal sequence is known.
In the three-way partition, why i does not move when a[i] > pivot
The value swapped in from position gt arrived from the unexamined right end, so the loop has to look at it on the next turn; that is why the greater branch moves gt and leaves i alone. The less branch can advance both counters because a[lt] is either the pivot itself or a value already known to equal it, so that swap places a known value rather than skipping an unknown one. Advancing i in the greater branch is the standard bug here, and it is quiet: unexamined values end up inside the equal zone, which the recursion never visits again.
Timsort's run stack is the correctness argument, not bookkeeping
Timsort does not merge a run as soon as it finds one. It pushes run lengths on a stack and merges only while the top entries break the invariants X > Y + Z and Y > Z, which keep merges roughly balanced and so bound both the total merge cost and how deep the stack can get - which is why the stack can be a small fixed-size array. A 2015 attempt to verify java.util.TimSort formally found the collapse check did not actually restore that invariant, and OpenJDK issue JDK-8072909 is the resulting crash: an array of about 67 million elements overruns the stack and throws ArrayIndexOutOfBoundsException. OpenJDK both enlarged the stack, to 49 entries from 119151 elements up, and tightened mergeCollapse to test four runs rather than three. CPython repaired its own check and then in 3.11 dropped this rule entirely for the powersort merge policy, which chooses the merge order from the run lengths and is provably near-optimal, so listsort.txt now describes powersort rather than the invariants above.
Why introsort falls back to heap sort rather than merge sort
Merge sort would cap the worst case just as well, but it needs a buffer, and std::sort is expected to sort in place without allocating. Heap sort is the common O(n log n) sort that runs in O(1) extra space, and its weakness - jumping around memory, so poor cache behaviour - barely matters because it only ever runs on the stretches where quicksort was already failing. The depth budget is loose for the same reason: a healthy quicksort recursion is about log2 n deep, so 2 log2 n almost never fires on ordinary input and the guarantee is effectively free.
Read more
- ShellsortWikipedia
- Dutch national flag problemWikipedia
- listsort.txt, Tim Peters' notes on the design of TimsortCPython · github.com
- TimSort fails with ArrayIndexOutOfBoundsException on worst case long arraysOpenJDK
- A killer adversary for quicksortM. D. McIlroy, 1999 · mcilroy.cs.dartmouth.edu