Quick Sort and Partitioning
The quick sort everyone writes
Quick sort picks a value, the pivot, moves everything smaller to its left and everything else to its right, and sorts the two sides the same way. The pivot is chosen at random, so no order of the input can make it slow on purpose. Here it is the way most people write it, on 50,000 random values and on 50,000 copies of one value, an array Sort an Array allows:
#include <bits/stdc++.h>
using namespace std;
uint32_t seed = 2463534242u;
int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed % bound;
}
vector<int> shuffled(int n) { // 0 .. n-1 in a random order
vector<int> nums(n);
iota(nums.begin(), nums.end(), 0);
for (int i = n - 1; i > 0; i--) swap(nums[i], nums[randomBelow(i + 1)]);
return nums;
}
void quickSort(vector<int>& a, int lo, int hi) { // Lomuto: a random pivot, one boundary, one scan
if (lo >= hi) return;
swap(a[lo + randomBelow(hi - lo + 1)], a[hi]); // the pivot goes to the end
int pivot = a[hi], b = lo;
for (int j = lo; j < hi; j++)
if (a[j] < pivot) swap(a[b++], a[j]); // smaller values go left of the boundary
swap(a[b], a[hi]); // the pivot lands in its final place
quickSort(a, lo, b - 1);
quickSort(a, b + 1, hi);
}
int main() {
int n = 50000;
vector<int> random(n), equal(n, 7);
for (int& v : random) v = randomBelow(100001) - 50000;
for (auto& input : vector<pair<string, vector<int>*>>{{"random", &random}, {"equal", &equal}}) {
const string& name = input.first;
vector<int>* a = input.second;
auto start = chrono::steady_clock::now();
quickSort(*a, 0, n - 1);
printf("%s: %.1f ms\n", name.c_str(), chrono::duration<double, milli>(chrono::steady_clock::now() - start).count());
}
}
// random: 3.8 ms
// equal: 1224.0 msseed = 2463534242
def random_below(bound): # xorshift: the same numbers in every language
global seed
seed ^= (seed << 13) & 0xFFFFFFFF
seed ^= seed >> 17
seed ^= (seed << 5) & 0xFFFFFFFF
return seed % bound
def shuffled(n): # 0 .. n-1 in a random order
nums = list(range(n))
for i in range(n - 1, 0, -1):
j = random_below(i + 1)
nums[i], nums[j] = nums[j], nums[i]
return nums
import time
def quick_sort(a, lo, hi): # Lomuto: a random pivot, one boundary, one scan
if lo >= hi:
return
r = lo + random_below(hi - lo + 1)
a[r], a[hi] = a[hi], a[r] # the pivot goes to the end
pivot, b = a[hi], lo
for j in range(lo, hi):
if a[j] < pivot: # smaller values go left of the boundary
a[b], a[j] = a[j], a[b]
b += 1
a[b], a[hi] = a[hi], a[b] # the pivot lands in its final place
quick_sort(a, lo, b - 1)
quick_sort(a, b + 1, hi)
n = 50000
inputs = {"random": [random_below(100001) - 50000 for _ in range(n)], "equal": [7] * n}
for name, a in inputs.items():
start = time.perf_counter()
try:
quick_sort(a, 0, n - 1)
print(f"{name}: {(time.perf_counter() - start) * 1000:.1f} ms")
except RecursionError:
print(f"{name}: RecursionError")
# random: 167.8 ms
# equal: RecursionErrorpublic class Main {
static int seed = (int) 2463534242L;
static int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return Integer.remainderUnsigned(seed, bound);
}
static int[] shuffled(int n) { // 0 .. n-1 in a random order
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = i;
for (int i = n - 1; i > 0; i--) {
int j = randomBelow(i + 1), t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
return nums;
}
static void quickSort(int[] a, int lo, int hi) { // Lomuto: a random pivot, one boundary, one scan
if (lo >= hi) return;
int r = lo + randomBelow(hi - lo + 1), t = a[r];
a[r] = a[hi]; a[hi] = t; // the pivot goes to the end
int pivot = a[hi], b = lo;
for (int j = lo; j < hi; j++)
if (a[j] < pivot) { t = a[b]; a[b] = a[j]; a[j] = t; b++; } // smaller values go left
t = a[b]; a[b] = a[hi]; a[hi] = t; // the pivot lands in its final place
quickSort(a, lo, b - 1);
quickSort(a, b + 1, hi);
}
public static void main(String[] args) {
int n = 50000;
int[] random = new int[n], equal = new int[n];
for (int i = 0; i < n; i++) { random[i] = randomBelow(100001) - 50000; equal[i] = 7; }
String[] names = {"random", "equal"};
int[][] inputs = {random, equal};
for (int k = 0; k < 2; k++) {
long start = System.nanoTime();
try {
quickSort(inputs[k], 0, n - 1);
System.out.printf("%s: %.1f ms%n", names[k], (System.nanoTime() - start) / 1e6);
} catch (StackOverflowError e) {
System.out.println(names[k] + ": StackOverflowError");
}
}
}
}
// random: 14.4 ms
// equal: StackOverflowErrorlet seed = 2463534242;
function randomBelow(bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return (seed >>> 0) % bound;
}
function shuffled(n) { // 0 .. n-1 in a random order
const nums = Array.from({ length: n }, (_, i) => i);
for (let i = n - 1; i > 0; i--) {
const j = randomBelow(i + 1);
[nums[i], nums[j]] = [nums[j], nums[i]];
}
return nums;
}
function quickSort(a, lo, hi) { // Lomuto: a random pivot, one boundary, one scan
if (lo >= hi) return;
const r = lo + randomBelow(hi - lo + 1);
[a[r], a[hi]] = [a[hi], a[r]]; // the pivot goes to the end
const pivot = a[hi];
let b = lo;
for (let j = lo; j < hi; j++)
if (a[j] < pivot) { [a[b], a[j]] = [a[j], a[b]]; b++; } // smaller values go left
[a[b], a[hi]] = [a[hi], a[b]]; // the pivot lands in its final place
quickSort(a, lo, b - 1);
quickSort(a, b + 1, hi);
}
const n = 50000;
const inputs = [["random", Array.from({ length: n }, () => randomBelow(100001) - 50000)], ["equal", new Array(n).fill(7)]];
for (const [name, a] of inputs) {
const start = process.hrtime.bigint();
try {
quickSort(a, 0, n - 1);
console.log(`${name}: ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(1)} ms`);
} catch (e) {
if (!(e instanceof RangeError)) throw e;
console.log(`${name}: RangeError`);
}
}
// random: 43.7 ms
// equal: RangeErrorThe random values took 3.8 ms in C++. The equal ones took 1.22 s, 322 times longer, and the other three never finished: Java threw StackOverflowError, JavaScript RangeError and Python RecursionError.
When every value equals the pivot, none is smaller, so the pivot lands at one end and the other side keeps everything but one value: n − 1, then n − 2, and so on. That's n²/2 comparisons and a call stack 50,000 deep. A random pivot protects against sorted input (Best, Worst and Average Case timed that trap); it can't help when every pivot is the same. This lesson takes partitioning apart, then fixes it twice.

Partitioning: one scan, one boundary
The partition above is Lomuto's. The pivot waits at the end; a boundary b starts at the left; one index j scans the rest. Each value smaller than the pivot is swapped to the boundary, which moves one place right. At the end the pivot is swapped to the boundary, and everything before it is smaller, everything after it is not:
#include <bits/stdc++.h>
using namespace std;
int partition(vector<int>& a, int lo, int hi) { // Lomuto around the last value; returns where it lands
int pivot = a[hi], b = lo;
for (int j = lo; j < hi; j++)
if (a[j] < pivot) swap(a[b++], a[j]);
swap(a[b], a[hi]);
return b;
}
int main() {
vector<int> a = {7, 2, 1, 8, 6, 3, 5, 4};
int p = partition(a, 0, a.size() - 1);
for (int v : a) printf("%d ", v);
printf("| the pivot %d is at %d\n", a[p], p);
}
// 2 1 3 4 6 7 5 8 | the pivot 4 is at 3def partition(a, lo, hi): # Lomuto around the last value; returns where it lands
pivot, b = a[hi], lo
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]
return b
a = [7, 2, 1, 8, 6, 3, 5, 4]
p = partition(a, 0, len(a) - 1)
print(*a, f"| the pivot {a[p]} is at {p}")
# 2 1 3 4 6 7 5 8 | the pivot 4 is at 3public class Main {
static int partition(int[] a, int lo, int hi) { // Lomuto around the last value; returns where it lands
int pivot = a[hi], b = lo, t;
for (int j = lo; j < hi; j++)
if (a[j] < pivot) { t = a[b]; a[b] = a[j]; a[j] = t; b++; }
t = a[b]; a[b] = a[hi]; a[hi] = t;
return b;
}
public static void main(String[] args) {
int[] a = {7, 2, 1, 8, 6, 3, 5, 4};
int p = partition(a, 0, a.length - 1);
StringBuilder line = new StringBuilder();
for (int v : a) line.append(v).append(" ");
System.out.println(line + "| the pivot " + a[p] + " is at " + p);
}
}
// 2 1 3 4 6 7 5 8 | the pivot 4 is at 3function partition(a, lo, hi) { // Lomuto around the last value; returns where it lands
const pivot = a[hi];
let b = lo;
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]];
return b;
}
const a = [7, 2, 1, 8, 6, 3, 5, 4];
const p = partition(a, 0, a.length - 1);
console.log(`${a.join(" ")} | the pivot ${a[p]} is at ${p}`);
// 2 1 3 4 6 7 5 8 | the pivot 4 is at 34 landed at index 3, its place in sorted order, and never moves again. Then quick sort recurses on 2 1 3 and on 6 7 5 8. Each partition is O(n), and when the pivot splits the values roughly in half, there are about log₂ n levels: O(n log n) on average, in place, with O(log n) of call stack. When the pivot lands at an end every time, it's O(n²), as above. Quick sort isn't stable: the swaps jump values over their equals.
Partition and Quickselect. Run Lomuto's partition yourself: for each value, decide whether it's smaller than the pivot and swaps to the boundary. Then say which side holds the kth smallest, the step quickselect repeats. Try the equal values to watch the pivot land at the end.

Hoare's partition: two indexes
Hoare's partition, the original, moves two indexes toward each other: i stops at a value that belongs on the right (not smaller than the pivot), j stops at one that belongs on the left (not bigger), and the two swap. Values equal to the pivot stop both indexes, so equal values are swapped and spread across both sides:
#include <bits/stdc++.h>
using namespace std;
uint32_t seed = 2463534242u;
int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed % bound;
}
vector<int> shuffled(int n) { // 0 .. n-1 in a random order
vector<int> nums(n);
iota(nums.begin(), nums.end(), 0);
for (int i = n - 1; i > 0; i--) swap(nums[i], nums[randomBelow(i + 1)]);
return nums;
}
void quickSort(vector<int>& a, int lo, int hi) { // Hoare: two indexes close in from both ends
if (lo >= hi) return;
swap(a[lo], a[lo + randomBelow(hi - lo + 1)]); // a random pivot, moved to the front
int pivot = a[lo], i = lo - 1, j = hi + 1;
while (true) {
do i++; while (a[i] < pivot); // stop at a value that belongs on the right...
do j--; while (a[j] > pivot); // ...and at one that belongs on the left
if (i >= j) break;
swap(a[i], a[j]);
}
quickSort(a, lo, j); // a[lo..j] <= pivot <= a[j+1..hi]
quickSort(a, j + 1, hi);
}
int main() {
int n = 50000;
vector<int> random(n), equal(n, 7);
for (int& v : random) v = randomBelow(100001) - 50000;
for (auto& input : vector<pair<string, vector<int>*>>{{"random", &random}, {"equal", &equal}}) {
const string& name = input.first;
vector<int>* a = input.second;
auto start = chrono::steady_clock::now();
quickSort(*a, 0, n - 1);
if (!is_sorted(a->begin(), a->end())) return 1;
printf("%s: %.1f ms\n", name.c_str(), chrono::duration<double, milli>(chrono::steady_clock::now() - start).count());
}
}
// random: 4.5 ms
// equal: 1.0 msseed = 2463534242
def random_below(bound): # xorshift: the same numbers in every language
global seed
seed ^= (seed << 13) & 0xFFFFFFFF
seed ^= seed >> 17
seed ^= (seed << 5) & 0xFFFFFFFF
return seed % bound
def shuffled(n): # 0 .. n-1 in a random order
nums = list(range(n))
for i in range(n - 1, 0, -1):
j = random_below(i + 1)
nums[i], nums[j] = nums[j], nums[i]
return nums
import time
def quick_sort(a, lo, hi): # Hoare: two indexes close in from both ends
if lo >= hi:
return
r = lo + random_below(hi - lo + 1)
a[lo], a[r] = a[r], a[lo] # a random pivot, moved to the front
pivot, i, j = a[lo], lo - 1, hi + 1
while True:
i += 1
while a[i] < pivot: # stop at a value that belongs on the right...
i += 1
j -= 1
while a[j] > pivot: # ...and at one that belongs on the left
j -= 1
if i >= j:
break
a[i], a[j] = a[j], a[i]
quick_sort(a, lo, j) # a[lo..j] <= pivot <= a[j+1..hi]
quick_sort(a, j + 1, hi)
n = 50000
inputs = {"random": [random_below(100001) - 50000 for _ in range(n)], "equal": [7] * n}
for name, a in inputs.items():
start = time.perf_counter()
quick_sort(a, 0, n - 1)
assert a == sorted(a)
print(f"{name}: {(time.perf_counter() - start) * 1000:.1f} ms")
# random: 180.7 ms
# equal: 175.5 mspublic class Main {
static int seed = (int) 2463534242L;
static int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return Integer.remainderUnsigned(seed, bound);
}
static int[] shuffled(int n) { // 0 .. n-1 in a random order
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = i;
for (int i = n - 1; i > 0; i--) {
int j = randomBelow(i + 1), t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
return nums;
}
static void quickSort(int[] a, int lo, int hi) { // Hoare: two indexes close in from both ends
if (lo >= hi) return;
int r = lo + randomBelow(hi - lo + 1), t = a[lo];
a[lo] = a[r]; a[r] = t; // a random pivot, moved to the front
int pivot = a[lo], i = lo - 1, j = hi + 1;
while (true) {
do i++; while (a[i] < pivot); // stop at a value that belongs on the right...
do j--; while (a[j] > pivot); // ...and at one that belongs on the left
if (i >= j) break;
t = a[i]; a[i] = a[j]; a[j] = t;
}
quickSort(a, lo, j); // a[lo..j] <= pivot <= a[j+1..hi]
quickSort(a, j + 1, hi);
}
public static void main(String[] args) {
int n = 50000;
int[] random = new int[n], equal = new int[n];
for (int i = 0; i < n; i++) { random[i] = randomBelow(100001) - 50000; equal[i] = 7; }
String[] names = {"random", "equal"};
int[][] inputs = {random, equal};
for (int k = 0; k < 2; k++) {
long start = System.nanoTime();
quickSort(inputs[k], 0, n - 1);
for (int i = 1; i < n; i++) if (inputs[k][i - 1] > inputs[k][i]) throw new AssertionError();
System.out.printf("%s: %.1f ms%n", names[k], (System.nanoTime() - start) / 1e6);
}
}
}
// random: 37.6 ms
// equal: 6.5 mslet seed = 2463534242;
function randomBelow(bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return (seed >>> 0) % bound;
}
function shuffled(n) { // 0 .. n-1 in a random order
const nums = Array.from({ length: n }, (_, i) => i);
for (let i = n - 1; i > 0; i--) {
const j = randomBelow(i + 1);
[nums[i], nums[j]] = [nums[j], nums[i]];
}
return nums;
}
function quickSort(a, lo, hi) { // Hoare: two indexes close in from both ends
if (lo >= hi) return;
const r = lo + randomBelow(hi - lo + 1);
[a[lo], a[r]] = [a[r], a[lo]]; // a random pivot, moved to the front
const pivot = a[lo];
let i = lo - 1, j = hi + 1;
while (true) {
do i++; while (a[i] < pivot); // stop at a value that belongs on the right...
do j--; while (a[j] > pivot); // ...and at one that belongs on the left
if (i >= j) break;
[a[i], a[j]] = [a[j], a[i]];
}
quickSort(a, lo, j); // a[lo..j] <= pivot <= a[j+1..hi]
quickSort(a, j + 1, hi);
}
const n = 50000;
const inputs = [["random", Array.from({ length: n }, () => randomBelow(100001) - 50000)], ["equal", new Array(n).fill(7)]];
for (const [name, a] of inputs) {
const start = process.hrtime.bigint();
quickSort(a, 0, n - 1);
if (a.some((v, i) => i > 0 && a[i - 1] > v)) throw new Error("not sorted");
console.log(`${name}: ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(1)} ms`);
}
// random: 33.6 ms
// equal: 17.6 msThe 50,000 equal values took 1.0 ms in C++, 6.5 ms in Java, 18 ms in JavaScript and 176 ms in Python: i and j meet in the middle, every split is even, and the recursion is log₂ n deep. Its catch is the boundary: the pivot isn't put in its final place, so it recurses on [lo, j] and [j + 1, hi], and the pivot must not be the last value (Mistakes that cost an interview).
Three-way partition: the equal values in the middle
The three-way partition (Dijkstra's Dutch national flag) splits into smaller, equal and bigger. Three indexes: below lt everything is smaller, from lt to i equal, above gt bigger, and i walks until it meets gt. The equal block is finished, and only the two outer parts recurse. Sort Colors, in Two Pointers, is exactly this partition on three values:

#include <bits/stdc++.h>
using namespace std;
uint32_t seed = 2463534242u;
int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed % bound;
}
vector<int> shuffled(int n) { // 0 .. n-1 in a random order
vector<int> nums(n);
iota(nums.begin(), nums.end(), 0);
for (int i = n - 1; i > 0; i--) swap(nums[i], nums[randomBelow(i + 1)]);
return nums;
}
void quickSort(vector<int>& a, int lo, int hi) { // three ways: < pivot | = pivot | > pivot
if (lo >= hi) return;
int pivot = a[lo + randomBelow(hi - lo + 1)], 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--]); // a[i] is new: look at it next
else i++;
}
quickSort(a, lo, lt - 1); // the equal values in a[lt..gt] are done
quickSort(a, gt + 1, hi);
}
int main() {
int n = 50000;
vector<int> random(n), equal(n, 7), few(n);
for (int& v : random) v = randomBelow(100001) - 50000;
for (int& v : few) v = randomBelow(3);
for (auto& input : vector<pair<string, vector<int>*>>{{"random", &random}, {"equal", &equal}, {"0 to 2", &few}}) {
const string& name = input.first;
vector<int>* a = input.second;
auto start = chrono::steady_clock::now();
quickSort(*a, 0, n - 1);
if (!is_sorted(a->begin(), a->end())) return 1;
printf("%s: %.1f ms\n", name.c_str(), chrono::duration<double, milli>(chrono::steady_clock::now() - start).count());
}
}
// random: 4.5 ms
// equal: 0.1 ms
// 0 to 2: 0.5 msseed = 2463534242
def random_below(bound): # xorshift: the same numbers in every language
global seed
seed ^= (seed << 13) & 0xFFFFFFFF
seed ^= seed >> 17
seed ^= (seed << 5) & 0xFFFFFFFF
return seed % bound
def shuffled(n): # 0 .. n-1 in a random order
nums = list(range(n))
for i in range(n - 1, 0, -1):
j = random_below(i + 1)
nums[i], nums[j] = nums[j], nums[i]
return nums
import time
def quick_sort(a, lo, hi): # three ways: < pivot | = pivot | > pivot
if lo >= hi:
return
pivot = a[lo + random_below(hi - lo + 1)]
lt, i, gt = lo, lo, hi
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]
gt -= 1 # a[i] is new: look at it next
else:
i += 1
quick_sort(a, lo, lt - 1) # the equal values in a[lt..gt] are done
quick_sort(a, gt + 1, hi)
n = 50000
inputs = {"random": [random_below(100001) - 50000 for _ in range(n)], "equal": [7] * n,
"0 to 2": [random_below(3) for _ in range(n)]}
for name, a in inputs.items():
start = time.perf_counter()
quick_sort(a, 0, n - 1)
assert a == sorted(a)
print(f"{name}: {(time.perf_counter() - start) * 1000:.1f} ms")
# random: 256.9 ms
# equal: 6.9 ms
# 0 to 2: 18.4 mspublic class Main {
static int seed = (int) 2463534242L;
static int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return Integer.remainderUnsigned(seed, bound);
}
static int[] shuffled(int n) { // 0 .. n-1 in a random order
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = i;
for (int i = n - 1; i > 0; i--) {
int j = randomBelow(i + 1), t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
return nums;
}
static void quickSort(int[] a, int lo, int hi) { // three ways: < pivot | = pivot | > pivot
if (lo >= hi) return;
int pivot = a[lo + randomBelow(hi - lo + 1)], lt = lo, i = lo, gt = hi, t;
while (i <= gt) {
if (a[i] < pivot) { t = a[lt]; a[lt++] = a[i]; a[i++] = t; }
else if (a[i] > pivot) { t = a[i]; a[i] = a[gt]; a[gt--] = t; } // a[i] is new: look at it next
else i++;
}
quickSort(a, lo, lt - 1); // the equal values in a[lt..gt] are done
quickSort(a, gt + 1, hi);
}
public static void main(String[] args) {
int n = 50000;
int[] random = new int[n], equal = new int[n], few = new int[n];
for (int i = 0; i < n; i++) { random[i] = randomBelow(100001) - 50000; equal[i] = 7; }
for (int i = 0; i < n; i++) few[i] = randomBelow(3);
String[] names = {"random", "equal", "0 to 2"};
int[][] inputs = {random, equal, few};
for (int k = 0; k < 3; k++) {
long start = System.nanoTime();
quickSort(inputs[k], 0, n - 1);
for (int i = 1; i < n; i++) if (inputs[k][i - 1] > inputs[k][i]) throw new AssertionError();
System.out.printf("%s: %.1f ms%n", names[k], (System.nanoTime() - start) / 1e6);
}
}
}
// random: 17.4 ms
// equal: 0.7 ms
// 0 to 2: 1.4 mslet seed = 2463534242;
function randomBelow(bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return (seed >>> 0) % bound;
}
function shuffled(n) { // 0 .. n-1 in a random order
const nums = Array.from({ length: n }, (_, i) => i);
for (let i = n - 1; i > 0; i--) {
const j = randomBelow(i + 1);
[nums[i], nums[j]] = [nums[j], nums[i]];
}
return nums;
}
function quickSort(a, lo, hi) { // three ways: < pivot | = pivot | > pivot
if (lo >= hi) return;
const pivot = a[lo + randomBelow(hi - lo + 1)];
let lt = lo, i = lo, gt = hi;
while (i <= gt) {
if (a[i] < pivot) { [a[lt], a[i]] = [a[i], a[lt]]; lt++; i++; }
else if (a[i] > pivot) { [a[i], a[gt]] = [a[gt], a[i]]; gt--; } // a[i] is new: look at it next
else i++;
}
quickSort(a, lo, lt - 1); // the equal values in a[lt..gt] are done
quickSort(a, gt + 1, hi);
}
const n = 50000;
const inputs = [["random", Array.from({ length: n }, () => randomBelow(100001) - 50000)], ["equal", new Array(n).fill(7)],
["0 to 2", Array.from({ length: n }, () => randomBelow(3))]];
for (const [name, a] of inputs) {
const start = process.hrtime.bigint();
quickSort(a, 0, n - 1);
if (a.some((v, i) => i > 0 && a[i - 1] > v)) throw new Error("not sorted");
console.log(`${name}: ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(1)} ms`);
}
// random: 53.4 ms
// equal: 15.5 ms
// 0 to 2: 2.8 msAll equal is a single pass: every value is the pivot, and nothing is left to recurse on. Each partition finishes every copy of its pivot, so values from 0 to 2 take three partitions in all, one per distinct value. On 50,000 equal values:
| Language | Lomuto | Hoare | Three-way |
|---|---|---|---|
| C++ | 1.22 s | 1.0 ms | 0.1 ms |
| Python | RecursionError | 176 ms | 6.9 ms |
| Java | StackOverflowError | 6.5 ms | 0.7 ms |
| JavaScript | RangeError | 18 ms | 16 ms |
Two more defences finish the picture. Median of three takes the middle of the first, middle and last values as the pivot, which beats sorted input without a random number. Introsort, behind C++'s sort, counts how deep the recursion gets and switches to heap sort (Introduction to Heaps) past about 2 log₂ n levels, so its worst case is O(n log n).
Quickselect: keep one side
To find the kth smallest value, partition once and look where the pivot block landed: if k falls in it, the pivot is the answer; otherwise keep only the side that holds k and partition again. One side per step, so the work halves on average: n + n/2 + n/4 + … ≈ 2n, O(n), where sorting is O(n log n). Top K and the Kth Element uses it for Kth Largest Element in an Array:
#include <bits/stdc++.h>
using namespace std;
uint32_t seed = 2463534242u;
int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed % bound;
}
vector<int> shuffled(int n) { // 0 .. n-1 in a random order
vector<int> nums(n);
iota(nums.begin(), nums.end(), 0);
for (int i = n - 1; i > 0; i--) swap(nums[i], nums[randomBelow(i + 1)]);
return nums;
}
int quickselect(vector<int> a, int k) { // the kth smallest, k from 1: partition, keep one side
int lo = 0, hi = a.size() - 1;
while (true) {
int pivot = a[lo + randomBelow(hi - lo + 1)], 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++;
}
if (k - 1 < lt) hi = lt - 1; // it's among the smaller values
else if (k - 1 > gt) lo = gt + 1; // among the bigger ones
else return pivot; // it's the pivot
}
}
int main() {
vector<int> a = {7, 2, 1, 8, 6, 3, 5, 4};
printf("%d %d %d %d\n", quickselect(a, 1), quickselect(a, 3), quickselect(a, 8), quickselect(vector<int>(10, 5), 4));
}
// 1 3 8 5seed = 2463534242
def random_below(bound): # xorshift: the same numbers in every language
global seed
seed ^= (seed << 13) & 0xFFFFFFFF
seed ^= seed >> 17
seed ^= (seed << 5) & 0xFFFFFFFF
return seed % bound
def shuffled(n): # 0 .. n-1 in a random order
nums = list(range(n))
for i in range(n - 1, 0, -1):
j = random_below(i + 1)
nums[i], nums[j] = nums[j], nums[i]
return nums
def quickselect(a, k): # the kth smallest, k from 1: partition, keep one side
a = a[:]
lo, hi = 0, len(a) - 1
while True:
pivot = a[lo + random_below(hi - lo + 1)]
lt, i, gt = lo, lo, hi
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]
gt -= 1
else:
i += 1
if k - 1 < lt: # it's among the smaller values
hi = lt - 1
elif k - 1 > gt: # among the bigger ones
lo = gt + 1
else: # it's the pivot
return pivot
a = [7, 2, 1, 8, 6, 3, 5, 4]
print(quickselect(a, 1), quickselect(a, 3), quickselect(a, 8), quickselect([5] * 10, 4))
# 1 3 8 5import java.util.*;
public class Main {
static int seed = (int) 2463534242L;
static int randomBelow(int bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return Integer.remainderUnsigned(seed, bound);
}
static int[] shuffled(int n) { // 0 .. n-1 in a random order
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = i;
for (int i = n - 1; i > 0; i--) {
int j = randomBelow(i + 1), t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
return nums;
}
static int quickselect(int[] values, int k) { // the kth smallest, k from 1: partition, keep one side
int[] a = values.clone();
int lo = 0, hi = a.length - 1;
while (true) {
int pivot = a[lo + randomBelow(hi - lo + 1)], lt = lo, i = lo, gt = hi, t;
while (i <= gt) {
if (a[i] < pivot) { t = a[lt]; a[lt++] = a[i]; a[i++] = t; }
else if (a[i] > pivot) { t = a[i]; a[i] = a[gt]; a[gt--] = t; }
else i++;
}
if (k - 1 < lt) hi = lt - 1; // it's among the smaller values
else if (k - 1 > gt) lo = gt + 1; // among the bigger ones
else return pivot; // it's the pivot
}
}
public static void main(String[] args) {
int[] a = {7, 2, 1, 8, 6, 3, 5, 4}, fives = new int[10];
Arrays.fill(fives, 5);
System.out.println(quickselect(a, 1) + " " + quickselect(a, 3) + " " + quickselect(a, 8) + " " + quickselect(fives, 4));
}
}
// 1 3 8 5let seed = 2463534242;
function randomBelow(bound) { // xorshift: the same numbers in every language
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return (seed >>> 0) % bound;
}
function shuffled(n) { // 0 .. n-1 in a random order
const nums = Array.from({ length: n }, (_, i) => i);
for (let i = n - 1; i > 0; i--) {
const j = randomBelow(i + 1);
[nums[i], nums[j]] = [nums[j], nums[i]];
}
return nums;
}
function quickselect(values, k) { // the kth smallest, k from 1: partition, keep one side
const a = values.slice();
let lo = 0, hi = a.length - 1;
while (true) {
const pivot = a[lo + randomBelow(hi - lo + 1)];
let lt = lo, i = lo, gt = hi;
while (i <= gt) {
if (a[i] < pivot) { [a[lt], a[i]] = [a[i], a[lt]]; lt++; i++; }
else if (a[i] > pivot) { [a[i], a[gt]] = [a[gt], a[i]]; gt--; }
else i++;
}
if (k - 1 < lt) hi = lt - 1; // it's among the smaller values
else if (k - 1 > gt) lo = gt + 1; // among the bigger ones
else return pivot; // it's the pivot
}
}
const a = [7, 2, 1, 8, 6, 3, 5, 4];
console.log(quickselect(a, 1), quickselect(a, 3), quickselect(a, 8), quickselect(new Array(10).fill(5), 4));
// 1 3 8 5The 1st, 3rd and 8th smallest of the eight values are 1, 3 and 8, and the 4th smallest of ten 5s is 5, found in a single pass by the three-way partition. Like quick sort, its worst case is O(n²), and the random pivot makes that unlikely.
Which sort for Sort an Array
Sort an Array asks for O(n log n) time with no library sort, on up to 50,000 values from −50,000 to 50,000, and nothing stops a test from being one value 50,000 times. Three answers pass:
- Merge sort (Merge Sort and Counting Inversions): O(n log n) always, stable, O(n) extra.
- Quick sort with a random pivot and a three-way partition: in place, O(log n) stack on average, and equal values cost one pass.
- Heap sort (Introduction to Heaps): O(n log n) always, O(1) extra, not stable.
Lomuto with a random pivot is the one that fails, as the first run showed; with a values range this small, counting sort works too (Counting, Bucket and Radix Sort).
Spotting it
The clues: "kth smallest" or "kth largest", the median, "top k" without the order of the k (quickselect, O(n) on average); rearranging around a value (all negatives first, colours, a pivot); sorting in place with O(1) extra space. Near misses:
- The k largest of a stream that keeps growing: a heap, since quickselect needs every value at once (Top K and the Kth Element).
- A sort that must keep equal values in order: merge sort, not quick sort.
- Three known values to arrange in one pass: the three-way partition alone (Sort Colors).
Mistakes that cost an interview
Lomuto on many equal values. The random pivot doesn't help: 1.22 s in C++ and a stack error in Java, JavaScript and Python above. Use Hoare or the three-way partition.
Hoare's partition with the last value as the pivot. On two values, j can stop at hi, and the recursion calls the same range again:
def quick_sort(a, lo, hi): # Hoare's loop, but with the last value as the pivot
if lo >= hi:
return
pivot, i, j = a[hi], 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:
break
a[i], a[j] = a[j], a[i]
quick_sort(a, lo, j) # j can be hi here: the same range again
quick_sort(a, j + 1, hi)
try:
quick_sort([1, 2], 0, 1)
except RecursionError:
print("RecursionError on [1, 2]")
# RecursionError on [1, 2]Take the pivot from the front, or move a random one there, as above; then j always stops before hi.
A first or last pivot on sorted input. O(n²) and a deep stack (Best, Worst and Average Case measured it); pick the pivot at random, or by median of three.
Expecting it to be stable. It isn't; if equal keys must keep their order, merge sort is the sort to use.
Check yourself
Quiz
1 / 6Multiple choice
Multiple choice
Multiple choice
Multiple choice
Multiple choice
Multiple choice
Practice
In this section:
- Quick Sort: a partition, then both sides; pick the pivot well.
- Sort an Array: O(n log n) on inputs full of repeats: three-way quick sort or merge sort.
Next: Counting, Bucket and Radix Sort, the sorts that don't compare at all.