Quick sort
Quick sort picks a pivot, partitions the array so smaller values sit left of it and larger values right, then sorts each side. It is usually the fastest comparison sort in practice, but a bad pivot choice makes it quadratic.
Learning goals
- Trace Lomuto and Hoare partitioning step by step
- Compare pivot strategies and explain which input breaks each one
- Use three-way partitioning to sort duplicate-heavy data quickly
- Keep recursion depth at O(log n) with a loop on the larger side
Prerequisites
To sort a class by height, pick one student and split everyone into shorter and taller. That student is now in the right spot. Repeat on each group. If you always happen to pick the shortest student, the split is useless: that is the worst case.
Partition schemes Lomuto/Hoare
Lomuto keeps one boundary i: everything left of i is smaller than the pivot. A scan pointer j moves right, and each smaller value is swapped into position i. At the end the pivot is swapped into index i, its final position. It is short and easy to get right.
Hoare uses two pointers that start at the ends and move toward each other. i stops at a value that belongs on the right, j stops at a value that belongs on the left, and they swap. It does about three times fewer swaps than Lomuto on average, but the pivot does not finish at a known index, so the recursion is on [lo..p] and [p+1..hi].
| Lomuto | Hoare | |
|---|---|---|
| Pointers | One scan pointer and one boundary | Two pointers moving inward |
| Pivot after partition | At its final index p | Not at a known index |
| Recursion | [lo..p-1] and [p+1..hi] | [lo..p] and [p+1..hi] |
| Swaps | More | About 3x fewer on average |
| Many equal keys | Degrades toward O(n²) | Splits equal keys evenly, stays fast |
Best
O(n log n)
Average
O(n log n)
Worst
O(n²)
Space
O(log n) stack
Quick sort (Lomuto partition)
Interactive · edit the inputint[] arr · length 8
Pick a pivot, partition around it, then sort each side. Pivot strategy: last element.
1public static void quickSort(int[] arr, int lo, int hi) {2if (lo >= hi) return;3int p = partition(arr, lo, hi);4quickSort(arr, lo, p - 1);5quickSort(arr, p + 1, hi);6}78static int partition(int[] arr, int lo, int hi) {9movePivotToEnd(arr, lo, hi); // strategy: last, first, middle, median of 310int pivot = arr[hi];11int i = lo; // arr[lo..i-1] < pivot12for (int j = lo; j < hi; j++) {13if (arr[j] < pivot) {14swap(arr, i, j);15i++;16}17}18swap(arr, i, hi); // pivot to its final place19return i;20}
Live state
n
8
Execution trace (dry run table)Show 61 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=8 | Pick a pivot, partition around it, then sort each side. Pivot strategy: last element. |
| 2 | L1 | lo=0 hi=7 | Enter quickSort(arr, 0, 7). |
| 3 | L2 | lo=0 hi=7 | Two or more elements, so partition. |
| 4 | L9 | lo=0 hi=7 | Pivot strategy: last element. The pivot is already at arr[7]. |
| 5 | L11 | pivot=15 i=0 | pivot = 15. i marks where the next value smaller than the pivot will go. |
| 6 | L13 | pivot=15 i=0 j=0 | Not smaller: leave it for the right region. |
| 7 | L13 | pivot=15 i=0 j=1 | Not smaller: leave it for the right region. |
| 8 | L13 | pivot=15 i=0 j=2 | Not smaller: leave it for the right region. |
| 9 | L13 | pivot=15 i=0 j=3 | Smaller than the pivot: it belongs in the left region. |
| 10 | L14 | pivot=15 i=1 j=3 | Swap arr[0] and arr[3], then i = 1. |
| 11 | L13 | pivot=15 i=1 j=4 | Smaller than the pivot: it belongs in the left region. |
| 12 | L14 | pivot=15 i=2 j=4 | Swap arr[1] and arr[4], then i = 2. |
| 13 | L13 | pivot=15 i=2 j=5 | Not smaller: leave it for the right region. |
| 14 | L13 | pivot=15 i=2 j=6 | Smaller than the pivot: it belongs in the left region. |
| 15 | L14 | pivot=15 i=3 j=6 | Swap arr[2] and arr[6], then i = 3. |
| 16 | L18 | pivot=15 p=3 | Swap the pivot into index 3. Everything left of it is < 15, everything right is >= 15, and 15 will never move again. |
| 17 | L4 | lo=0 hi=2 | Enter quickSort(arr, 0, 2). |
| 18 | L2 | lo=0 hi=2 | Two or more elements, so partition. |
| 19 | L9 | lo=0 hi=2 | Pivot strategy: last element. The pivot is already at arr[2]. |
| 20 | L11 | pivot=10 i=0 | pivot = 10. i marks where the next value smaller than the pivot will go. |
| 21 | L13 | pivot=10 i=0 j=0 | Smaller than the pivot: it belongs in the left region. |
| 22 | L14 | pivot=10 i=1 j=0 | arr[0] is already in place; i advances to 1. |
| 23 | L13 | pivot=10 i=1 j=1 | Smaller than the pivot: it belongs in the left region. |
| 24 | L14 | pivot=10 i=2 j=1 | arr[1] is already in place; i advances to 2. |
| 25 | L18 | pivot=10 p=2 | Swap the pivot into index 2. Everything left of it is < 10, everything right is >= 10, and 10 will never move again. |
| 26 | L4 | lo=0 hi=1 | Enter quickSort(arr, 0, 1). |
| 27 | L2 | lo=0 hi=1 | Two or more elements, so partition. |
| 28 | L9 | lo=0 hi=1 | Pivot strategy: last element. The pivot is already at arr[1]. |
| 29 | L11 | pivot=9 i=0 | pivot = 9. i marks where the next value smaller than the pivot will go. |
| 30 | L13 | pivot=9 i=0 j=0 | Smaller than the pivot: it belongs in the left region. |
| 31 | L14 | pivot=9 i=1 j=0 | arr[0] is already in place; i advances to 1. |
| 32 | L18 | pivot=9 p=1 | Swap the pivot into index 1. Everything left of it is < 9, everything right is >= 9, and 9 will never move again. |
| 33 | L4 | lo=0 hi=0 | Enter quickSort(arr, 0, 0). |
| 34 | L2 | lo=0 hi=0 | Zero or one element: already sorted. |
| 35 | L5 | lo=2 hi=1 | Enter quickSort(arr, 2, 1). |
| 36 | L2 | lo=2 hi=1 | Zero or one element: already sorted. |
| 37 | L5 | lo=3 hi=2 | Enter quickSort(arr, 3, 2). |
| 38 | L2 | lo=3 hi=2 | Zero or one element: already sorted. |
| 39 | L5 | lo=4 hi=7 | Enter quickSort(arr, 4, 7). |
| 40 | L2 | lo=4 hi=7 | Two or more elements, so partition. |
| 41 | L9 | lo=4 hi=7 | Pivot strategy: last element. The pivot is already at arr[7]. |
| 42 | L11 | pivot=38 i=4 | pivot = 38. i marks where the next value smaller than the pivot will go. |
| 43 | L13 | pivot=38 i=4 j=4 | Smaller than the pivot: it belongs in the left region. |
| 44 | L14 | pivot=38 i=5 j=4 | arr[4] is already in place; i advances to 5. |
| 45 | L13 | pivot=38 i=5 j=5 | Not smaller: leave it for the right region. |
| 46 | L13 | pivot=38 i=5 j=6 | Not smaller: leave it for the right region. |
| 47 | L18 | pivot=38 p=5 | Swap the pivot into index 5. Everything left of it is < 38, everything right is >= 38, and 38 will never move again. |
| 48 | L4 | lo=4 hi=4 | Enter quickSort(arr, 4, 4). |
| 49 | L2 | lo=4 hi=4 | Zero or one element: already sorted. |
| 50 | L5 | lo=6 hi=7 | Enter quickSort(arr, 6, 7). |
| 51 | L2 | lo=6 hi=7 | Two or more elements, so partition. |
| 52 | L9 | lo=6 hi=7 | Pivot strategy: last element. The pivot is already at arr[7]. |
| 53 | L11 | pivot=82 i=6 | pivot = 82. i marks where the next value smaller than the pivot will go. |
| 54 | L13 | pivot=82 i=6 j=6 | Smaller than the pivot: it belongs in the left region. |
| 55 | L14 | pivot=82 i=7 j=6 | arr[6] is already in place; i advances to 7. |
| 56 | L18 | pivot=82 p=7 | Swap the pivot into index 7. Everything left of it is < 82, everything right is >= 82, and 82 will never move again. |
| 57 | L4 | lo=6 hi=6 | Enter quickSort(arr, 6, 6). |
| 58 | L2 | lo=6 hi=6 | Zero or one element: already sorted. |
| 59 | L5 | lo=8 hi=7 | Enter quickSort(arr, 8, 7). |
| 60 | L2 | lo=8 hi=7 | Zero or one element: already sorted. |
| 61 | L6 | Sorted with 14 comparisons and 5 swaps. |
Quick sort (Hoare partition)
Interactive · edit the inputint[] arr · length 8
Hoare's scheme does fewer swaps than Lomuto on average, but the pivot does not end at a fixed index.
1public static void quickSort(int[] arr, int lo, int hi) {2if (lo >= hi) return;3int p = partition(arr, lo, hi);4quickSort(arr, lo, p);5quickSort(arr, p + 1, hi);6}78static int partition(int[] arr, int lo, int hi) {9int pivot = arr[lo + (hi - lo) / 2];10int i = lo - 1, j = hi + 1;11while (true) {12do { i++; } while (arr[i] < pivot);13do { j--; } while (arr[j] > pivot);14if (i >= j) return j;15swap(arr, i, j);16}17}
Live state
n
8
Execution trace (dry run table)Show 110 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=8 | Hoare's scheme does fewer swaps than Lomuto on average, but the pivot does not end at a fixed index. |
| 2 | L1 | lo=0 hi=7 | Enter quickSort(arr, 0, 7). |
| 3 | L9 | lo=0 hi=7 pivot=3 | pivot = arr[3] = 3. Two pointers will walk toward each other. |
| 4 | L10 | pivot=3 i=-1 j=8 | i starts just left of the window and j just right of it. |
| 5 | L12 | pivot=3 i=0 j=8 | i stops: this value belongs on the right side. |
| 6 | L13 | pivot=3 i=0 j=7 | Larger than the pivot, so it is on the correct side. Move j left. |
| 7 | L13 | pivot=3 i=0 j=6 | Larger than the pivot, so it is on the correct side. Move j left. |
| 8 | L13 | pivot=3 i=0 j=5 | Larger than the pivot, so it is on the correct side. Move j left. |
| 9 | L13 | pivot=3 i=0 j=4 | Larger than the pivot, so it is on the correct side. Move j left. |
| 10 | L13 | pivot=3 i=0 j=3 | j stops: this value belongs on the left side. |
| 11 | L14 | pivot=3 i=0 j=3 | i and j both stopped on misplaced values, so swap them. |
| 12 | L15 | pivot=3 i=0 j=3 | Swapped arr[0] and arr[3]. |
| 13 | L12 | pivot=3 i=1 j=3 | i stops: this value belongs on the right side. |
| 14 | L13 | pivot=3 i=1 j=2 | Larger than the pivot, so it is on the correct side. Move j left. |
| 15 | L13 | pivot=3 i=1 j=1 | Larger than the pivot, so it is on the correct side. Move j left. |
| 16 | L13 | pivot=3 i=1 j=0 | j stops: this value belongs on the left side. |
| 17 | L14 | pivot=3 i=1 j=0 | The pointers met or crossed. Split point is j = 0. |
| 18 | L3 | p=0 | Partition returns 0: [0..0] holds values <= pivot and [1..7] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 19 | L4 | lo=0 hi=0 | Enter quickSort(arr, 0, 0). |
| 20 | L2 | lo=0 hi=0 | Zero or one element: already sorted. |
| 21 | L5 | lo=1 hi=7 | Enter quickSort(arr, 1, 7). |
| 22 | L9 | lo=1 hi=7 pivot=9 | pivot = arr[4] = 9. Two pointers will walk toward each other. |
| 23 | L10 | pivot=9 i=0 j=8 | i starts just left of the window and j just right of it. |
| 24 | L12 | pivot=9 i=1 j=8 | i stops: this value belongs on the right side. |
| 25 | L13 | pivot=9 i=1 j=7 | Larger than the pivot, so it is on the correct side. Move j left. |
| 26 | L13 | pivot=9 i=1 j=6 | Larger than the pivot, so it is on the correct side. Move j left. |
| 27 | L13 | pivot=9 i=1 j=5 | Larger than the pivot, so it is on the correct side. Move j left. |
| 28 | L13 | pivot=9 i=1 j=4 | j stops: this value belongs on the left side. |
| 29 | L14 | pivot=9 i=1 j=4 | i and j both stopped on misplaced values, so swap them. |
| 30 | L15 | pivot=9 i=1 j=4 | Swapped arr[1] and arr[4]. |
| 31 | L12 | pivot=9 i=2 j=4 | i stops: this value belongs on the right side. |
| 32 | L13 | pivot=9 i=2 j=3 | Larger than the pivot, so it is on the correct side. Move j left. |
| 33 | L13 | pivot=9 i=2 j=2 | Larger than the pivot, so it is on the correct side. Move j left. |
| 34 | L13 | pivot=9 i=2 j=1 | j stops: this value belongs on the left side. |
| 35 | L14 | pivot=9 i=2 j=1 | The pointers met or crossed. Split point is j = 1. |
| 36 | L3 | p=1 | Partition returns 1: [1..1] holds values <= pivot and [2..7] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 37 | L4 | lo=1 hi=1 | Enter quickSort(arr, 1, 1). |
| 38 | L2 | lo=1 hi=1 | Zero or one element: already sorted. |
| 39 | L5 | lo=2 hi=7 | Enter quickSort(arr, 2, 7). |
| 40 | L9 | lo=2 hi=7 pivot=27 | pivot = arr[4] = 27. Two pointers will walk toward each other. |
| 41 | L10 | pivot=27 i=1 j=8 | i starts just left of the window and j just right of it. |
| 42 | L12 | pivot=27 i=2 j=8 | i stops: this value belongs on the right side. |
| 43 | L13 | pivot=27 i=2 j=7 | j stops: this value belongs on the left side. |
| 44 | L14 | pivot=27 i=2 j=7 | i and j both stopped on misplaced values, so swap them. |
| 45 | L15 | pivot=27 i=2 j=7 | Swapped arr[2] and arr[7]. |
| 46 | L12 | pivot=27 i=3 j=7 | i stops: this value belongs on the right side. |
| 47 | L13 | pivot=27 i=3 j=6 | j stops: this value belongs on the left side. |
| 48 | L14 | pivot=27 i=3 j=6 | i and j both stopped on misplaced values, so swap them. |
| 49 | L15 | pivot=27 i=3 j=6 | Swapped arr[3] and arr[6]. |
| 50 | L12 | pivot=27 i=4 j=6 | i stops: this value belongs on the right side. |
| 51 | L13 | pivot=27 i=4 j=5 | Larger than the pivot, so it is on the correct side. Move j left. |
| 52 | L13 | pivot=27 i=4 j=4 | j stops: this value belongs on the left side. |
| 53 | L14 | pivot=27 i=4 j=4 | The pointers met or crossed. Split point is j = 4. |
| 54 | L3 | p=4 | Partition returns 4: [2..4] holds values <= pivot and [5..7] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 55 | L4 | lo=2 hi=4 | Enter quickSort(arr, 2, 4). |
| 56 | L9 | lo=2 hi=4 pivot=10 | pivot = arr[3] = 10. Two pointers will walk toward each other. |
| 57 | L10 | pivot=10 i=1 j=5 | i starts just left of the window and j just right of it. |
| 58 | L12 | pivot=10 i=2 j=5 | i stops: this value belongs on the right side. |
| 59 | L13 | pivot=10 i=2 j=4 | Larger than the pivot, so it is on the correct side. Move j left. |
| 60 | L13 | pivot=10 i=2 j=3 | j stops: this value belongs on the left side. |
| 61 | L14 | pivot=10 i=2 j=3 | i and j both stopped on misplaced values, so swap them. |
| 62 | L15 | pivot=10 i=2 j=3 | Swapped arr[2] and arr[3]. |
| 63 | L12 | pivot=10 i=3 j=3 | i stops: this value belongs on the right side. |
| 64 | L13 | pivot=10 i=3 j=2 | j stops: this value belongs on the left side. |
| 65 | L14 | pivot=10 i=3 j=2 | The pointers met or crossed. Split point is j = 2. |
| 66 | L3 | p=2 | Partition returns 2: [2..2] holds values <= pivot and [3..4] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 67 | L4 | lo=2 hi=2 | Enter quickSort(arr, 2, 2). |
| 68 | L2 | lo=2 hi=2 | Zero or one element: already sorted. |
| 69 | L5 | lo=3 hi=4 | Enter quickSort(arr, 3, 4). |
| 70 | L9 | lo=3 hi=4 pivot=15 | pivot = arr[3] = 15. Two pointers will walk toward each other. |
| 71 | L10 | pivot=15 i=2 j=5 | i starts just left of the window and j just right of it. |
| 72 | L12 | pivot=15 i=3 j=5 | i stops: this value belongs on the right side. |
| 73 | L13 | pivot=15 i=3 j=4 | Larger than the pivot, so it is on the correct side. Move j left. |
| 74 | L13 | pivot=15 i=3 j=3 | j stops: this value belongs on the left side. |
| 75 | L14 | pivot=15 i=3 j=3 | The pointers met or crossed. Split point is j = 3. |
| 76 | L3 | p=3 | Partition returns 3: [3..3] holds values <= pivot and [4..4] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 77 | L4 | lo=3 hi=3 | Enter quickSort(arr, 3, 3). |
| 78 | L2 | lo=3 hi=3 | Zero or one element: already sorted. |
| 79 | L5 | lo=4 hi=4 | Enter quickSort(arr, 4, 4). |
| 80 | L2 | lo=4 hi=4 | Zero or one element: already sorted. |
| 81 | L5 | lo=5 hi=7 | Enter quickSort(arr, 5, 7). |
| 82 | L9 | lo=5 hi=7 pivot=38 | pivot = arr[6] = 38. Two pointers will walk toward each other. |
| 83 | L10 | pivot=38 i=4 j=8 | i starts just left of the window and j just right of it. |
| 84 | L12 | pivot=38 i=5 j=8 | i stops: this value belongs on the right side. |
| 85 | L13 | pivot=38 i=5 j=7 | Larger than the pivot, so it is on the correct side. Move j left. |
| 86 | L13 | pivot=38 i=5 j=6 | j stops: this value belongs on the left side. |
| 87 | L14 | pivot=38 i=5 j=6 | i and j both stopped on misplaced values, so swap them. |
| 88 | L15 | pivot=38 i=5 j=6 | Swapped arr[5] and arr[6]. |
| 89 | L12 | pivot=38 i=6 j=6 | i stops: this value belongs on the right side. |
| 90 | L13 | pivot=38 i=6 j=5 | j stops: this value belongs on the left side. |
| 91 | L14 | pivot=38 i=6 j=5 | The pointers met or crossed. Split point is j = 5. |
| 92 | L3 | p=5 | Partition returns 5: [5..5] holds values <= pivot and [6..7] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 93 | L4 | lo=5 hi=5 | Enter quickSort(arr, 5, 5). |
| 94 | L2 | lo=5 hi=5 | Zero or one element: already sorted. |
| 95 | L5 | lo=6 hi=7 | Enter quickSort(arr, 6, 7). |
| 96 | L9 | lo=6 hi=7 pivot=82 | pivot = arr[6] = 82. Two pointers will walk toward each other. |
| 97 | L10 | pivot=82 i=5 j=8 | i starts just left of the window and j just right of it. |
| 98 | L12 | pivot=82 i=6 j=8 | i stops: this value belongs on the right side. |
| 99 | L13 | pivot=82 i=6 j=7 | j stops: this value belongs on the left side. |
| 100 | L14 | pivot=82 i=6 j=7 | i and j both stopped on misplaced values, so swap them. |
| 101 | L15 | pivot=82 i=6 j=7 | Swapped arr[6] and arr[7]. |
| 102 | L12 | pivot=82 i=7 j=7 | i stops: this value belongs on the right side. |
| 103 | L13 | pivot=82 i=7 j=6 | j stops: this value belongs on the left side. |
| 104 | L14 | pivot=82 i=7 j=6 | The pointers met or crossed. Split point is j = 6. |
| 105 | L3 | p=6 | Partition returns 6: [6..6] holds values <= pivot and [7..7] holds values >= pivot. The pivot itself is not necessarily in its final place. |
| 106 | L4 | lo=6 hi=6 | Enter quickSort(arr, 6, 6). |
| 107 | L2 | lo=6 hi=6 | Zero or one element: already sorted. |
| 108 | L5 | lo=7 hi=7 | Enter quickSort(arr, 7, 7). |
| 109 | L2 | lo=7 hi=7 | Zero or one element: already sorted. |
| 110 | L6 | Sorted with 43 comparisons and 7 swaps. |
Pivot selection
The pivot decides how balanced the split is. A good pivot is near the median, so each side is about half the array and the recursion has log₂ n levels. A pivot that is the minimum or maximum leaves one side empty and the other with n - 1 elements.
Common strategies are the last or first element (simple, but terrible on sorted input), the middle element (fine for sorted input, but an adversary can still build a bad input), median of three (take the median of first, middle and last), and a random element. In the animation, change the Pivot option and run “Sorted, last pivot (worst)” and then “Sorted, middle pivot”: 28 comparisons versus 13 for 8 values.
| Strategy | Sorted input | Reverse input | Why |
|---|---|---|---|
| First or last | O(n²) | O(n²) | Pivot is always an extreme value |
| Middle | O(n log n) | O(n log n) | Pivot is the median of sorted data |
| Median of 3 | O(n log n) | O(n log n) | Protects against sorted and reversed data |
| Random | O(n log n) expected | O(n log n) expected | No fixed input is always bad |
Randomized pivot
Choose the pivot uniformly at random from arr[lo..hi]. No particular input can be consistently bad, because the running time depends on the random choices and not on the data. The expected number of comparisons is about 1.39 · n · log₂ n.
Swap the chosen element to the end, then use Lomuto as usual. Use ThreadLocalRandom in Java, since Math.random() is shared across threads and slower.
1private static int randomizedPartition(int[] arr, int lo, int hi) {2int r = lo + ThreadLocalRandom.current().nextInt(hi - lo + 1);3swap(arr, r, hi); // move the random pivot to the end4return partition(arr, lo, hi); // Lomuto, pivot = arr[hi]5}
Three-way partition
With many equal keys, two-way partitioning keeps recursing on elements that are already equal to the pivot. Three-way (Dutch national flag) partitioning splits into < pivot, = pivot and > pivot. The equal band is finished immediately and is never sorted again.
Maintain four regions with three indices: [lo..lt-1] < pivot, [lt..i-1] = pivot, [i..gt] unexamined and [gt+1..hi] > pivot. When arr[i] > pivot swap it with arr[gt] and decrement gt, but do not advance i, because the value swapped in has not been examined yet. An array of all equal keys is sorted in one pass: run “All equal”.
Best
O(n)
Average
O(n log n)
Worst
O(n²)
Space
O(log n) stack
Three-way quick sort (Dutch national flag)
Interactive · edit the inputint[] arr · length 8
Three-way partitioning groups every key equal to the pivot together, so duplicates are never sorted again.
1public static void quickSort3(int[] arr, int lo, int hi) {2if (lo >= hi) return;3int pivot = arr[lo + (hi - lo) / 2];4int lt = lo, i = lo, gt = hi;5while (i <= gt) {6if (arr[i] < pivot) swap(arr, lt++, i++);7else if (arr[i] > pivot) swap(arr, i, gt--);8else i++;9}10quickSort3(arr, lo, lt - 1);11quickSort3(arr, gt + 1, hi);12}
Live state
n
8
Execution trace (dry run table)Show 55 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=8 | Three-way partitioning groups every key equal to the pivot together, so duplicates are never sorted again. |
| 2 | L1 | lo=0 hi=7 | Enter quickSort3(arr, 0, 7). |
| 3 | L4 | pivot=4 lt=0 i=0 gt=7 | pivot = 4. Invariant: [lo..lt-1] < pivot, [lt..i-1] = pivot, [i..gt] unexamined, [gt+1..hi] > pivot. |
| 4 | L6 | pivot=4 lt=0 i=0 gt=7 arr[i]=4 | Not smaller, check whether it is larger. |
| 5 | L7 | pivot=4 lt=0 i=0 gt=7 arr[i]=4 | Equal to the pivot: leave it in the middle band and advance i. |
| 6 | L8 | pivot=4 lt=0 i=1 gt=7 | i = 1. The equal band grew by one. |
| 7 | L6 | pivot=4 lt=0 i=1 gt=7 arr[i]=2 | Smaller: swap it into the low region. |
| 8 | L6 | pivot=4 lt=1 i=2 gt=7 | lt = 1, i = 2. The low region grew by one. |
| 9 | L6 | pivot=4 lt=1 i=2 gt=7 arr[i]=4 | Not smaller, check whether it is larger. |
| 10 | L7 | pivot=4 lt=1 i=2 gt=7 arr[i]=4 | Equal to the pivot: leave it in the middle band and advance i. |
| 11 | L8 | pivot=4 lt=1 i=3 gt=7 | i = 3. The equal band grew by one. |
| 12 | L6 | pivot=4 lt=1 i=3 gt=7 arr[i]=4 | Not smaller, check whether it is larger. |
| 13 | L7 | pivot=4 lt=1 i=3 gt=7 arr[i]=4 | Equal to the pivot: leave it in the middle band and advance i. |
| 14 | L8 | pivot=4 lt=1 i=4 gt=7 | i = 4. The equal band grew by one. |
| 15 | L6 | pivot=4 lt=1 i=4 gt=7 arr[i]=1 | Smaller: swap it into the low region. |
| 16 | L6 | pivot=4 lt=2 i=5 gt=7 | lt = 2, i = 5. The low region grew by one. |
| 17 | L6 | pivot=4 lt=2 i=5 gt=7 arr[i]=4 | Not smaller, check whether it is larger. |
| 18 | L7 | pivot=4 lt=2 i=5 gt=7 arr[i]=4 | Equal to the pivot: leave it in the middle band and advance i. |
| 19 | L8 | pivot=4 lt=2 i=6 gt=7 | i = 6. The equal band grew by one. |
| 20 | L6 | pivot=4 lt=2 i=6 gt=7 arr[i]=2 | Smaller: swap it into the low region. |
| 21 | L6 | pivot=4 lt=3 i=7 gt=7 | lt = 3, i = 7. The low region grew by one. |
| 22 | L6 | pivot=4 lt=3 i=7 gt=7 arr[i]=4 | Not smaller, check whether it is larger. |
| 23 | L7 | pivot=4 lt=3 i=7 gt=7 arr[i]=4 | Equal to the pivot: leave it in the middle band and advance i. |
| 24 | L8 | pivot=4 lt=3 i=8 gt=7 | i = 8. The equal band grew by one. |
| 25 | L9 | pivot=4 lt=3 gt=7 | Partition done. All 5 elements equal to 4 are in their final place [3..7]. Recurse only on the strictly smaller and strictly larger parts. |
| 26 | L10 | lo=0 hi=2 | Enter quickSort3(arr, 0, 2). |
| 27 | L4 | pivot=1 lt=0 i=0 gt=2 | pivot = 1. Invariant: [lo..lt-1] < pivot, [lt..i-1] = pivot, [i..gt] unexamined, [gt+1..hi] > pivot. |
| 28 | L6 | pivot=1 lt=0 i=0 gt=2 arr[i]=2 | Not smaller, check whether it is larger. |
| 29 | L7 | pivot=1 lt=0 i=0 gt=2 arr[i]=2 | Larger: swap it to the high region. Do not advance i, because the value swapped in is unexamined. |
| 30 | L7 | pivot=1 lt=0 i=0 gt=1 | gt = 1. The high region grew by one. |
| 31 | L6 | pivot=1 lt=0 i=0 gt=1 arr[i]=2 | Not smaller, check whether it is larger. |
| 32 | L7 | pivot=1 lt=0 i=0 gt=1 arr[i]=2 | Larger: swap it to the high region. Do not advance i, because the value swapped in is unexamined. |
| 33 | L7 | pivot=1 lt=0 i=0 gt=0 | gt = 0. The high region grew by one. |
| 34 | L6 | pivot=1 lt=0 i=0 gt=0 arr[i]=1 | Not smaller, check whether it is larger. |
| 35 | L7 | pivot=1 lt=0 i=0 gt=0 arr[i]=1 | Equal to the pivot: leave it in the middle band and advance i. |
| 36 | L8 | pivot=1 lt=0 i=1 gt=0 | i = 1. The equal band grew by one. |
| 37 | L9 | pivot=1 lt=0 gt=0 | Partition done. All 1 element equal to 1 are in their final place [0..0]. Recurse only on the strictly smaller and strictly larger parts. |
| 38 | L10 | lo=0 hi=-1 | Enter quickSort3(arr, 0, -1). |
| 39 | L2 | lo=0 hi=-1 | Zero or one element: done. |
| 40 | L11 | lo=1 hi=2 | Enter quickSort3(arr, 1, 2). |
| 41 | L4 | pivot=2 lt=1 i=1 gt=2 | pivot = 2. Invariant: [lo..lt-1] < pivot, [lt..i-1] = pivot, [i..gt] unexamined, [gt+1..hi] > pivot. |
| 42 | L6 | pivot=2 lt=1 i=1 gt=2 arr[i]=2 | Not smaller, check whether it is larger. |
| 43 | L7 | pivot=2 lt=1 i=1 gt=2 arr[i]=2 | Equal to the pivot: leave it in the middle band and advance i. |
| 44 | L8 | pivot=2 lt=1 i=2 gt=2 | i = 2. The equal band grew by one. |
| 45 | L6 | pivot=2 lt=1 i=2 gt=2 arr[i]=2 | Not smaller, check whether it is larger. |
| 46 | L7 | pivot=2 lt=1 i=2 gt=2 arr[i]=2 | Equal to the pivot: leave it in the middle band and advance i. |
| 47 | L8 | pivot=2 lt=1 i=3 gt=2 | i = 3. The equal band grew by one. |
| 48 | L9 | pivot=2 lt=1 gt=2 | Partition done. All 2 elements equal to 2 are in their final place [1..2]. Recurse only on the strictly smaller and strictly larger parts. |
| 49 | L10 | lo=1 hi=0 | Enter quickSort3(arr, 1, 0). |
| 50 | L2 | lo=1 hi=0 | Zero or one element: done. |
| 51 | L11 | lo=3 hi=2 | Enter quickSort3(arr, 3, 2). |
| 52 | L2 | lo=3 hi=2 | Zero or one element: done. |
| 53 | L11 | lo=8 hi=7 | Enter quickSort3(arr, 8, 7). |
| 54 | L2 | lo=8 hi=7 | Zero or one element: done. |
| 55 | L12 | Sorted with 23 comparisons and 5 swaps. |
Worst-case O(n²)
The worst case happens when every partition is as lopsided as possible: sizes 0 and n - 1. The work is (n - 1) + (n - 2) + … + 1 = n(n - 1)/2 comparisons, and the recursion depth reaches n, which can overflow the stack on large arrays.
Typical triggers are sorted or reverse sorted input with a first or last pivot, and all-equal input with two-way partitioning. Production sorts defend against it with median-of-three or random pivots, three-way partitioning, and a fallback to heap sort once recursion gets too deep (introsort, see lesson 18.07).
Tail recursion
Quick sort makes two recursive calls, and the second is a tail call. Java does not eliminate tail calls, so instead recurse into the smaller side and loop on the larger side. The smaller side is at most half the size, so the stack depth is at most log₂ n even when the partitions are badly unbalanced.
This bounds stack space to O(log n) in the worst case, although time may still be O(n²).
1static void quickSort(int[] arr, int lo, int hi) {2while (lo < hi) {3int p = partition(arr, lo, hi);4if (p - lo < hi - p) { // left side is smaller: recurse into it5quickSort(arr, lo, p - 1);6lo = p + 1; // then loop on the larger right side7} else {8quickSort(arr, p + 1, hi);9hi = p - 1;10}11}12}
Worst
O(n²) time
Space
O(log n) stack guaranteed
Practice ladder
Now try it yourself
Easy
- Partition an array around a pivot (Lomuto)
- Sort colors (Dutch national flag, values 0/1/2)
- Move all zeros to the end
Medium
- Implement quick sort with a random pivot
- Count the comparisons quick sort makes on sorted input of size n
- Sort an array with many duplicates using three-way partitioning
Advanced
- Introsort: switch to heap sort at depth 2·log₂ n
- Dual-pivot quick sort partitioning
- Prove the expected 2n·ln n comparison count
Key takeaways
- Quick sort is O(n log n) on average but O(n²) in the worst case, and it is not stable.
- Pivot choice decides everything: median-of-three or random pivots avoid the usual bad inputs.
- Three-way partitioning makes duplicates cheap; recursing into the smaller side bounds stack depth.