AlgoScope

More Sorts

algorithmintermediateTime O(n log n) typicalSpace O(n)

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.

23051422831644515697

Insertion sort moves each value one slot at a time, so a small value far to the right takes many steps. Shell sort first runs insertion sort on values 4 apart, then with the gap halved, and finally with gap 1, which is plain insertion sort on a nearly sorted array.

Check your understanding

The player pauses before each decision in this run and asks what happens next. Here are all 17, with their answers.

  1. Gap 4: 23 sits 4 slots before 16. Swap them?

    • Swap, 23 is larger
    • Keep, they are in order

    Answer: Swap, 23 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  2. Gap 4: 5 sits 4 slots before 4. Swap them?

    • Swap, 5 is larger
    • Keep, they are in order

    Answer: Swap, 5 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  3. Gap 4: 42 sits 4 slots before 15. Swap them?

    • Swap, 42 is larger
    • Keep, they are in order

    Answer: Swap, 42 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  4. Gap 4: 8 sits 4 slots before 9. Swap them?

    • Swap, 8 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  5. Gap 2: 16 sits 2 slots before 15. Swap them?

    • Swap, 16 is larger
    • Keep, they are in order

    Answer: Swap, 16 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  6. Gap 2: 4 sits 2 slots before 8. Swap them?

    • Swap, 4 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  7. Gap 2: 16 sits 2 slots before 23. Swap them?

    • Swap, 16 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  8. Gap 2: 8 sits 2 slots before 5. Swap them?

    • Swap, 8 is larger
    • Keep, they are in order

    Answer: Swap, 8 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  9. Gap 2: 23 sits 2 slots before 42. Swap them?

    • Swap, 23 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  10. Gap 2: 8 sits 2 slots before 9. Swap them?

    • Swap, 8 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  11. Gap 1: 15 sits 1 slots before 4. Swap them?

    • Swap, 15 is larger
    • Keep, they are in order

    Answer: Swap, 15 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  12. Gap 1: 15 sits 1 slots before 16. Swap them?

    • Swap, 15 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  13. Gap 1: 16 sits 1 slots before 5. Swap them?

    • Swap, 16 is larger
    • Keep, they are in order

    Answer: Swap, 16 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  14. Gap 1: 16 sits 1 slots before 23. Swap them?

    • Swap, 16 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  15. Gap 1: 23 sits 1 slots before 8. Swap them?

    • Swap, 23 is larger
    • Keep, they are in order

    Answer: Swap, 23 is larger. Values a gap apart are ordered like neighbours in insertion sort.

  16. Gap 1: 23 sits 1 slots before 42. Swap them?

    • Swap, 23 is larger
    • Keep, they are in order

    Answer: Keep, they are in order. Values a gap apart are ordered like neighbours in insertion sort.

  17. Gap 1: 42 sits 1 slots before 9. Swap them?

    • Swap, 42 is larger
    • Keep, they are in order

    Answer: Swap, 42 is larger. Values a gap apart are ordered like neighbours in insertion sort.

How it runs, step by step

  1. Insertion sort moves each value one slot at a time, so a small value far to the right takes many steps. Shell sort first runs insertion sort on values 4 apart, then with the gap halved, and finally with gap 1, which is plain insertion sort on a nearly sorted array.

    Shell sort on 23, 5, 42, 8, 16, 4, 15, 9 with a first gap of 4.

  2. Gap 4: compare index 0 (23) with index 4 (16), 4 apart. 23 > 16, so swap them and keep stepping back by 4.

    Compare 23 and 16, 4 apart: swap.

  3. Gap 4: compare index 1 (5) with index 5 (4), 4 apart. 5 > 4, so swap them and keep stepping back by 4.

    Compare 5 and 4, 4 apart: swap.

  4. Gap 4: compare index 2 (42) with index 6 (15), 4 apart. 42 > 15, so swap them and keep stepping back by 4.

    Compare 42 and 15, 4 apart: swap.

  5. Gap 4: compare index 3 (8) with index 7 (9), 4 apart. In order, so index 7 is placed for this gap.

    Compare 8 and 9, 4 apart: keep.

  6. Gap 2: compare index 0 (16) with index 2 (15), 2 apart. 16 > 15, so swap them and keep stepping back by 2.

    Compare 16 and 15, 2 apart: swap.

  7. Gap 2: compare index 1 (4) with index 3 (8), 2 apart. In order, so index 3 is placed for this gap.

    Compare 4 and 8, 2 apart: keep.

  8. Gap 2: compare index 2 (16) with index 4 (23), 2 apart. In order, so index 4 is placed for this gap.

    Compare 16 and 23, 2 apart: keep.

  9. Gap 2: compare index 3 (8) with index 5 (5), 2 apart. 8 > 5, so swap them and keep stepping back by 2.

    Compare 8 and 5, 2 apart: swap.

  10. Gap 2: compare index 1 (4) with index 3 (5), 2 apart. In order, so index 5 is placed for this gap.

    Compare 4 and 5, 2 apart: keep.

  11. Gap 2: compare index 4 (23) with index 6 (42), 2 apart. In order, so index 6 is placed for this gap.

    Compare 23 and 42, 2 apart: keep.

  12. Gap 2: compare index 5 (8) with index 7 (9), 2 apart. In order, so index 7 is placed for this gap.

    Compare 8 and 9, 2 apart: keep.

  13. Gap 1: compare index 0 (15) with index 1 (4), 1 apart. 15 > 4, so swap them and keep stepping back by 1.

    Compare 15 and 4, 1 apart: swap.

  14. Gap 1: compare index 1 (15) with index 2 (16), 1 apart. In order, so index 2 is placed for this gap.

    Compare 15 and 16, 1 apart: keep.

  15. Gap 1: compare index 2 (16) with index 3 (5), 1 apart. 16 > 5, so swap them and keep stepping back by 1.

    Compare 16 and 5, 1 apart: swap.

  16. Gap 1: compare index 1 (15) with index 2 (5), 1 apart. 15 > 5, so swap them and keep stepping back by 1.

    Compare 15 and 5, 1 apart: swap.

  17. Gap 1: compare index 0 (4) with index 1 (5), 1 apart. In order, so index 3 is placed for this gap.

    Compare 4 and 5, 1 apart: keep.

  18. Gap 1: compare index 3 (16) with index 4 (23), 1 apart. In order, so index 4 is placed for this gap.

    Compare 16 and 23, 1 apart: keep.

  19. Gap 1: compare index 4 (23) with index 5 (8), 1 apart. 23 > 8, so swap them and keep stepping back by 1.

    Compare 23 and 8, 1 apart: swap.

  20. Gap 1: compare index 3 (16) with index 4 (8), 1 apart. 16 > 8, so swap them and keep stepping back by 1.

    Compare 16 and 8, 1 apart: swap.

  21. Gap 1: compare index 2 (15) with index 3 (8), 1 apart. 15 > 8, so swap them and keep stepping back by 1.

    Compare 15 and 8, 1 apart: swap.

  22. Gap 1: compare index 1 (5) with index 2 (8), 1 apart. In order, so index 5 is placed for this gap.

    Compare 5 and 8, 1 apart: keep.

  23. Gap 1: compare index 5 (23) with index 6 (42), 1 apart. In order, so index 6 is placed for this gap.

    Compare 23 and 42, 1 apart: keep.

  24. Gap 1: compare index 6 (42) with index 7 (9), 1 apart. 42 > 9, so swap them and keep stepping back by 1.

    Compare 42 and 9, 1 apart: swap.

  25. Gap 1: compare index 5 (23) with index 6 (9), 1 apart. 23 > 9, so swap them and keep stepping back by 1.

    Compare 23 and 9, 1 apart: swap.

  26. Gap 1: compare index 4 (16) with index 5 (9), 1 apart. 16 > 9, so swap them and keep stepping back by 1.

    Compare 16 and 9, 1 apart: swap.

  27. Gap 1: compare index 3 (15) with index 4 (9), 1 apart. 15 > 9, so swap them and keep stepping back by 1.

    Compare 15 and 9, 1 apart: swap.

  28. Gap 1: compare index 2 (8) with index 3 (9), 1 apart. In order, so index 7 is placed for this gap.

    Compare 8 and 9, 1 apart: keep.

  29. Sorted with 27 comparisons and 15 swaps. The big gaps did the long-distance moves cheaply, so the final gap-1 pass found almost everything in place. With these halving gaps shell sort is O(n^2) in the worst case but far faster than insertion sort in practice; better gap sequences reach O(n^1.3).

    Sorted after 27 comparisons and 15 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;}
Ln 1, Col 16 linesTab indents; Escape then Tab leaves the editor. Ctrl-Enter runs, Cmd-Enter on a Mac.

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.

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

Next up