Problem (Dijkstra). Given an array of three values — say 0, 1, 2 — sort it in one pass with extra space.
The three-pointer partition
void dutchFlag(vector<int>& a) {
int low = 0, mid = 0, high = a.size() - 1;
while (mid <= high) {
if (a[mid] == 0) swap(a[low++], a[mid++]);
else if (a[mid] == 1) mid++;
else swap(a[mid], a[high--]); // do NOT advance mid
}
}The invariant, at every step:
[0, low) all 0s
[low, mid) all 1s
[mid, high] unexamined
(high, n) all 2s
Do not advance
midon the 2-caseThe element swapped in from
highhas not been examined yet. Advancingmidskips it — the single most common bug in this algorithm.
time, space, one pass.
Why it matters: three-way quicksort
The same partition makes quicksort on arrays with many duplicates, by grouping elements equal to the pivot into their own region:
void quicksort3(vector<int>& a, int lo, int hi) {
if (lo >= hi) return;
int pivot = a[lo + rng() % (hi - lo + 1)];
int lt = lo, i = lo, gt = hi;
while (i <= gt) {
if (a[i] < pivot) swap(a[lt++], a[i++]);
else if (a[i] > pivot) swap(a[i], a[gt--]);
else i++;
}
quicksort3(a, lo, lt - 1);
quicksort3(a, gt + 1, hi); // skip the equal block
}An array of identical elements sorts in instead of — and this is why std::sort uses a three-way partition internally. It is also the right choice for radix-style string sorting (three-way radix quicksort).
Two-way partitioning
The Lomuto and Hoare schemes are the two-value cases:
// Lomuto: simpler, more swaps
int lomuto(vector<int>& a, int lo, int hi) {
int pivot = a[hi], i = lo;
for (int j = lo; j < hi; j++) if (a[j] < pivot) swap(a[i++], a[j]);
swap(a[i], a[hi]);
return i;
}
// Hoare: fewer swaps, faster in practice
int hoare(vector<int>& a, int lo, int hi) {
int pivot = a[lo], i = lo - 1, j = hi + 1;
while (true) {
do i++; while (a[i] < pivot);
do j--; while (a[j] > pivot);
if (i >= j) return j;
swap(a[i], a[j]);
}
}Hoare’s version does about three times fewer swaps and degrades more gracefully on duplicates, but returns a split point rather than the pivot’s final position — a common source of off-by-one errors.
Applications of partitioning
| Task | Partition use |
|---|---|
| Sort 3 distinct values | Dutch flag |
| Quicksort with duplicates | three-way partition |
nth_element / quickselect | partition, recurse on one side only |
| Median of medians | partition around a good pivot |
| Move all zeros to the end | two-pointer partition |
| Segregate even and odd | two-pointer partition |
| Group by a predicate, stable | std::stable_partition |
| Sort colours / categories | Dutch flag generalised to groups |
For groups with , a counting sort () is simpler and faster than a -way partition.
Quickselect — the close relative
int quickselect(vector<int>& a, int k) { // k-th smallest, 0-indexed
int lo = 0, hi = a.size() - 1;
while (true) {
int p = hoare(a, lo, hi);
if (k <= p) hi = p; else lo = p + 1;
}
} expected — and std::nth_element is exactly this (introselect, falling back to median-of-medians for the worst case). Use it whenever you need the -th element but not a full sort.
Why it is worth knowing
Two reasons:
- The invariant discipline. Dijkstra’s original presentation is the textbook example of writing a loop by stating its invariant first and deriving the body from it. If you can state the four regions, the code is forced.
- Three-way partitioning is genuinely important. Arrays with few distinct values are common, and the difference between and on them is the difference between accepted and TLE.
See also: Sorting · Two Pointers · STL Containers