Quicksort

Pick a pivot, move smaller items left and bigger items right, and repeat on each side. Learn partitioning, why pivots matter, and quicksort's worst case.

16 minLesson 3 of 7
On this page
  1. The idea: partition around a pivot
  2. Partitioning, traced
  3. The code
  4. Good pivots and bad pivots
  5. Common mistakes
  6. Check yourself
  7. What to practise next

A photographer wants a class of children lined up by height. She does not compare everyone with everyone. She picks one child, Sam, and says: "Shorter than Sam, stand on his left. Taller, on his right." Nobody on the left is sorted yet, nobody on the right is sorted yet, but Sam is now standing exactly where he belongs in the final line.

Then she does the same thing with the group on the left, and the group on the right. Each round puts at least one more child in their final place, and the groups keep shrinking. That is quicksort.

The idea: partition around a pivot

Quicksort has three steps:

  1. Pick a pivot, one item from the range.
  2. Partition: rearrange the range so every item smaller than the pivot comes before it and every other item comes after it. The pivot lands in its final position.
  3. Recurse on the part before the pivot and the part after it.

Compare it with merge sort. Merge sort splits without looking at the values, then does its real work when merging. Quicksort does its real work while splitting, and then there is nothing to combine: once both sides are sorted, the whole range is sorted. It also sorts in place, inside the original array.

Partitioning, traced

The partition step is the heart of quicksort, so trace it carefully. This version uses the last item as the pivot. A boundary b marks where the next small item should go: everything left of b is smaller than the pivot. Pointer j scans the range once.

Partition [7, 2, 9, 4, 1, 6] around 6Use the arrows or ← → keys
pivot = a[hi]b = lofor j from lo to hi - 1:  if a[j] < pivot:    swap a[b] and a[j]    b = b + 1swap a[b] and a[hi]return b
  1. a
    07b j
    12
    29
    34
    41
    56
    pivot
    6
    comparisons
    0

    1The pivot is the last item, 6. No small items found yet, so b is at the start.

  2. a
    07b j
    12
    29
    34
    41
    56
    pivot
    6
    comparisons
    1

    27 is not smaller than 6. Leave it where it is; it belongs on the right side.

  3. a
    02
    17b j
    29
    34
    41
    56
    pivot
    6
    comparisons
    2

    32 is smaller. Swap it into slot b, swapping 7 out, then move b right. Slot 0 now holds a small item.

  4. a
    02
    17b
    29j
    34
    41
    56
    pivot
    6
    comparisons
    3

    49 is not smaller. Skip it.

  5. a
    02
    14
    29b
    37j
    41
    56
    pivot
    6
    comparisons
    4

    54 is smaller. Swap it with the 7 at slot b, then move b.

  6. a
    02
    14
    21
    37b
    49j
    56
    pivot
    6
    comparisons
    5

    61 is smaller. Swap it with the 9 at slot b, then move b. The small items 2, 4, 1 are now packed at the front.

  7. a
    02
    14
    21
    36b
    49
    57
    pivot
    6
    comparisons
    5

    7The scan is done. Swap the pivot into slot b. Everything before it is smaller, everything after it is bigger, so 6 is in its final place.

  8. a
    02
    14
    21
    36
    49
    57
    returns
    3

    8Now recurse on [2, 4, 1] and [9, 7]. They never need to look at each other again, and there is no merge at the end.

  9. a
    01
    12
    24
    36
    47
    59

    9Both sides sorted the same way. One pass of 5 comparisons fixed the pivot and split the problem in two.

1 / 9

The code

The partition below is the one you just traced, with one addition: it first picks a random item and swaps it to the end, so the pivot is random instead of always the last item. The next section explains why.

function quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return a; // 0 or 1 items: sorted
  const p = partition(a, lo, hi);
  quickSort(a, lo, p - 1);  // the pivot at p is already final
  quickSort(a, p + 1, hi);
  return a;
}
 
function partition(a, lo, hi) {
  const r = lo + Math.floor(Math.random() * (hi - lo + 1));
  [a[r], a[hi]] = [a[hi], a[r]]; // random pivot, moved to the end
  const pivot = a[hi];
  let b = lo; // a[lo..b-1] holds items smaller than the pivot
  for (let j = lo; j < hi; j++) {
    if (a[j] < pivot) {
      [a[b], a[j]] = [a[j], a[b]];
      b++;
    }
  }
  [a[b], a[hi]] = [a[hi], a[b]]; // pivot into its final slot
  return b;
}
 
quickSort([7, 2, 9, 4, 1, 6]); // [1, 2, 4, 6, 7, 9]
import random
 
def quick_sort(a, lo=0, hi=None):
    if hi is None:
        hi = len(a) - 1
    if lo >= hi:  # 0 or 1 items: sorted
        return a
    p = partition(a, lo, hi)
    quick_sort(a, lo, p - 1)  # the pivot at p is already final
    quick_sort(a, p + 1, hi)
    return a
 
def partition(a, lo, hi):
    r = random.randint(lo, hi)
    a[r], a[hi] = a[hi], a[r]  # random pivot, moved to the end
    pivot = a[hi]
    b = lo  # a[lo..b-1] holds items smaller than the pivot
    for j in range(lo, hi):
        if a[j] < pivot:
            a[b], a[j] = a[j], a[b]
            b += 1
    a[b], a[hi] = a[hi], a[b]  # pivot into its final slot
    return b
 
quick_sort([7, 2, 9, 4, 1, 6])  # [1, 2, 4, 6, 7, 9]

Good pivots and bad pivots

Partitioning a range of n items costs O(n): one scan. What matters is how evenly the pivot splits the range.

If the pivot lands near the middle every time, each level halves the work, just like merge sort. There are about log n levels with O(n) work each, so the total is O(n log n).

Now take an array that is already sorted and always use the last item as the pivot. The pivot is the biggest item, so the "smaller" side gets everything else and the "bigger" side is empty. Each level removes just one item. That is n + (n − 1) + … + 1 steps, which is O(n²), and the recursion goes n calls deep.

good pivots 8 4 4 2 2 2 2 3 levels: about log₂ n worst pivots 8 7 6 5 4 3 2 7 levels: about n
Left: pivots near the middle halve the range, so 8 items take 3 levels. Right: the worst pivot every time peels off one item per level.

A random pivot fixes this. A sorted or reversed input can no longer force bad pivots, because the choice is made fresh every run. On a large array the chance of hitting the worst case is tiny, and the expected cost is O(n log n) for any input of distinct items. Many equal items are a separate problem, covered in the mistakes below.

ApproachTimeSpaceWhy
Average (random pivot)O(n log n)O(log n)About log n levels of O(n) partitioning. Space is the recursion stack.
Worst caseO(n²)O(n)Every pivot is the smallest or largest item, so each level removes only one.

Quicksort is not stable. The swaps in the partition throw items over each other: in the trace, 7 was thrown from slot 0 to slot 1, then to slot 3, then to slot 5. If two items had the same key, nothing would keep them in their original order.

Common mistakes

Check yourself

Quick check

You partition [3, 8, 1, 5] using the last item, 5, as the pivot, with the code from the trace. Where does 5 end up?

Quicksort with the last item as pivot gets [1, 2, 3, …, 1000]. Roughly how many comparisons does it make?

Which is true of quicksort but not of merge sort?

What to practise next

Partition [5, 9, 2, 7, 3] by hand around its last item, 3: you should get [2, 3, 5, 7, 9] with the pivot at index 1. Then read Heap Sort, which guarantees O(n log n) in place, with no bad inputs at all.