Algocraft · Theory
Sorting, from swaps to strategy
Sorting is the first algorithm most people learn, and it's tempting to treat it as a solved problem you can forget about. It isn't — it's foundational, because the ideas inside sorting (divide & conquer, partitioning, heaps, exploiting known ranges) reappear everywhere else in DSA. The real skill here isn't memorizing eight implementations; it's knowing which one to reach for given your data's size, shape, and constraints. That's what this page builds toward.
01 · Foundation
Comparison-based vs. non-comparison-based
Bubble, Insertion, Selection, Merge, Quick, and Heap Sort all share one trait: the only thing they're allowed to do to figure out order is compare two elements and decide which comes first. That restriction has a hard consequence — no comparison-based sort can ever guarantee better than O(n log n) in the worst case, no matter how cleverly it's written.
Here's the intuitive version of why, without a formal proof. An array of n distinct elements has n! possible orderings, and exactly one of them is "sorted." Every comparison you make gives you at best one bit of information — it splits the remaining possibilities roughly in half. So the number of comparisons needed to narrow n! possibilities down to the one correct answer is at least log₂(n!), and log₂(n!) ≈ n log n. You simply cannot ask fewer yes/no questions than that and still be guaranteed a correct answer for every possible input.
n! argument entirely and gets you O(n + k) for Counting Sort or O(d·(n + k)) for Radix Sort, where k is the value range and d is the number of digits. The trade-off: they only work when you actually know something about the values in advance.
02 · Strategies
Four families you'll keep re-deriving
Nearly every sorting algorithm you'll ever meet is a variation on one of these four underlying strategies. Learn the family, and the specific algorithm becomes a detail.
-
1
Incremental / comparison-swapBubble, Insertion, and Selection Sort all repeatedly find or fix one element at a time relative to its neighbors. All three are
O(n²)and simple to write, but too slow for large inputs. The one exception worth remembering: Insertion Sort is genuinely fast — close toO(n)— on data that's already nearly sorted, since each new element only has to shift a short distance. -
2
Divide & conquerMerge Sort and Quick Sort both split the problem, solve the pieces, and combine. They put the work in opposite places: Merge Sort's split is trivial (just cut the array in half) but its merge step does all the work — that guarantees
O(n log n)every time, at the cost ofO(n)extra space. Quick Sort's partition step does all the work and the combine is trivial (the two sides are already on the correct sides of the pivot) — that'sO(n log n)on average and in-place, but a poor pivot choice can degrade it toO(n²). -
3
Heap-basedHeap Sort builds a max-heap out of the array, then repeatedly pulls the maximum off the top and moves it to the end. It guarantees
O(n log n)like Merge Sort, but does it in-place like Quick Sort — the catch is it isn't stable, and it tends to have worse real-world cache behavior than either. - 4 Distribution-basedCounting Sort and Radix Sort skip comparisons altogether and bucket elements by their actual value or by individual digits. This is the non-comparison family from the section above — extremely fast when the value range or digit count is small and known, and a poor fit otherwise.
03 · Worked example
Partitioning in Quick Sort, one pointer at a time
Quick Sort's entire personality comes from one step: partition. Given an array and a chosen pivot, partition rearranges the array so every element smaller than the pivot ends up to its left, everything larger ends up to its right, and the pivot itself lands exactly at the boundary — its final, correct, sorted position. Then Quick Sort recurses on the left and right pieces the same way. That recursive "partition, then repeat on each half" structure is the divide-and-conquer.
Let's trace it on [5, 3, 8, 4, 2] using the Lomuto scheme, with the pivot fixed as the last element — here, 2. We walk a pointer j across every element before the pivot. We also track i, which marks the boundary of "elements confirmed ≤ pivot so far" (it starts at -1, meaning nothing confirmed yet). Whenever arr[j] ≤ pivot, we advance i and swap arr[i] with arr[j] to pull that element into the confirmed region.
| step | j | arr[j] | vs pivot (2) | i | array after step |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 5 > 2 → leave | -1 | [5, 3, 8, 4, 2] |
| 2 | 1 | 3 | 3 > 2 → leave | -1 | [5, 3, 8, 4, 2] |
| 3 | 2 | 8 | 8 > 2 → leave | -1 | [5, 3, 8, 4, 2] |
| 4 | 3 | 4 | 4 > 2 → leave | -1 | [5, 3, 8, 4, 2] |
| final | — | swap arr[i+1] ↔ arr[high] | place pivot | -1 → 0 | [2, 3, 8, 4, 5] |
Every single element in this array happens to be bigger than the pivot, so i never advances during the scan — nothing gets pulled into the "confirmed small" region. The last step is the one that always runs regardless: swap arr[i + 1] with the pivot at arr[high]. Here that's index 0 swapping with index 4, which drops the pivot straight to the front. After this single partition call, 2 is permanently in its correct sorted position — it will never move again — and Quick Sort now recurses independently on the empty left side and the unsorted right side [3, 8, 4, 5], partitioning each the same way until everything is sorted.
Because 2 is the smallest value in this array, the "smaller than pivot" side ends up empty — the pivot lands at index 0, and every other element shifts right of the wavy divider, ready for the next recursive partition call.
04 · Complexity
Complexity cheat-sheet
The number that matters most in an interview is rarely the average case — it's whether you can name the worst case and explain when it happens.
| algorithm | time | space | stable? | notes |
|---|---|---|---|---|
| Bubble / Selection Sort | O(n²) | O(1) | varies | simple, slow, mainly educational |
| Insertion Sort | O(n²) | O(1) | stable | adaptive — near O(n) on nearly-sorted input |
| Merge Sort | O(n log n) | O(n) | stable | guaranteed, no bad-input case |
| Quick Sort | O(n log n) avg / O(n²) worst | O(log n) | not stable | worst case hits on sorted input with a naive pivot |
| Heap Sort | O(n log n) | O(1) | not stable | guaranteed and in-place, but not adaptive |
| Counting / Radix Sort | O(n+k) / O(d(n+k)) | O(n+k) | stable | only viable when the value range/digits are known and bounded |
Space column for Quick Sort is the recursion call stack, not extra array storage — it's in-place in the sense that it doesn't allocate another full array like Merge Sort does.
05 · Before you pick one
Common mistakes
- Picking Quick Sort's pivot naively as the first or last element — on an already-sorted (or reverse-sorted) array, this triggers the
O(n²)worst case every single time instead of the average-caseO(n log n). - Assuming a sort is stable when it isn't. If your problem depends on equal-key elements keeping their original relative order (say, sorting orders by status while preserving arrival order within a status), an unstable sort like Quick Sort or Heap Sort will silently scramble that.
- Reaching for Counting Sort on a huge or effectively unbounded value range — its
O(n+k)promise assumeskis small; ifkis much bigger thann, you've traded time for a memory blow-up. - Forgetting Merge Sort needs
O(n)auxiliary space for the merge step — it's a real constraint if you're sorting under a tight memory budget, even though its time guarantee is excellent. - Confusing average-case with worst-case guarantees when the choice actually matters — e.g. picking Quick Sort for a latency-sensitive system without acknowledging its worst case, instead of a guaranteed-
O(n log n)option like Merge or Heap Sort. - Treating all
O(n²)sorts as interchangeable — Insertion Sort's adaptiveness on nearly-sorted data makes it a genuinely reasonable choice in that specific situation, unlike Bubble or Selection Sort.
06 · Pick the right tool
Pick the right one for the job
Eight algorithms, four families, one decision each time: what do you actually know about the data, and what are you optimizing for? Each card links to the matching interactive visualizer.
Bubble Sort
repeatedly swap adjacent out-of-order pairs — O(n²)
The simplest possible sort: keep sweeping the array, swapping neighbors that are out of order, until a full sweep makes no swaps.
Try Bubble Sort →Insertion Sort
insert each element into the sorted prefix — O(n²), adaptive
Grows a sorted prefix one element at a time. Genuinely fast when the input is already close to sorted.
Try Insertion Sort →Selection Sort
find the min, swap it to the front — O(n²)
Repeatedly scans for the smallest remaining element and places it. Fewer swaps than Bubble Sort, same comparison count.
Try Selection Sort →Merge Sort
split, sort halves, merge — O(n log n) guaranteed, O(n) space
Split does nothing clever; the merge step does the real work. Stable and predictable no matter what the input looks like.
Try Merge Sort →Quick Sort
partition around a pivot, recurse — O(n log n) avg, in-place
Partition does the real work; combining the sides is free since they're already correctly separated. Fast in practice, but pivot choice matters.
Try Quick Sort →Heap Sort
build a max-heap, extract repeatedly — O(n log n), O(1) space
Guaranteed O(n log n) and fully in-place, trading away stability and adaptiveness for that consistency.
Try Heap Sort →Counting Sort
count occurrences per value — O(n+k), no comparisons
Skips comparisons entirely by counting how many times each value appears. Only works when the value range k is small and known.
Try Counting Sort →Radix Sort
counting-sort by digit, least-significant first — O(d(n+k))
Applies a stable digit-by-digit pass (typically Counting Sort) to sort numbers without ever comparing two of them directly.
Try Radix Sort →