Skip to main content

Quicksort

Overviewโ€‹

Quicksort picks an element as the pivot, rearranges the array so that everything smaller sits to its left and everything larger to its right, then recurses on both sides. After partitioning, the pivot is already in its final position and never moves again.

It is the mirror image of mergesort: mergesort splits trivially and does its work while combining, quicksort does its work while splitting and combines trivially. In practice quicksort is usually the faster of the two, despite a worst case that is quadratic.

A diagram of quicksort partitioning the list 3 7 8 5 2 1 9 5 4 around the pivot 4, moving smaller elements left and larger right, then recursing into each side until sorted
Partitioning around the pivot 4. Once the pivot lands between the two groups it is in its final position; the algorithm then repeats on each side independently. Wikimedia Commons, Public domain

Core Conceptsโ€‹

PropertyValue
Best caseO(n log n) โ€” balanced partitions
AverageO(n log n)
Worst caseO(nยฒ) โ€” maximally unbalanced partitions
SpaceO(log n) โ€” recursion stack only
StableNo
AdaptiveNo (though pdqsort makes it partly so)
In placeYes

Architecture / Mechanismโ€‹

def quicksort(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
if lo >= hi:
return a
p = partition(a, lo, hi)
quicksort(a, lo, p - 1) # the pivot at p is already final
quicksort(a, p + 1, hi)
return a

def partition(a, lo, hi):
"""Lomuto scheme: pivot is the last element."""
pivot = a[hi]
i = lo # boundary of the "smaller than pivot" region
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] # put the pivot between the two regions
return i
Animation of quicksort on a set of bars, with a pivot chosen and elements swapped around it, then the same process repeating on each side
Each pass partitions a region around one pivot. The recursion narrows until every region is a single element. Wikimedia Commons, CC BY-SA 3.0

Why it beats mergesort in practice despite equal complexityโ€‹

  • In place. No O(n) buffer, and no allocation in the hot path.
  • Excellent locality. Partitioning is two sequential scans converging on each other, which the prefetcher handles perfectly. Mergesort's merges are also sequential but write to a separate buffer, doubling memory traffic.
  • Tight inner loop. A comparison, a conditional swap, and a pointer bump.

The pivot choice is the whole gameโ€‹

The partition is balanced only if the pivot is near the median. Choosing badly gives partitions of size 0 and nโˆ’1, which makes the recursion n levels deep and the cost O(nยฒ).

Pivot strategyWorst case triggered byVerdict
First or last elementAlready-sorted inputDangerous โ€” the most common real input
Random elementNothing predictableGood; O(nยฒ) becomes vanishingly unlikely
Median of three (first, middle, last)Crafted "median-of-3 killer" inputsStandard practice; cheap and effective
True median (median-of-medians)Nothing โ€” O(n log n) guaranteedToo slow in practice
Naive quicksort is quadratic on sorted input

Picking the first or last element as pivot means an already-sorted array partitions into an empty side and everything else, every time โ€” n levels of recursion, O(nยฒ) comparisons, and O(n) stack depth, which on a large array is a stack overflow rather than merely slow.

Sorted or nearly-sorted input is extremely common. Never ship a quicksort with a fixed pivot position; randomise it or use median-of-three.

Hoare partitioningโ€‹

The Lomuto scheme above is easier to read, but Hoare's original does about three times fewer swaps and handles duplicate-heavy input better:

def hoare_partition(a, lo, hi):
pivot = a[(lo + hi) // 2]
i, j = lo - 1, hi + 1
while True:
i += 1
while a[i] < pivot:
i += 1
j -= 1
while a[j] > pivot:
j -= 1
if i >= j:
return j # note: returns a split point, not a pivot index
a[i], a[j] = a[j], a[i]

Note the different contract โ€” it returns a boundary, so the recursion becomes quicksort(a, lo, j) and quicksort(a, j + 1, hi), with no element excluded. Mixing up the two schemes' contracts is a classic source of infinite recursion.

Practical Usageโ€‹

Production quicksorts are always hybrids:

def introsort(a, lo, hi, depth_budget):
if hi - lo < 16:
insertion_sort_range(a, lo, hi) # small ranges: insertion sort wins
elif depth_budget == 0:
heapsort_range(a, lo, hi) # too deep: bail out to a guaranteed O(n log n)
else:
p = partition(a, lo, hi)
introsort(a, lo, p - 1, depth_budget - 1)
introsort(a, p + 1, hi, depth_budget - 1)

# Entry point: budget of 2ยทlogโ‚‚(n) partitions before giving up on quicksort

Introsort โ€” this exact structure โ€” is what C++'s std::sort uses. It keeps quicksort's speed while making the O(nยฒ) worst case unreachable, because exceeding the depth budget hands the range to heapsort.

Also worth knowing: three-way partitioning (into < pivot, == pivot, > pivot) turns arrays with many duplicate keys from a weakness into an O(n) best case. Without it, equal keys pile up on one side.

Edge Cases & Pitfallsโ€‹

  • Tail-recursion on the larger side risks stack overflow. Recurse into the smaller partition and loop on the larger, bounding stack depth to O(log n) even in the worst case.
  • (lo + hi) // 2 can overflow in fixed-width integer languages. Use lo + (hi - lo) // 2.
  • Quicksort is not stable, and cannot cheaply be made so โ€” partitioning moves elements across long distances. If stability matters, use mergesort.
  • Many equal elements degrade two-way partitioning to O(nยฒ) in some schemes. Use three-way.

Comparisonsโ€‹

QuicksortMergesortHeapsort
Typical speedFastestGoodSlowest
Worst caseO(nยฒ)O(n log n)O(n log n)
SpaceO(log n)O(n)O(1)
StableNoYesNo
Used byC++ std::sort, Rust sort_unstableJava objects, Python (as Timsort)Introsort's fallback

Referencesโ€‹

  • Hoare, C.A.R. (1961), "Algorithm 64: Quicksort", Communications of the ACM โ€” the original.
  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 7 โ€” quicksort, randomised quicksort, and the average-case analysis.
  • Musser, D. (1997), "Introspective Sorting and Selection Algorithms" โ€” the paper introducing introsort.

Books & Videosโ€‹

  • Mergesort โ€” the stable, guaranteed-O(n log n) counterpart.
  • Heapsort โ€” introsort's escape hatch.
  • Choosing a Sort โ€” what standard libraries actually ship.