A learning sheet β you'll rarely write a sort, but you must know each one's guarantees, the divide-and-conquer subroutines, and the meta-move: "sort first, and the problem collapses."
Sorting wears two hats. Hat 1 β the toolbox: the library sorts in O(n log n), so you usually just call it β but you must know which sort it uses and its guarantees (stable? in-place? worst case?), because those decide correctness. Hat 2 β the weapon: a huge class of problems isn't "sort," but becomes trivial once sorted β enabling two pointers, greedy, sweep lines, or a custom comparator.
Underneath the named algorithms sit three reusable subroutines that show up as building blocks far beyond sorting: the merge (combine two sorted runs), the partition (split around a pivot), and quickselect (find the k-th element without fully sorting).
O(nΒ²) β every element compared against many others.T(n) = 2Β·T(n/2) + O(n) β O(n log n). Log levels of halving, each level doing O(n) of merging.O(n log n), and stable (keep <= in the merge) β the cost is O(n) extra space.| Ask | If yes β | Pattern |
|---|---|---|
Need stable and/or guaranteed O(n log n) (or sorting a linked list)? | merge sort | Merge sort (Β§4.1) |
| Want in-place, fast average, stability not needed? | quicksort / the library default | Quicksort (Β§4.2) |
| Only need the k-th element or top-k by value? | don't sort β partition around it | Quickselect (Β§4.3) |
| Keys are a small bounded range of integers? | count / bucket / radix β O(n) | Linear sorts (Β§4.4) |
| Not "sort," but easy once sorted? | sort, then two-pointer / greedy / sweep / comparator | Sort-as-prep (Β§4.5) |
O(n) avg) or a heap (O(n log k)) beat a full O(n log n) sort.O(n); the n log n lower bound only binds comparison sorts.| Words in the prompt | Reach for |
|---|---|
| "stable", "count inversions", "merge sorted", "sort a linked list" | Merge Β§4.1 |
| "in-place", "sort the array" (generic), "partition around a pivot" | Quicksort Β§4.2 |
| "k-th largest/smallest", "top k closest/frequent", "median of a stream" | Quickselect Β§4.3 (or heap) |
| "sort colors / 0-1-2", "values in [0, k]", "bucket", "radix", "maximum gap" | Linear Β§4.4 |
| "merge intervals", "meeting rooms", "largest number", "group anagrams", "3Sum" | Sort-as-prep Β§4.5 |
Β§4.1β4.3 are the divide-and-conquer trio (merge, partition, select). Β§4.4β4.5 are the "beat or avoid the sort" moves. Each: a one-liner, a π invariant, a template, a π€ interview lens.
βSort each half, then zip the two sorted runs together.β
<= on ties preserves left-before-right order (stability).function mergeSort(a: number[]): number[] {
if (a.length <= 1) return a;
const mid = a.length >> 1;
return merge(mergeSort(a.slice(0, mid)), mergeSort(a.slice(mid)));
}
// the merge step IS a two-sequence two-pointer
function merge(a: number[], b: number[]): number[] {
const out: number[] = []; let i = 0, j = 0;
while (i < a.length && j < b.length) out.push(a[i] <= b[j] ? a[i++] : b[j++]); // <= β stable
while (i < a.length) out.push(a[i++]);
while (j < b.length) out.push(b[j++]);
return out;
}
O(n) space, and the O(n log n) recurrence. Β· Escalation: sort β "Count Inversions" (count cross-pairs during merge) β "Merge k Sorted Lists" β external merge sort for data that doesn't fit in memory.βPick a pivot, shove smaller-left / bigger-right, recurse on each side.β
Setup: the partition is the reusable jewel β it places the pivot at its final sorted index and returns it. Quicksort just recurses on both sides; quickselect (Β§4.3) recurses on one.
a[lo..i) are all < pivot and a[i..j) are all β₯ pivot. Swapping the pivot into slot i at the end puts it exactly where it belongs, so each side can be sorted independently.function quickSort(a: number[], lo = 0, hi = a.length - 1): void {
if (lo >= hi) return;
const p = partition(a, lo, hi);
quickSort(a, lo, p - 1);
quickSort(a, p + 1, hi);
}
// Lomuto partition; randomize the pivot to dodge the O(nΒ²) worst case
function partition(a: number[], lo: number, hi: number): number {
const r = lo + Math.floor(Math.random() * (hi - lo + 1));
[a[r], a[hi]] = [a[hi], a[r]]; // random pivot β swap to the end
const pivot = a[hi]; let i = lo;
for (let j = lo; j < hi; j++) if (a[j] < pivot) { [a[i], a[j]] = [a[j], a[i]]; i++; }
[a[i], a[hi]] = [a[hi], a[i]];
return i;
}
O(nΒ²) worst case (sorted input + last-element pivot) and the random-pivot fix; that it's in-place but not stable. Β· Escalation: partition β quickselect (Β§4.3) β 3-way partition (Dutch flag) when there are many duplicate keys.βPartition, then recurse into only the side that holds k.β
Setup: a full sort is wasteful if you only need rank k. After a partition, the pivot's final index tells you which side k is on β keep only that side.
n + n/2 + n/4 + β¦ = O(n) (a geometric series, unlike sort's O(n log n)). Because you recurse into one side, the log factor disappears.// k-th SMALLEST (0-indexed k); for k-th largest use a.length-1-k
function quickselect(a: number[], k: number): number {
let lo = 0, hi = a.length - 1;
while (true) {
const p = partition(a, lo, hi); // same partition as Β§4.2
if (p === k) return a[p];
if (p < k) lo = p + 1; // k is to the right
else hi = p - 1; // k is to the left
}
}
O(n) geometric-series argument and the O(nΒ²) worst case (random pivot or median-of-medians for a guaranteed bound). Β· Escalation: k-th largest β "Top K Frequent Elements" (or bucket sort) β "K Closest Points to Origin."βBounded integer keys? Count them β no comparisons, O(n).β
Setup: comparison sorts are stuck at Ξ©(n log n). If keys are integers in a small range, you can tally instead of compare and beat that bound.
count[v] is the exact number of times v appears. Emitting values 0, 1, 2, β¦ in order, each repeated its count, produces a sorted array β correctness comes from the range being known and small, not from comparisons.// Counting sort β keys in [0, maxVal]; O(n + maxVal)
function countingSort(a: number[], maxVal: number): number[] {
const count = new Array(maxVal + 1).fill(0);
for (const x of a) count[x]++;
const out: number[] = [];
for (let v = 0; v <= maxVal; v++) while (count[v]-- > 0) out.push(v);
return out;
}
O(n) actually beats O(n log n) (range βͺ n log n), and counting sort's stability if you emit by prefix-sum position. Β· Escalation: counting β "Sort Colors" (3-way Dutch flag, one pass) β bucket sort for "Top K Frequent" β radix sort ("Maximum Gap").βThe problem isn't sorting β but sorting makes it fall over.β
Setup: sorting imposes order, which unlocks linear follow-ups: two pointers, greedy-by-order, sweep lines, grouping, or a custom comparator that defines the order you actually need.
// Largest Number β order so that concatenation is biggest (custom total order)
function largestNumber(nums: number[]): string {
const s = nums.map(String).sort((x, y) => (y + x).localeCompare(x + y)); // "x then y" vs "y then x"
return s[0] === '0' ? '0' : s.join(''); // all zeros β "0"
}
| Algorithm | Best | Avg | Worst | Space | Stable? | In-place? |
|---|---|---|---|---|---|---|
| Insertion | O(n) | O(nΒ²) | O(nΒ²) | O(1) | β | β |
| Merge | O(n log n) | O(n log n) | O(n log n) | O(n) | β | β |
| Quicksort | O(n log n) | O(n log n) | O(nΒ²) | O(log n) | β | β |
| Heapsort | O(n log n) | O(n log n) | O(n log n) | O(1) | β | β |
| Counting / Radix | O(n+k) | O(n+k) | O(n+k) | O(n+k) | β | β |
| Quickselect (k-th) | O(n) | O(n) | O(nΒ²) | O(1) | β | β |
O(n log n) (Timsort: stable; introsort: not). Reach for merge when you need stability/guarantees, quickselect/heap for top-k, counting/radix for bounded integers. The Ξ©(n log n) floor is for comparison sorts only.| Bug | The input that exposes it |
|---|---|
| Quicksort worst case from a fixed (last-element) pivot. | Already-sorted [1,2,β¦,n] β O(nΒ²) / stack overflow. Fix: random or median-of-three pivot. |
| JS default sort is lexicographic, not numeric. | [10, 2, 1].sort() β [1, 10, 2]. Pass a numeric comparator (a,b)=>a-b. |
| Assuming stability from an unstable sort. | Sort records by one key expecting prior order kept β ties reorder unexpectedly. |
| Counting sort with negatives or a huge range. | Negative keys β index out of bounds; range 10βΉ β out of memory. |
| Non-transitive custom comparator. | A comparator where a<b, b<c, but c<a β engine produces garbage / throws. |
| Full sort when only top-k is needed. | "k-th largest of 10β· items" β O(n log n) when quickselect/heap is faster; flagged in review. |
n the array can be its own hash table (41, 448) β cyclic sort or sign-marking, O(n) with no sort. 128 is "don't sort, use a set and start only at sequence heads". 853 is sort-then-one-pass.// 41 First Missing Positive β cyclic sort: put v at index v-1
function firstMissingPositive(nums: number[]): number {
const n = nums.length;
for (let i = 0; i < n; i++) {
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
const j = nums[i] - 1, tmp = nums[i]; // temp swap β [nums[i], nums[nums[i]-1]] = ... reads the modified nums[i]
nums[i] = nums[j]; nums[j] = tmp;
}
}
for (let i = 0; i < n; i++) if (nums[i] !== i + 1) return i + 1;
return n + 1;
}
// 853 Car Fleet β sort by position (closest to target first), compare arrival times
function carFleet(target: number, position: number[], speed: number[]): number {
const cars = position.map((p, i) => [p, (target - p) / speed[i]]).sort((a, b) => b[0] - a[0]);
let fleets = 0, lead = 0;
for (const [, t] of cars) if (t > lead) { fleets++; lead = t; } // slower than the fleet ahead β new fleet
return fleets;
}
// 128 Longest Consecutive Sequence β O(n): only count from sequence HEADS
function longestConsecutive(nums: number[]): number {
const set = new Set(nums); let best = 0;
for (const x of set) {
if (set.has(x - 1)) continue; // not a head
let len = 1; while (set.has(x + len)) len++;
best = Math.max(best, len);
}
return best;
}
// 448 Find All Numbers Disappeared β mark presence by negating nums[v-1]
function findDisappearedNumbers(nums: number[]): number[] {
for (const x of nums) { const i = Math.abs(x) - 1; if (nums[i] > 0) nums[i] = -nums[i]; }
const res: number[] = [];
nums.forEach((v, i) => { if (v > 0) res.push(i + 1); });
return res;
}
i loops forever on duplicates). 853: merge on t β€ lead. 128: skip non-heads or it is O(nΒ²). 448: read with Math.abs, negate only if still positive.Own each pattern and these are in range. Bold = Hard tier.
| Pattern | Representative LeetCode | Tier |
|---|---|---|
| Merge sort | 912 Sort an Array Β· 148 Sort List Β· 315 Count of Smaller Β· 493 Reverse Pairs | Med β Hard |
| Quick sort | 912 Sort an Array Β· 75 Sort Colors | Med |
| Quickselect | 215 Kth Largest Β· 973 K Closest Points Β· 347 Top K Frequent | Med |
| Linear (counting / radix) | 1122 Relative Sort Β· 164 Maximum Gap Β· 451 Sort by Frequency Β· 41 First Missing Positive Β· 448 Disappeared Numbers | Med β Hard |
| Sort as prep | 56 Merge Intervals Β· 252/253 Meeting Rooms Β· 179 Largest Number Β· 853 Car Fleet Β· 128 Longest Consecutive (set beats sort) | Med |
Cover the right column (blurred β hover/tap to reveal). Say your answer out loud first.
| Prompt | Answer |
|---|---|
| What three properties decide which sort you pick? | Stable? In-place? Worst-case bound? (Plus: is the data bounded-range or do you only need top-k?) |
Why is merge sort O(n log n) and what does it cost? | log n levels of halving Γ O(n) merging per level; it costs O(n) extra space (and is stable). |
| What does the partition guarantee after one call? | The pivot is at its final sorted index; everything left is < it, everything right is β₯ it. The two sides are independent. |
Why is quickselect O(n) average but sort is O(n log n)? | Quickselect recurses into one side (n + n/2 + β¦ = O(n)); sort recurses into both, adding the log factor. |
When does an O(n) sort exist? | Non-comparison sorts (counting/radix) on bounded integer keys β the Ξ©(n log n) floor is for comparison sorts only. |
| What must a custom comparator be, or the sort breaks? | A consistent total order β in particular transitive; and in JS it must return a number, not a boolean. |
| Name three problems that "collapse" once sorted. | 3Sum (sort + two-pointer), Merge Intervals (sort + sweep), Group Anagrams (sorted-string key). Many more. |
| First Missing Positive β why is the answer always in [1, n+1]? | n numbers can cover at most 1..n; if all of them are present the answer is n+1. That bound lets the array itself act as the hash table. |
| Longest Consecutive Sequence β why is it O(n) with a while inside a for? | You only start counting at a sequence head (xβ1 absent), so every element is walked at most once from its head. |
mergeSort + merge (stable)partition (random pivot) + quickSortquickselect (k-th smallest)countingSortlargestNumber)For each: state the guarantee (stable? in-place? worst case?) as a comment before the code.