Last 30 Days
No notifications
These algorithms achieve O(n log n) time by using divide-and-conquer or clever counting strategies.
| Algorithm | Best | Average | Worst | Space | Stable? |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | O(k) | Yes |
Once you cross n = a few thousand, the O(n²) sorts become unusable and you reach for the O(n log n) family — Merge sort, Quicksort, Heap sort — plus the *non-comparison* sorts (Counting, Radix, Bucket) that beat the O(n log n) lower bound by exploiting the structure of the data. Every standard library sort you've ever called is one of these (or a hybrid of several).
Three big ideas, each gives an O(n log n) algorithm:
1. Divide and Conquer → split the array, sort each half, *combine*. → Merge sort. 2. Partition → pick a pivot, put smaller things left, larger right, recurse. → Quicksort. 3. Heap → repeatedly pull the max out of a heap. → Heap sort.
A separate idea, breaking the comparison-based O(n log n) lower bound:
4. Don't compare elements at all → count, distribute, or bucket. → Counting / Radix / Bucket sort.
Real-world libraries (Timsort, Introsort, PDQSort) glue 2–3 of these together because no single algorithm wins on every input.
1. Picking the first element as the quicksort pivot on already-sorted data. Instant O(n²). Use random or median-of-three.
2. Forgetting merge sort needs O(n) extra memory. It's *not* in-place. If memory is tight, this rules it out.
3. Thinking quicksort is always faster than mergesort. True on average for arrays in RAM (cache locality), false for linked lists or external sorting (disk).
4. Using radix/counting sort on huge value ranges. Counting sort is O(n + k); if \k = 10⁹\, that's worse than n log n.
5. Assuming any sort you've heard of is stable. Quicksort and heapsort are not. If you sort by multiple keys, this matters.
6. Reimplementing your own sort in production. Standard libraries are *fast*, *correct*, and *adaptive*. The right answer is almost always \arr.sort(...)\.
\\\`js
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = arr.length >> 1;
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(a, b) {
const out = []; let i = 0, j = 0;
while (i < a.length && j < b.length) {
if (a[i] <= b[j]) out.push(a[i++]);
else out.push(b[j++]);
}
while (i < a.length) out.push(a[i++]);
while (j < b.length) out.push(b[j++]);
return out;
}
\\\`
<=\ in \merge\ preserves order of equal elements).merge\.\\\`js
function quicksort(arr, lo = 0, hi = arr.length - 1) {
if (lo >= hi) return arr;
const p = partition(arr, lo, hi);
quicksort(arr, lo, p - 1);
quicksort(arr, p + 1, hi);
return arr;
}
// Lomuto partition with last element as pivot
function partition(arr, lo, hi) {
const pivot = arr[hi];
let i = lo;
for (let j = lo; j < hi; j++) {
if (arr[j] < pivot) { [arr[i], arr[j]] = [arr[j], arr[i]]; i++; }
}
[arr[i], arr[hi]] = [arr[hi], arr[i]];
return i;
}
\\\`
| Strategy | Worst case |
| First / last element | O(n²) on sorted input |
| Random | O(n²) astronomically rare |
| Median-of-three (first/mid/last) | O(n²) almost impossible |
| Ninther (median of 3 medians-of-3) | basically guaranteed O(n log n) |
Lomuto vs Hoare partition: Lomuto is simpler (one pointer). Hoare uses two pointers from both ends, does ~3× fewer swaps on average — what real libraries use.
\\\`
heapSort(arr):
buildMaxHeap(arr) // O(n)
for i = n - 1 down to 1:
swap(arr[0], arr[i]) // largest moves to position i
heapifyDown(arr, 0, i) // restore heap on prefix [0..i)
\\\`
Every comparison-based sort can be modeled as a binary decision tree where each leaf represents one of the n! possible orderings. Such a tree must have ≥ n! leaves, so its height ≥ log₂(n!) ≈ n log₂ n (Stirling). No comparison sort can beat O(n log n) in the worst case. Counting / Radix / Bucket dodge this because they don't compare elements — they exploit *structure*.
Counting Sort — O(n + k), works when keys are integers in a known small range [0, k]:
\\\`
count = array of size k+1, all zeros
for x in arr: count[x]++
out = []; for v from 0 to k: repeat count[v] times: out.push(v)
\\\`
Stable when implemented with prefix sums. Space O(k). Useless when k ≫ n (sorting 32-bit ints would need a 16 GB count array).
Radix Sort — O(d · (n + k)). Sort by each digit, least-significant first, using a stable sub-sort (counting sort). For fixed-width keys, d is a small constant → effectively O(n). Used in databases, graphics (z-buffer), GPU sorts.
Bucket Sort — O(n + k) average. Distribute elements into buckets based on value range, sort each bucket, concatenate. Best when input is uniformly distributed; worst case O(n²) when everything piles into one bucket.
| Language / API | Algorithm | |||||||||
Python \sorted()\ | Timsort — natural-run detection + insertion sort + merge with galloping | |||||||||
Java \Arrays.sort(Object[])\ | Timsort | |||||||||
Java \Arrays.sort(int[])\ | Dual-Pivot Quicksort + insertion sort | |||||||||
C++ \std::sort\ | Introsort — quicksort, switch to heapsort if depth > 2 log₂ n, insertion sort for small partitions | |||||||||
C++ \std::stable_sort\ | Mergesort | |||||||||
Rust \sort\ | Stable PDQ-mergesort (Timsort variant) | |||||||||
Rust \sort_unstable\ | PDQSort — pattern-defeating quicksort | |||||||||
| V8 (JavaScript since 2018) | Timsort | |||||||||
Go \sort.Sort\ | Introsort variant | Notice: every single one is a hybrid. The one-algorithm-fits-all sort is a textbook fiction. Advanced: Choosing the Right Algorithm | Scenario | Best choice | Why | |||||
| General-purpose, in RAM | Introsort / PDQSort / Timsort | fast average, cache-friendly, hybrid safety net | ||||||||
| Stability required | Timsort / Mergesort | preserve equal-element order | ||||||||
| O(n log n) guarantee + O(1) space | Heapsort | no worst-case spike, no extra memory | ||||||||
| Sort huge integers in known range | Counting sort | O(n + k) beats O(n log n) | ||||||||
| Sort fixed-width strings / IDs | Radix sort | O(d · n) with small d | ||||||||
| Uniformly distributed floats | Bucket sort | expected O(n) | ||||||||
| Linked lists | Mergesort | doesn't need random access | ||||||||
| External / disk-based | Mergesort | sequential I/O, natural merging | ||||||||
| Nearly-sorted data | Timsort | adaptive — exploits existing order | ||||||||
| Real-time, embedded | Heapsort | predictable bounds | Advanced: Comparison Cheat Sheet | Algorithm | Best | Average | Worst | Space | Stable | In-place |
| Mergesort | 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 | O(n+k) | O(n+k) | O(n+k) | O(k) | ✓ | ✗ | ||||
| Radix | O(d·n) | O(d·n) | O(d·n) | O(n+k) | ✓ | ✗ | ||||
| Bucket | O(n+k) | O(n+k) | O(n²) | O(n+k) | ✓ | ✗ | ||||
| Timsort | O(n) | O(n log n) | O(n log n) | O(n) | ✓ | ✗ | ||||
| Introsort | O(n log n) | O(n log n) | O(n log n) | O(log n) | ✗ | ✓ |
1. Implement merge sort with the recursive split + iterative merge. Test on \[]\, \[5]\, \[3,1,2,5,4]\. Then add an inversion counter — sort \[5,4,3,2,1]\ and confirm 10 inversions.
2. Implement quicksort with Lomuto partition and last-element pivot. Run it on \[1,2,3,…,1000]\. Then switch to random pivot — confirm runtime drops dramatically.
3. Implement counting sort. Use it inside a radix sort for an array of 4-digit integers. Time it vs quicksort on n = 100,000.
4. Sort an array of \{name, score}\ objects by \score\ using a stable sort — verify two equal scores keep their original order. Then do it with quicksort and confirm they don't.