Sorting is one of those problems that sounds boring until you add a constraint. Anyone can sort an array — you just compare and swap until everything lines up. But what if we cared about how many swaps it takes?
Here's an array of five numbers:
const arr = [2, 1, 4, 5, 3]Notice something: every value is between 1 and 5, and there are no duplicates.
This is a permutation — the numbers 1 through 5, jumbled up. Each value
appears exactly once, and each has exactly one correct position in the sorted result.
If you reached for something like bubble sort — swapping neighbors until nothing's
out of place — you'd churn through a lot of motion. Bubble sort on this array
takes 4 neighbor-swaps just to push 5 to the right end, and then you still
have work left. You're moving values one step at a time, like passing a note
across a classroom seat by seat.
But we're not limited to neighbors. You can swap any two positions. That's a huge advantage — you can teleport a value across the entire array in a single move. The question is: can you use that freedom wisely?
Your challenge: sort [2, 1, 4, 5, 3] in as few swaps as possible. There's a
par score — a minimum number of swaps that a perfect strategy would use. See how
close you can get.
Pick any two cells — swap them to sort the array.
Take a moment to think about the swaps you just made. Some of them felt useful — you grabbed a value and put it exactly where it needed to go, and maybe the displaced value landed in its correct spot too. Two birds, one swap. Other swaps felt like shuffling: you moved something out of the way, only to realize you'd need to move it again later.
A naive approach — say, selection sort — scans for the minimum, swaps it into
position 0, then finds the next minimum, swaps it into position 1, and so on.
Let's read it in three pieces, because each piece tells part of the wasted-motion story:
The outer sweep — one pass per final position:
for (let i = 0; i < arr.length; i++) {On pass i, we commit to the value that belongs at slot i and place it there.
After this pass finishes, slot i is done — never touched again. That sounds
efficient, and for slot i it is. The trouble is what happens to everyone else.
The inner scan — find the smallest unsorted value:
let minIdx = i for (let j = i + 1; j < arr.length; j++) { if (arr[j] < arr[minIdx]) minIdx = j }We walk the suffix i+1..n-1 to locate the minimum. No swaps happen here, just
looking. Cost: n - i - 1 comparisons per pass, which add up to O(n²) total —
but that's a comparison cost, not a swap cost. Keep reading.
The critical swap — place the minimum, displace the incumbent:
[arr[i], arr[minIdx]] = [arr[minIdx], arr[i]]}This is the only line that writes to the array. The minimum lands in slot i —
one value placed at home. But the value that was sitting in slot i gets flung
out to wherever the minimum came from. Did that displaced value land where it
belongs? Almost certainly not. It's just floating in the suffix, waiting for a
future pass to rescue it. One swap → one value placed, one value scattered.
Over the full run: at most n - 1 swaps, which sounds reasonable — but each swap
only guarantees progress for one side of the pair. The other side is a coin flip.
The key observation is this: some swaps move both values closer to home, and some swaps only help one value while scattering the other. The gap between your swap count and par is almost entirely explained by “wasted” displacement — swaps where only one value made progress.
What if there were a strategy where every single swap placed at least one value in its final, correct position? No wasted motion. No displacement. Every move counts.
That strategy exists — and it starts with a simple question: what if each value already knew exactly where it belongs?