Last 30 Days
No notifications
Sorting means arranging elements in a specific order. These basic algorithms are simple but teach fundamental concepts.
| Algorithm | Best | Average | Worst | Space | Stable? |
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
Sorting feels like a solved problem — every language has \sort()\ built in — but the *basic* sorts (Bubble, Selection, Insertion) are still worth knowing well. Real production sorts (Timsort, Introsort) are hybrids that fall back to insertion sort on small partitions. Plus, the only way to genuinely understand "why is O(n log n) the limit?" is to first feel the pain of O(n²).
The three classic O(n²) algorithms — Bubble, Selection, Insertion — all do the same job (rearrange an array into order) using only comparisons and swaps of adjacent or nearby elements. They differ in which pair to compare next and how many swaps it takes. Each one teaches a different lesson, and each one wins in some real situation.
Imagine sorting a hand of playing cards.
1. Not adding the "did anything swap?" flag to bubble sort. Without it, bubble sort is O(n²) on already-sorted input. *With* it, it's O(n). 2. Confusing "stable" with "in-place". They're orthogonal. Stable = equal elements keep relative order. In-place = O(1) extra space. Insertion sort is both. Selection sort is in-place but not stable. 3. Saying "Selection Sort is the worst because it's O(n²)". It's the worst on *time* but the best on writes (exactly n − 1 swaps). On hardware where writes are expensive (EEPROM, flash) selection sort wins. 4. Forgetting that insertion sort is O(n) on nearly-sorted data. This is *why* Timsort uses it. 5. Using bubble sort in production. Don't. There is essentially no input on which bubble sort is the right choice over insertion sort. Outside teaching, treat it as folklore. 6. Sorting a 1,000,000-element array with bubble sort to "see how long it takes" — and then concluding "sorting is slow". You measured the wrong algorithm.
\\\`js
function bubble(arr) {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
if (!swapped) break; // already sorted
}
return arr;
}
\\\`
i\, the largest \i + 1\ elements are guaranteed to be at the end — that's why the inner bound is \n - 1 - i\.\\\`js
function selection(arr) {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let min = i;
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[min]) min = j;
}
if (min !== i) [arr[i], arr[min]] = [arr[min], arr[i]];
}
return arr;
}
\\\`
\\\`js
function insertion(arr) {
for (let i = 1; i < arr.length; i++) {
const key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // shift right
j--;
}
arr[j + 1] = key;
}
return arr;
}
\\\`
d\ is the number of inversions.A sort is stable if equal elements keep their original relative order. Real example:
\\\`
records = [{name:"Ana", score:90}, {name:"Bo", score:80}, {name:"Cy", score:90}]
sortByScore(records) // stable → Ana still before Cy among score=90
\\\`
You need stability when sorting by multiple keys in passes: first sort by secondary key, then by primary key — only stable sort preserves the secondary order inside ties.
| Algorithm | Stable? | Why / Why not |
| Bubble | Yes | only swaps strictly out-of-order adjacent pairs |
| Insertion | Yes | only shifts past *strictly greater* elements |
| Selection | No | the swap to position \i\ can leapfrog an equal element earlier |
The asymptotic win of merge/quicksort doesn't kick in for small \n\. Constants and overhead matter. Real numbers:
| Property | Bubble | Selection | Insertion | Merge | Quick |
| In-place (O(1) extra) | ✓ | ✓ | ✓ | ✗ (O(n)) | ✓ (O(log n) stack) |
| Stable | ✓ | ✗ | ✓ | ✓ | ✗ |
| Adaptive (faster on sorted) | ✓ (with flag) | ✗ | ✓ | ✗ | ✗ |
| Best case | O(n) | O(n²) | O(n) | O(n log n) | O(n log n) |
"Adaptive" means *the algorithm gets faster on already-mostly-sorted input*. Insertion sort is the gold standard.
sorted()\ → Timsort: scans for natural runs, extends short runs with binary insertion sort, then merges.Arrays.sort(Object[])\ → Timsort. \Arrays.sort(int[])\ → Dual-Pivot Quicksort with insertion sort under threshold.std::sort\ → Introsort: Quicksort, falls back to Heapsort if recursion is too deep, and Insertion sort for small partitions.sort\ → stable variant of Timsort (PDQ-mergesort).| Algorithm | Best | Avg | Worst | Space | Stable | Writes | Adaptive |
| Bubble | O(n) | O(n²) | O(n²) | O(1) | ✓ | high | ✓ (with flag) |
| Selection | O(n²) | O(n²) | O(n²) | O(1) | ✗ | low (n) | ✗ |
| Insertion | O(n) | O(n²) | O(n²) | O(1) | ✓ | medium | ✓ |
1. Implement bubble sort without the early-exit flag, then add it. Time both on \[1,2,3,…,1000]\ (already sorted) — note the difference.
2. Implement selection sort. Verify it makes at most n − 1 swaps by counting them on a random array of size 100.
3. Implement insertion sort. Run it on \[1,2,3,…,1000]\, then on \[1000,…,1]\. Compare the comparison counts — best vs worst case in action.
4. Write binary insertion sort: same algorithm but use binary search to find where to insert. Same O(n²) total work (still O(n) shifts) but fewer comparisons — useful when comparison is expensive (e.g., comparing long strings).