The idea
Quick sort is another divide-and-conquer algorithm, but it does the hard work before recursing instead of after:
- Pick a pivot — here, the last element.
- Partition: rearrange the array so everything ≤ pivot is on its left and everything > pivot is on its right. The pivot is now in its final position.
- Recurse on the left part and the right part.
No merging is needed — when the recursion finishes, the array is sorted.
Analogy: a teacher sorting students by height picks one student (the pivot) and says “shorter than her, stand on the left; taller, on the right”. She is now in exactly the right spot. Repeat inside each group.
Lomuto partition, step by step
We scan with j from lo to hi − 1, and i marks the end of the “≤ pivot” zone:
pivot = a[hi]
i = lo - 1
for j in lo .. hi-1:
if a[j] <= pivot:
i += 1
swap(a[i], a[j]) # grow the ≤ zone
swap(a[i+1], a[hi]) # put the pivot between the zones
return i + 1 # the pivot's final index
At every moment the range looks like this:
lo … i |
i+1 … j−1 |
j … hi−1 |
hi |
|---|---|---|---|
| ≤ pivot (cyan) | > pivot (purple) | not checked yet | pivot (pink) |
In the 3D model the pivot steps forward out of the row while the partition happens, and drops back into its final slot at the end.
Why is it fast — and when isn’t it?
If the pivot splits the array roughly in half each time, there are about log₂ n levels of recursion, each doing O(n) work in total → O(n log n).
But if the pivot is always the smallest or largest element, one side gets everything: n levels of O(n) → O(n²). With “last element as pivot”, this happens on already sorted data — try it in the model!
Fixes used in practice:
- Random pivot — swap a random element into the pivot position first.
- Median-of-three — use the median of the first, middle and last elements.
- Introsort (C++
std::sort) — switch to heap sort if the recursion gets too deep.
Even so, quick sort is often the fastest sort in practice: it works in-place and its inner loop is very cache-friendly.
Code
def quick_sort(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
if lo < hi:
p = partition(a, lo, hi)
quick_sort(a, lo, p - 1)
quick_sort(a, p + 1, hi)
return a
def partition(a, lo, hi):
pivot = a[hi]
i = lo - 1
for j in range(lo, hi):
if a[j] <= pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i + 1], a[hi] = a[hi], a[i + 1]
return i + 1
print(quick_sort([10, 80, 30, 90, 40, 50, 70])) # [10, 30, 40, 50, 70, 80, 90]
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
int partition(vector<int>& a, int lo, int hi) {
int pivot = a[hi], i = lo - 1;
for (int j = lo; j < hi; j++)
if (a[j] <= pivot) swap(a[++i], a[j]);
swap(a[i + 1], a[hi]);
return i + 1;
}
void quickSort(vector<int>& a, int lo, int hi) {
if (lo >= hi) return;
int p = partition(a, lo, hi);
quickSort(a, lo, p - 1);
quickSort(a, p + 1, hi);
}
int main() {
vector<int> a = {10, 80, 30, 90, 40, 50, 70};
quickSort(a, 0, a.size() - 1);
for (int x : a) cout << x << " "; // 10 30 40 50 70 80 90
}
Quick sort vs merge sort
| Quick sort | Merge sort | |
|---|---|---|
| Average time | O(n log n) | O(n log n) |
| Worst time | O(n²) | O(n log n) |
| Extra space | O(log n) stack | O(n) array |
| Stable | ❌ No | ✅ Yes |
| Typical speed | Usually faster (in-place, cache-friendly) | Predictable |
Common mistakes
- Recursing on
(lo, p)instead of(lo, p − 1)— the pivot is already placed, and including it can loop forever. - Forgetting the final swap that moves the pivot into place.
- Assuming O(n log n) always — remember the sorted-input trap.
Complexity at a glance
| Case / operation | Time | Why |
|---|---|---|
| Best case | O(n log n) | The pivot splits the array into two equal halves. |
| Average case | O(n log n) | Random data gives reasonably balanced splits. |
| Worst case | O(n²) | The pivot is always the smallest or largest (e.g. sorted input with a last-element pivot). |
| Extra space | O(log n) |
Quick check
Test yourself — pick an answer to see if you got it.
1. After one partition step, what is true about the pivot?
Everything to its left is ≤ it and everything to its right is > it, so it is exactly where it belongs.
2. When does quick sort with the last element as pivot hit its worst case?
Each pivot is then the maximum, so one side has n−1 elements and the other is empty — n levels of O(n) work.
3. How do real libraries usually avoid the worst case?
A random or median-of-three pivot makes consistently bad splits extremely unlikely.
4. Is quick sort in-place?
Partitioning swaps elements inside the same array. Only the recursion uses extra stack space, O(log n) on average.