Quick Sort
Pick a pivot, partition elements into smaller and larger sides, and recursively sort each side.
Overview
Quick sort chooses a pivot, Partitioning the array so every element left of the pivot is ≤ it and every element right is ≥ it, then sorts the two sides recursively. The pivot never moves again. Average time is O(n log n) with a small constant and excellent cache behaviour; it is in-place (O(log n) stack) but not stable.
The worst case is O(n²) when pivots are consistently extreme (e.g. first-element pivot on sorted input). Production sorts guard against this: C++ std::sort uses introsort (quick sort that switches to Heap Sort when recursion gets too deep and to Insertion Sort for small ranges); Go and Rust use pdqsort, a pattern-defeating variant. Java uses dual-pivot quick sort for primitives.
Intuition
A mental model before the formal terms.
Line up people by height by picking one person as a reference and asking everyone shorter to move to their left and everyone taller to their right. The reference person is now in exactly the right spot forever. Now do the same thing separately on the left group and the right group. Good references (near the median) split groups evenly, so the job finishes in about log₂ n rounds.
How it works
- If the range has fewer than two elements, return.
- Choose a pivot: random index, median-of-three (
a[lo],a[mid],a[hi]), or the middle element. Swap it toa[hi]for Lomuto or keep it as a value for Hoare. - Lomuto partition:
i = lo; for eachjin[lo, hi), ifa[j] < pivot, swapa[i]anda[j]and incrementi. Finally swap the pivot intoa[i];iis its final position. - Recurse on
[lo, i - 1]and[i + 1, hi]. Recurse on the smaller side first and loop on the larger to cap stack depth atO(log n). - With many duplicates, use 3-way (Dutch national flag) partitioning into
< pivot,== pivot,> pivotregions.
Why it works
Partition invariant (Lomuto): a[lo..i-1] < pivot, a[i..j-1] ≥ pivot, a[j..hi-1] unprocessed. When j reaches hi, swapping the pivot into i puts it between the two classes — its final sorted position.
Correctness follows by induction on range length: each side is sorted independently and every element in the left side is ≤ pivot ≤ every element in the right side.
Expected cost with random pivots: each pair of elements is compared at most once, and the probability that the i-th and j-th smallest are compared is 2/(j - i + 1), summing to about 2n ln n.
Recognition
How to tell a problem wants this.
- A general-purpose in-memory sort where average speed and low memory matter and stability does not.
- Problems that reduce to partitioning around a value: k-th element (Quickselect), Dutch national flag, move zeroes.
- Asked about worst-case behaviour, pivot selection, or why library sorts are hybrids.
Interactive visualization
Play, step, change the input. ← → and space work too.
1quickSort(a, lo, hi):2 if lo >= hi: return3 pivot = a[hi]; i = lo4 for j in lo .. hi-1:5 if a[j] < pivot:6 swap(a[i], a[j]); i = i + 17 swap(a[i], a[hi]) # pivot to final spot8 quickSort(a, lo, i-1)9 quickSort(a, i+1, hi)Pseudocode
1quickSort(a, lo, hi):2 if lo >= hi: return3 swap(a[randomIndex(lo, hi)], a[hi]); pivot = a[hi]; i = lo4 for j from lo to hi - 1:5 if a[j] < pivot: swap(a[i], a[j]); i += 16 swap(a[i], a[hi])7 quickSort(a, lo, i - 1); quickSort(a, i + 1, hi)Implementations
1import random2 3 4def quick_sort(a: list[int]) -> None:51 · Recursive sort with tail-loop on the larger side6 def sort(lo: int, hi: int) -> None:7 while lo < hi:82 · Pick a random pivot and move it to the end9 p = random.randint(lo, hi)10 a[p], a[hi] = a[hi], a[p]11 pivot = a[hi]123 · Lomuto partition: a[lo..i) < pivot, a[i..j) >= pivot13 i = lo14 for j in range(lo, hi):15 if a[j] < pivot:16 a[i], a[j] = a[j], a[i]17 i += 118 a[i], a[hi] = a[hi], a[i] # pivot lands at its final index i194 · Recurse on the smaller side, loop on the larger (O(log n) stack)20 if i - lo < hi - i:21 sort(lo, i - 1)22 lo = i + 123 else:24 sort(i + 1, hi)25 hi = i - 126 275 · Entry point28 sort(0, len(a) - 1)random.randint(lo, hi)is inclusive on both ends, matching the closed range.- The pivot is swapped to
a[hi]sorange(lo, hi)scans everything else. - Tuple swaps advance the
< pivotboundaryi; the final swap places the pivot ati. - The
while lo < hiloop plus recursion on the smaller side keeps depth at O(log n); Python's default recursion limit is 1000, so naive recursion would fail on large sorted inputs. - The nested function mutates
ain place; rebindinglo/hiis fine because they are locals ofsort.
Pure-Python swaps are slow; list.sort() (C TimSort) is 50-100x faster. Use this to learn partitioning and quickselect.
- Python has no library quick sort;
sortedis TimSort.heapq.nsmallest(k, a)covers most quickselect use cases. - The comprehension form
quick_sort([x for x in a if x < p]) + [p] + ...is elegant but O(n) extra memory per level and not in place. - Recursion limit:
sys.setrecursionlimitis a workaround; the tail loop above is the fix.
- Using
a[0]as the pivot —RecursionErroron sorted input of a few thousand elements. - The list-comprehension version with
<=on one side and>on the other is correct, but<and>alone drop duplicates. - Recursing on both sides instead of looping on the larger one.
- Library sorts are NOT quick sort in JS or Python: V8 and CPython use stable TimSort. C++
std::sortis introsort (quick sort + heap sort fallback + insertion sort), unstable and guaranteed O(n log n). - Stack depth: Python raises
RecursionErrorat ~1000 frames and JS engines at ~10k; C++ segfaults on overflow. Recursing on the smaller side and looping on the larger keeps all four at O(log n). - Random numbers: C++
std::mt19937+uniform_int_distribution(inclusive), Pythonrandom.randint(inclusive), JSMath.random()(needs thefloor(random * (hi - lo + 1))idiom). - Lomuto vs Hoare: Lomuto returns the pivot's final index (recurse on
[lo, i-1]and[i+1, hi]); Hoare returns a split point (recurse on[lo, p]and[p+1, hi]) — mixing them up is the classic infinite loop, in any language. - JS
[10, 2, 5].sort()sorts as strings; hand-written quick sort compares numerically.
Complexity
Worst case with adversarial pivots; random or median-of-three pivots make it negligible; introsort bounds it at O(n log n). In-place, not stable, not adaptive.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- General in-memory sorting of primitives where stability is irrelevant — it is usually the fastest comparison sort in practice.
- Memory-constrained sorting:
O(log n)stack, no buffer. - As the engine behind Quickselect and partition-based problems.
- Stability is required — use Merge Sort / TimSort.
- Hard worst-case guarantees on adversarial input with no randomization — use Heap Sort or introsort.
- Linked lists (partitioning needs random access; merge sort is natural there).
- Many duplicate keys without 3-way partitioning — 2-way Lomuto degrades to
O(n²)on all-equal input.
Alternatives
Common mistakes
- Always using
a[lo]ora[hi]as the pivot —O(n²)on sorted or reverse-sorted input. - Recursing on both sides without the smaller-first trick, allowing
O(n)stack depth. - Forgetting the final swap that places the pivot, or recursing on a range that includes the pivot (infinite recursion).
- Hoare partition off-by-ones: the returned index is not the pivot's final position, so the recursion ranges differ from Lomuto's.
- Expecting stability.
Interview patterns
- Sort colors / Dutch national flag: 3-way partition in one pass.
- Kth largest element via Quickselect.
- Explain introsort and why
std::sortnever hitsO(n²). - Partition an array so all negatives precede positives (unstable,
O(n),O(1)space).
- Recognizing the approach from an array and a targetIntermediate
- Where does O(n log n) come from?Beginner
- Average case versus worst caseIntermediate
- When space complexity mattersIntermediate
- Two SumBeginner
- Kth Largest Element in an ArrayIntermediate