Day 11: Quick Sort
DAY 11 / 30 · WEEK 2
Goal of the day
Section titled “Goal of the day”- Implement quicksort: pick a pivot, partition around it, recurse on both sides.
- Understand why the average case is O(n log n) but the worst case is O(n²).
- Watch the pivot land in its exact final sorted position on every partition step.
Concept
Section titled “Concept”Line up a group of people to sort by height. Pick one person as a reference point — the pivot — and have everyone shorter step to their left, everyone taller step to their right. Once that’s done, the pivot is standing in exactly the spot they’ll occupy in the final sorted line — nobody will ever move them again. Now repeat the same process independently on the “shorter” group and the “taller” group, each shrinking the problem until every group has 0 or 1 people left.
This is different from Day 10’s merge sort: merge sort does easy splits (always exactly in half) but hard combines (a real merge step). Quicksort does hard splits (the partition does real work) but trivial combines (there’s nothing left to do — once both sides are sorted, the whole array is sorted, because the pivot was already in place).
Diagram
Section titled “Diagram”Interactive visualization: full sort, partition + recurse
Section titled “Interactive visualization: full sort, partition + recurse”Running example: arr = [5, 2, 8, 1, 9, 3]
Press Play or Step to begin.
Step 0 / 18
The code
Section titled “The code”function quickSort(arr, lo = 0, hi = arr.length - 1) { if (lo < hi) { const p = partition(arr, lo, hi); quickSort(arr, lo, p - 1); // everything left of the pivot quickSort(arr, p + 1, hi); // everything right of the pivot } return arr;}
function partition(arr, lo, hi) { const pivot = arr[hi]; // pick the last element as pivot let i = lo - 1; // boundary of "elements ≤ pivot seen so far" for (let j = lo; j < hi; j++) { if (arr[j] <= pivot) { i++; [arr[i], arr[j]] = [arr[j], arr[i]]; } } [arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]]; // pivot into its final slot return i + 1; // the pivot's final index}Line by line:
partition—itracks the boundary of the “small” region. Every timearr[j] <= pivot, we grow that region by one and swap the new small element into it. Everything betweeni+1andjat any moment is “seen, but bigger than pivot.”- The final swap places the pivot right after the last “small” element — that’s its permanent, correct position. This is why quicksort needs no separate merge step, unlike Day 10.
quickSort’sif (lo < hi)is the base case: a range of 0 or 1 elements needs no work.
Dry run: partition([5, 2, 8, 1, 3], lo=0, hi=4, pivot=3)
Section titled “Dry run: partition([5, 2, 8, 1, 3], lo=0, hi=4, pivot=3)”| j | arr[j] | ≤ pivot? | Action | Array after |
|---|---|---|---|---|
| 0 | 5 | No | — | [5, 2, 8, 1, 3] |
| 1 | 2 | Yes | i=0, swap(0,1) | [2, 5, 8, 1, 3] |
| 2 | 8 | No | — | [2, 5, 8, 1, 3] |
| 3 | 1 | Yes | i=1, swap(1,3) | [2, 1, 8, 5, 3] |
| — | — | — | final swap(2, 4) | [2, 1, 3, 5, 8] |
3 lands at index 2 — everything before it (2, 1) is ≤ 3, everything after (5, 8) is not yet sorted but is guaranteed > 3.
Complexity
Section titled “Complexity”| Case | Time | Space | Why |
|---|---|---|---|
| Average | O(n log n) | O(log n) | Pivots tend to roughly halve the array — log n levels of recursion, O(n) work per level. |
| Worst | O(n²) | O(n) | A consistently bad pivot (e.g. always the smallest/largest) splits n elements into 1 and n−1 — n levels deep instead of log n. |
Space is the recursion depth, not extra arrays — quicksort partitions in place, unlike merge sort’s O(n) auxiliary arrays. That’s quicksort’s main practical advantage.
Interview corner
Section titled “Interview corner”What causes quicksort’s O(n²) worst case? Consistently picking a bad pivot — the smallest or largest remaining element every time. On an already-sorted array, always picking the last element as pivot (like today’s code) triggers exactly this.
↳ Common follow-up: “How do you avoid this?” — pick a random pivot, or the median of the first/middle/last elements, instead of always the same position. This makes the worst case astronomically unlikely, not impossible.Quicksort vs. merge sort — when would you choose each? Quicksort is usually faster in practice (better cache locality, in-place, smaller constant factor), but merge sort has a guaranteed O(n log n) and is stable. If worst-case guarantees matter (e.g. real-time systems) or ties must preserve order, merge sort wins.
⚠ Trap: don’t say “quicksort is always faster” — it’s faster on average, with a worse worst case. Precision here signals real understanding.-
Is quicksort stable? No — the partition step swaps elements across potentially large distances, which can reorder equal elements relative to each other.
How to talk through it out loud: name the trade-off explicitly — “quicksort partitions in place, so it avoids merge sort’s O(n) extra space, but that trade-off is a worst case that depends on pivot choice.” Following up with how you’d choose a pivot to avoid the worst case (random or median-of-three) shows you know the failure mode and how to defend against it, not just the happy path.
Practice problems
Section titled “Practice problems”-
Partition Array According to Given Pivot (LeetCode) — Easy
Given an array
numsand a valuepivot, rearrange the elements so everything less thanpivotcomes first, then everything equal to it, then everything greater — but unlike today’s in-place partition, you must preserve each group’s original relative order.Solution
function pivotArray(nums, pivot) {const less = [], equal = [], greater = [];for (const n of nums) {if (n < pivot) less.push(n);else if (n === pivot) equal.push(n);else greater.push(n);}return [...less, ...equal, ...greater];} -
Kth Largest Element in an Array (LeetCode) — Medium
Find the kth largest element in an unsorted array. (You solved this with a selection-sort approach on Day 8b — today, solve it faster.)
Solution
// Quickselect: like quicksort, but only recurse into the side that// contains the answer — average O(n) instead of O(n log n).function findKthLargest(nums, k) {const target = nums.length - k; // index of kth largest in sorted orderlet lo = 0, hi = nums.length - 1;while (true) {const p = partition(nums, lo, hi);if (p === target) return nums[p];else if (p < target) lo = p + 1;else hi = p - 1;}}function partition(arr, lo, hi) {const pivot = arr[hi];let i = lo - 1;for (let j = lo; j < hi; j++) {if (arr[j] <= pivot) { i++; [arr[i], arr[j]] = [arr[j], arr[i]]; }}[arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]];return i + 1;} -
3Sum (LeetCode) — Medium
Given an array of integers, find all unique triplets that sum to zero. (Combines Day 4’s two-pointer technique with sorting — a classic pairing of today’s and an earlier day’s ideas.)
Solution
function threeSum(nums) {nums = [...nums].sort((a, b) => a - b);const result = [];for (let i = 0; i < nums.length - 2; i++) {if (i > 0 && nums[i] === nums[i - 1]) continue; // skip duplicate first elementslet left = i + 1, right = nums.length - 1;while (left < right) {const sum = nums[i] + nums[left] + nums[right];if (sum === 0) {result.push([nums[i], nums[left], nums[right]]);while (left < right && nums[left] === nums[left + 1]) left++;while (left < right && nums[right] === nums[right - 1]) right--;left++; right--;} else if (sum < 0) left++;else right--;}}return result;}