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.

range 0-870317212374157620738

Quicksort with a three-way partition. The pivot is the first value of the range; everything smaller is moved to a zone on the left, everything larger to a zone on the right, and every copy of the pivot settles in the middle, where it is final. Only the left and right zones are sorted further, so an array full of duplicates finishes in one pass.

Check your understanding

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

  1. i holds 3 and the pivot is 7. Where does 3 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Left zone: swap with lt. Smaller goes left, larger goes right, equal stays put.

  2. i holds 7 and the pivot is 7. Where does 7 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Middle: it stays, i moves on. Smaller goes left, larger goes right, equal stays put.

  3. i holds 12 and the pivot is 7. Where does 12 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Right zone: swap with gt. Smaller goes left, larger goes right, equal stays put.

  4. i holds 3 and the pivot is 7. Where does 3 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Left zone: swap with lt. Smaller goes left, larger goes right, equal stays put.

  5. i holds 7 and the pivot is 7. Where does 7 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Middle: it stays, i moves on. Smaller goes left, larger goes right, equal stays put.

  6. i holds 1 and the pivot is 7. Where does 1 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Left zone: swap with lt. Smaller goes left, larger goes right, equal stays put.

  7. i holds 7 and the pivot is 7. Where does 7 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Middle: it stays, i moves on. Smaller goes left, larger goes right, equal stays put.

  8. i holds 20 and the pivot is 7. Where does 20 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Right zone: swap with gt. Smaller goes left, larger goes right, equal stays put.

  9. i holds 3 and the pivot is 3. Where does 3 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Middle: it stays, i moves on. Smaller goes left, larger goes right, equal stays put.

  10. i holds 1 and the pivot is 3. Where does 1 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Left zone: swap with lt. Smaller goes left, larger goes right, equal stays put.

  11. i holds 12 and the pivot is 20. Where does 12 go?

    • Left zone: swap with lt
    • Middle: it stays, i moves on
    • Right zone: swap with gt

    Answer: Left zone: swap with lt. Smaller goes left, larger goes right, equal stays put.

How it runs, step by step

  1. Quicksort with a three-way partition. The pivot is the first value of the range; everything smaller is moved to a zone on the left, everything larger to a zone on the right, and every copy of the pivot settles in the middle, where it is final. Only the left and right zones are sorted further, so an array full of duplicates finishes in one pass.

    Three-way quicksort on 7, 3, 7, 12, 7, 1, 7, 20, 3.

  2. Partition indices 0 to 8 around the pivot 7. lt marks the end of the smaller zone, gt the start of the larger zone, and i walks the unknown values between them.

    Partition 0 to 8 around 7.

  3. i = 1 holds 3. 3 < 7: swap it with index 0, the first pivot copy, so the smaller zone grows. Both lt and i move on.

    Value 3 against pivot 7.

  4. i = 2 holds 7. 7 equals the pivot: it is already in the middle zone, so only i moves on.

    Value 7 against pivot 7.

  5. i = 3 holds 12. 12 > 7: swap it with index 8, the slot before the larger zone, and shrink gt. i stays, since the value that arrived is still unknown.

    Value 12 against pivot 7.

  6. i = 3 holds 3. 3 < 7: swap it with index 1, the first pivot copy, so the smaller zone grows. Both lt and i move on.

    Value 3 against pivot 7.

  7. i = 4 holds 7. 7 equals the pivot: it is already in the middle zone, so only i moves on.

    Value 7 against pivot 7.

  8. i = 5 holds 1. 1 < 7: swap it with index 2, the first pivot copy, so the smaller zone grows. Both lt and i move on.

    Value 1 against pivot 7.

  9. i = 6 holds 7. 7 equals the pivot: it is already in the middle zone, so only i moves on.

    Value 7 against pivot 7.

  10. i = 7 holds 20. 20 > 7: swap it with index 7, the slot before the larger zone, and shrink gt. i stays, since the value that arrived is still unknown.

    Value 20 against pivot 7.

  11. i has passed gt, so the range is partitioned: indices 0 to 2 are smaller than 7, 3 to 6 are equal to it and final, 7 to 8 are larger. All 4 copies of 7 were placed in one pass. Sort the two outer zones.

    Partitioned: 4 copies of 7 are final.

  12. Partition indices 0 to 2 around the pivot 3. lt marks the end of the smaller zone, gt the start of the larger zone, and i walks the unknown values between them.

    Partition 0 to 2 around 3.

  13. i = 1 holds 3. 3 equals the pivot: it is already in the middle zone, so only i moves on.

    Value 3 against pivot 3.

  14. i = 2 holds 1. 1 < 3: swap it with index 0, the first pivot copy, so the smaller zone grows. Both lt and i move on.

    Value 1 against pivot 3.

  15. i has passed gt, so the range is partitioned: indices 0 to 0 are smaller than 3, 1 to 2 are equal to it and final, 3 to 2 are larger. All 2 copies of 3 were placed in one pass. Sort the two outer zones.

    Partitioned: 2 copies of 3 are final.

  16. Partition indices 7 to 8 around the pivot 20. lt marks the end of the smaller zone, gt the start of the larger zone, and i walks the unknown values between them.

    Partition 7 to 8 around 20.

  17. i = 8 holds 12. 12 < 20: swap it with index 7, the first pivot copy, so the smaller zone grows. Both lt and i move on.

    Value 12 against pivot 20.

  18. i has passed gt, so the range is partitioned: indices 7 to 7 are smaller than 20, 8 to 8 are equal to it and final, 9 to 8 are larger. Sort the two outer zones.

    Partitioned: 1 copies of 20 are final.

  19. Sorted with 11 comparisons and 7 swaps. Two-way quicksort degrades to O(n^2) on many equal keys because equal values keep landing on one side; three-way partitioning takes them out after one pass, so runs of duplicates cost O(n) and the sort is O(n log n) on average.

    Sorted after 11 comparisons and 7 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