Theory · Sorting
DSA Roadmap Open Visualizer Back to intro

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.

Counting Sort and Radix Sort break this bound — not by being cleverer with comparisons, but by refusing to compare at all. They exploit extra information about the values themselves (a known, bounded range, or a fixed number of digits) to place elements directly into buckets. That sidesteps the 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. 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 to O(n) — on data that's already nearly sorted, since each new element only has to shift a short distance.
  2. 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 of O(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's O(n log n) on average and in-place, but a poor pivot choice can degrade it to O(n²).
  3. 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. 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.

stepjarr[j]vs pivot (2)iarray after step
1055 > 2 → leave-1[5, 3, 8, 4, 2]
2133 > 2 → leave-1[5, 3, 8, 4, 2]
3288 > 2 → leave-1[5, 3, 8, 4, 2]
4344 > 2 → leave-1[5, 3, 8, 4, 2]
finalswap 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.

before partitioning 5 3 8 4 2 ↑ pivot after partitioning 2 3 8 4 5 pivot's final spot larger than pivot — recurse here next

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.

algorithmtimespacestable?notes
Bubble / Selection SortO(n²)O(1)variessimple, slow, mainly educational
Insertion SortO(n²)O(1)stableadaptive — near O(n) on nearly-sorted input
Merge SortO(n log n)O(n)stableguaranteed, no bad-input case
Quick SortO(n log n) avg / O(n²) worstO(log n)not stableworst case hits on sorted input with a naive pivot
Heap SortO(n log n)O(1)not stableguaranteed and in-place, but not adaptive
Counting / Radix SortO(n+k) / O(d(n+k))O(n+k)stableonly 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

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 →