Searching and sorting · 15 min

Quick sort: place the pivot, then sort each side

Why a partition puts its pivot in its final place for m − 1 comparisons, why that makes quick sort O(n log n) on average but O(n²) when every pivot is the largest value left, and what sorting in place still costs.

01Why it matters

Put one value in its place, then split

Merge sort sorts a million values in at most about 20 million comparisons, whatever their order, but it needs a buffer as long as the array. The sorts in sorting in code need no buffer, but make up to about n²/2 comparisons: 500 billion for a million values.

Quick sort usually gets the best of both. Pick one value, the pivot, and move every smaller value to its left and every other value to its right. The pivot is now exactly where it belongs, and no value will ever cross it again. Sort each side the same way and the array is sorted, with nothing to merge and no buffer.

"Usually" is the catch: the speed depends on how evenly the pivots split the values, and the simplest rule, taking the last value, is at its worst on an already sorted list.

02Key parts

The pieces you will see

pivot
The value a partition compares every other value with. This version takes the last value of the part.
partition
One pass over a part that moves the values smaller than the pivot before it and the rest after it. The pivot ends in its final place.
boundary i
During a partition, the position where the next smaller value goes. Everything before i is smaller than the pivot.
in place
Sorting inside the array itself, with a few variables and the call stack as the only extra memory.
average and worst case
The average is the mean cost over every order of the input; the worst case is the most any input can cost.

03See it work

Partition first, then recurse

Here are 8 values. Play the run and watch the boundary i and the two sides grow.

The pivot is 45, the last value. Each other value is compared with it once: 18, 31, 9 and 26 are smaller and swap to the left, so 45 swaps into position 4, its final place, though neither side is sorted yet. Then each side is partitioned the same way: around 26, then 9, on the left, and 77, then 52, on the right. 14 comparisons in all.

A partition walks j along the part once and keeps one boundary, i: values before i are smaller than the pivot, values from i to j − 1 are at least the pivot, and the rest are unseen. A smaller A[j] swaps into position i and the boundary moves up one; otherwise j just moves on. Finally the pivot swaps into position i, between the two sides. This is Lomuto's partition; Tony Hoare's original walks two indices in from both ends and swaps less often.

Merge sort splits by position for free and compares afterwards, in the merges. Quick sort does all its comparing before its recursive calls, and afterwards there is nothing left to do: two sorted sides around a pivot already in place make a sorted part.

A partition of m values compares the other m − 1 with the pivot, once each. If pivots split parts roughly in half, as 45 split 8 values into 4 and 3, each level costs fewer than n comparisons, and there are about log₂ n levels.

Pivots don't need to be perfect. A random order mixes good and bad ones, and they average out to O(n log n): about 25 million comparisons for a million values, a third more than merge sort makes. Quick sort usually runs faster anyway: its inner loop is tiny, and it works within one array, which suits a computer's cache.

  • Quick Sort
  • Merge Sort
  • n²/2 (reference)
  • n log₂ n (reference)
05001K1.5K2K2.5K816243240485664input size nn²/2n log₂ n
At n = 64Quick Sort: about 360 comparisons, ×2.4 from n = 32Merge Sort: about 310 comparisons, ×2.5 from n = 32
Each point is a real run on a random list. Move along the chart, or use the arrow keys, to read the counts at any n.

At n = 64 quick sort makes about 360 comparisons and merge sort about 310, far below n²/2. Doubling n from 32 multiplies their counts by about 2.4 and 2.5, the mark of n log n.

04Predict

Your turn: which side?

Before each comparison, predict which side of the pivot the value goes to. Smaller values go left. A value equal to the pivot goes right, because the test in the code panel is A[j] < pivot. This list holds two 45s, and one of them is the first pivot.

Make 8 predictions: step forward and answer each question before the next step plays.

0 of 8 made

05Try it

The partition in code, and what it costs

Here is one partition on its own, as the visualiser runs it, on [7, 2, 9, 4, 1, 6] with 6 as the pivot. It returns the pivot's final index.

def partition(a, lo, hi):
    pivot = a[hi]
    i = lo
    for j in range(lo, hi):
        if a[j] < pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[hi] = a[hi], a[i]
    return i

nums = [7, 2, 9, 4, 1, 6]
p = partition(nums, 0, len(nums) - 1)
print('pivot at ' + str(p))
print(*nums)

The rest of quick sort is a few lines: if the part holds two or more values, partition it, then sort lo to p − 1 and p + 1 to hi. But look at the second line printed: 2 4 1 6 9 7.

Those swaps have another side effect: quick sort is not stable, since a long swap can carry a value past others equal to it. In the Predict run, the 45 that started last was the first pivot and landed in position 3, ahead of the 45 that started in position 5. Equal plain numbers can't be told apart, so for them that's harmless: Java sorts arrays of numbers with a dual-pivot quick sort, but objects with a stable merge sort.

The cost rides on where the pivots land. Here is this version's worst input.

12 values in sorted order. Each pivot is the largest value left, so every other value stays on its left and the pivot doesn't move: 11 + 10 + … + 1 = 66 comparisons, no swaps, and depth climbs to 11. Switch the input to Random: the same 12 values, shuffled, take 35 comparisons.

Reversed input is as bad, with the pivots alternating between the smallest and largest value left, and so is one value repeated, since values equal to the pivot all go right. A million sorted values would take about 500 billion comparisons. The first challenge in Break it runs this quick sort on 10 values you choose: make it reach its worst, 45 comparisons, where a random order takes about 24.

Every move is a swap inside the array, so there is no buffer. But watch depth among the variables: it reaches 11 on the sorted run and 5 on the Random one.

06Check yourself

Check yourself

  1. Question 1 of 5

    The visualiser’s quick sort partitions [70, 45, 12, 60, 33] around its last value, 33, leaving 12 33 70 60 45. What does it do with 70, 60 and 45?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 5

    Most library quick sorts don’t take the last value as the pivot. Why not?

  3. Question 3 of 5

    Beyond the array itself, how much memory does quick sort need on a random order of n values?

  4. Question 4 of 5

    Which of these does the quick sort in the visualiser not guarantee?

  5. Question 5 of 5

    Merge sort and quick sort both divide and conquer. Where does each one compare values?

07Explain it back

Say it in your own words

How does quick sort put an array in order? Why is it O(n log n) on average but O(n²) at worst, and what does it need in memory?

08Recap

What to remember

  • A partition puts smaller values left of the pivot and the rest right, so the pivot lands in its final place; then each side is sorted the same way, with all the comparing before the recursive calls.
  • A partition of m values costs m − 1 comparisons. Even splits give about log₂ n levels: O(n log n) on average.
  • If every pivot is the smallest or largest value left, as with the last value of a sorted list, the cost is n(n − 1)/2 comparisons. Random pivots and median-of-three make that rare; introsort rules it out.
  • It sorts in place, but its call stack is O(log n) deep on average and up to n deep at worst; recursing into the smaller side caps it at O(log n). It is not stable.
  • Next: heap sort, which uses a heap to sort in place in O(n log n) even at worst (its visualiser), and quickselect, which partitions and goes into only the side holding the k-th position: O(n) on average. Practise with Kth Largest Number in a List and Closest Points to the Origin, or make the swaps yourself in Be the algorithm.
Back to path