Quick Sort Visualization — Step by Step
Selects a pivot element, partitions the array around it, then recursively sorts both sides.
// quick answer
Quick Sort is a sorting algorithm that runs in O(n log n) time on average (O(n²) worst case) with O(log n) space. Selects a pivot element, partitions the array around it, then recursively sorts both sides.
How Quick Sort Works
Quick Sort picks a pivot element and rearranges the array so that everything smaller goes left and everything larger goes right (Lomuto partition scheme). Then it recursively sorts the two partitions. Average case O(n log n) but worst case O(n²) for already sorted input. In practice, it is often the fastest comparison sort due to cache efficiency.
Quick Sort Step by Step
The big idea: put one element exactly where it belongs
Quick Sort never merges anything. Instead, each round picks one element — the pivot — and rearranges the array so that everything smaller sits to the pivot’s left and everything larger to its right. After that single pass, the pivot is in its *final sorted position*, guaranteed: nothing on its left is bigger, nothing on its right is smaller. The two sides are then sorted recursively, each round locking another pivot in place. Where Merge Sort does its work combining on the way up, Quick Sort does all its work partitioning on the way down.
The Lomuto partition, step by step
This implementation uses the Lomuto scheme: the pivot is the last element, and a boundary index i marks the end of the "small values" zone. A scanner j walks left to right; every time it finds a value ≤ pivot, that value is swapped to the boundary and the boundary advances. Take [4, 8, 2, 7, 5], pivot 5. j=0: 4 ≤ 5, already at the boundary, boundary moves to 1. j=1: 8 > 5, skip. j=2: 2 ≤ 5, swap with 8 → [4, 2, 8, 7, 5], boundary moves to 2. j=3: 7 > 5, skip.
The pivot lands in its final home
When the scan ends, the boundary sits at the first element bigger than the pivot — in the example, index 2 (the 8). One last swap puts the pivot there: [4, 2, 5, 7, 8]. Look at the shape now: [4, 2] on the left are all ≤ 5, [7, 8] on the right are all > 5, and 5 will never move again. This is the moment to watch in the visualizer — the pivot bar flips to the "complete" color mid-run, long before the array is sorted, because its final position is already known.
Recurse on both sides
Quick Sort now calls itself on [4, 2] and on [7, 8] — the pivot itself is excluded, since it is done. Left side: pivot 2, nothing scans below it, so 2 swaps to the front → [2, 4], both locked. Right side: pivot 8, 7 ≤ 8 keeps the boundary moving, 8 stays put, both locked. The full array is [2, 4, 5, 7, 8]. Each level of recursion locks at least one element, and the ranges shrink until they hit single elements, which are trivially in position.
The worst case: when the pivot picks badly
Everything hinges on the pivot landing near the middle. If each partition splits roughly in half, there are about log₂ n levels doing O(n) work each — the O(n log n) average. But with the last element as pivot, an already-sorted array is the nightmare: every pivot is the maximum, every split is n−1 elements versus zero, and the recursion degrades to n levels — O(n²), plus deep recursion. Real implementations dodge this with randomized pivots, median-of-three selection, or introsort, which detects deep recursion and switches to a heap-based sort.
Why it is usually the fastest anyway
Despite the scary worst case, Quick Sort tends to beat other O(n log n) sorts on in-memory arrays. Partitioning is in place — no O(n) auxiliary array like Merge Sort — and the scan touches memory sequentially, which caches love. The inner loop is a couple of comparisons and an occasional swap: very little bookkeeping per element. The price is instability (partition swaps can reorder equal elements) and the need for good pivots. Race it against Merge Sort on random data to see the average case play out.
Time & Space Complexity
| Best Case | Average Case | Worst Case | Space |
|---|---|---|---|
| O(n log n) | O(n log n) | O(n²) | O(log n) |
Quick Sort Pseudocode
function quickSort(arr, low, high):
if low >= high: return
pivotIdx = partition(arr, low, high)
quickSort(arr, low, pivotIdx - 1)
quickSort(arr, pivotIdx + 1, high)
function partition(arr, low, high):
pivot = arr[high]
i = low
for j = low to high-1:
if arr[j] <= pivot:
swap(arr[i], arr[j])
i++
swap(arr[i], arr[high])
return iPress play in the visualizer above to watch each line execute with live highlighting.
Quick SortCode in JavaScript, Python & Java
JavaScript
function quickSort(arr, low = 0, high = arr.length - 1) {
if (low < high) {
const p = partition(arr, low, high);
quickSort(arr, low, p - 1);
quickSort(arr, p + 1, high);
}
return arr;
}
function partition(arr, low, high) {
const pivot = arr[high]; // Lomuto: last element is the pivot
let i = low; // boundary of the "<= pivot" zone
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
}
}
[arr[i], arr[high]] = [arr[high], arr[i]]; // pivot to final spot
return i;
}Python
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
p = partition(arr, low, high)
quick_sort(arr, low, p - 1)
quick_sort(arr, p + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high] # Lomuto: last element is the pivot
i = low # boundary of the "<= pivot" zone
for j in range(low, high):
if arr[j] <= pivot:
arr[i], arr[j] = arr[j], arr[i]
i += 1
arr[i], arr[high] = arr[high], arr[i] # pivot to final spot
return iJava
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int p = partition(arr, low, high);
quickSort(arr, low, p - 1);
quickSort(arr, p + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // Lomuto: last element is the pivot
int i = low; // boundary of the "<= pivot" zone
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
i++;
}
}
int tmp = arr[i]; arr[i] = arr[high]; arr[high] = tmp;
return i; // pivot's final index
}When to Use Quick Sort
- General-purpose in-memory sorting — on random-access arrays it is typically the fastest comparison sort in practice, which is why C’s qsort and many standard libraries build on it.
- When memory is tight — it sorts in place with only O(log n) stack space for recursion, versus Merge Sort’s O(n) auxiliary array.
- When average-case speed matters more than worst-case guarantees — with randomized or median-of-three pivots, hitting O(n²) is vanishingly unlikely on real data.
- Avoid it when stability is required — partitioning reorders equal elements; use Merge Sort or your library’s stable sort instead.
- Avoid the naive last-element-pivot version on sorted or nearly-sorted input — that is exactly its O(n²) worst case; if you must handle such input, randomize the pivot or use introsort.
- Avoid it for latency-critical or adversarial settings — an attacker who controls input can force the worst case of a predictable-pivot quicksort; guaranteed O(n log n) algorithms are safer.
Quick Sort — Frequently Asked Questions
Is Quick Sort stable?
No, not in its standard in-place forms. Partitioning swaps elements across long distances, so equal values can end up in a different relative order than they started. Stable variants exist but require extra memory, which sacrifices quicksort’s main space advantage. If stability matters — sorting by one field while preserving order on another — use merge sort or your language’s built-in stable sort.
What is the time complexity of Quick Sort?
O(n log n) on average and in the best case, when pivots split the array into roughly equal halves. The worst case is O(n²), which occurs when every pivot is the smallest or largest remaining element — for the last-element Lomuto scheme, already-sorted or reverse-sorted input triggers exactly this. Space is O(log n) on average for the recursion stack, since it partitions in place.
Why is Quick Sort called quick?
Its inventor, Tony Hoare, named it in 1961 for its speed in practice, and the name stuck because it is deserved: on typical in-memory arrays quicksort beats other comparison sorts. It partitions in place with no auxiliary array to copy, scans memory sequentially (cache-friendly), and has a very tight inner loop. Same asymptotic class as merge sort and heapsort, but smaller constants.
How do real implementations avoid the O(n²) worst case?
Three common defenses: pick the pivot randomly, so no fixed input pattern is reliably bad; use median-of-three, taking the median of the first, middle, and last elements, which handles sorted input well; or use introsort, which monitors recursion depth and switches to heapsort if it grows too deep, guaranteeing O(n log n). Many libraries also hand small partitions to insertion sort.
Quick Sort vs Merge Sort — which should I use?
Quicksort is usually faster on in-memory arrays and uses O(log n) space instead of O(n), so it wins when memory matters and average speed is the goal. Merge sort wins when you need stability, a guaranteed O(n log n) worst case, linked-list sorting, or external sorting of data larger than RAM. In practice, most standard libraries choose a quicksort-derived hybrid for primitives and a merge-based one for objects.
Compare Quick Sort
- Quick Sort vs Merge Sort — when to choose which, head to head
- Quick Sort vs Bubble Sort — when to choose which, head to head
- Quick Sort vs Insertion Sort — when to choose which, head to head
- Quick Sort vs Selection Sort — when to choose which, head to head
Related Algorithms
- Merge Sort Visualization — Divides the array in half recursively, sorts each half, then merges the sorted halves back together.
- Bubble Sort Visualization — Repeatedly steps through the list, compares adjacent elements, and swaps them if out of order.
References & Further Reading
- Hoare, C. A. R. (1962) — Quicksort — The Computer Journal, 5(1)
- Wikipedia — Quicksort
- Cormen, Leiserson, Rivest & Stein — Introduction to Algorithms (4th ed., MIT Press)