The Story That Makes Selection Sort Obvious
Notice the rhythm: a long look across everyone unsorted, then one quick move. You never shuffle people around repeatedly — each chosen player is placed exactly once and never touched again.
That is Selection Sort: repeatedly select the smallest remaining element and swap it into its final position.
Selection Sort splits the array into a sorted part (on the left, already in final position) and an unsorted part (on the right). On each pass it scans the whole unsorted part to find the minimum, then swaps that minimum into the first unsorted slot. The boundary between sorted and unsorted moves one step right every pass.
Selection Sort trades comparisons for moves. It does a lot of looking (always about n²/2 comparisons, no matter the input) but only a tiny number of swaps — at most n−1. When writing/moving data is far more expensive than comparing it, that trade can be exactly what you want.
The Algorithm in Four Plain-English Steps
The pointer sweeps across the whole unsorted region. The smallest value it finds (here 1) turns amber as the chosen minimum, then locks into the front slot as sorted-green.
Hands-On: Watch Every Line of Code Execute
This is the heart of the tutorial. Press Play (or step through manually) and
watch the highlighted line of Python on the right move in lock-step with the
animated array on the left. The status bar narrates exactly what the active
line is doing right now. Notice how the inner if line lights up constantly (all
that comparing) while the swap line fires only once per pass. Use Shuffle for
a fresh array.
min_idx currently points at.
The dashed amber slot is i — the destination for this pass. The solid
amber bar is the smallest found so far; watch it hop whenever
min_idx = j fires. The blue bar is the current comparison. When
arr[i], arr[min_idx] = … lights up, the winner swaps into slot i and
turns sorted-green — permanently.
The Full Python Implementation
Here is the same algorithm as a clean, copy-ready function, plus a tiny driver that prints the array before and after sorting.
def selection_sort(arr):
# The sorted region grows from the left, one element per pass.
for i in range(len(arr) - 1):
min_idx = i # assume the first unsorted item is smallest
# Scan the unsorted part for a smaller element.
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j # found a new minimum
# One swap puts the minimum in its final place.
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
# --- driver ---
data = [5, 3, 8, 1, 9, 2, 7]
print("Before:", data)
selection_sort(data)
print("After: ", data)
A Verbose Version That Narrates Each Pass
Run this once to see the sorted region grow and to count how few swaps actually happen.
def selection_sort_verbose(arr):
swaps = 0
for i in range(len(arr) - 1):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
if min_idx != i:
arr[i], arr[min_idx] = arr[min_idx], arr[i]
swaps += 1
print(f"Pass {i}: min={arr[i]:>2} -> {arr}")
print(f"Total swaps: {swaps}")
return arr
selection_sort_verbose([5, 3, 8, 1, 9, 2, 7])
Two passes (3 and 5) needed no swap at all because the minimum was already in place. Selection Sort never performs more than n−1 swaps total — that is its defining strength.
Why Use Selection Sort? The Real Benefits
On hardware like flash memory and EEPROM, every write physically wears out the medium, so minimising writes extends device life. The same logic applies when each element is a huge record that is expensive to move but cheap to compare by key — Selection Sort moves each item at most once.
When to Use It — and When Not To
sort().Time & Space Complexity
| Case | When It Happens | Comparisons | Swaps | Time |
|---|---|---|---|---|
| Best | Already sorted | ~n²/2 (full scan anyway) | 0–(n−1) | O(n²) |
| Average | Random order | ~n²/2 | ≤ n−1 | O(n²) |
| Worst | Any order | ~n²/2 | ≤ n−1 | O(n²) |
| Space | Any input (in-place) | — | — | O(1) auxiliary |
The two nested loops always run their full course: the outer loop n−1 times, the inner loop scanning the entire shrinking unsorted region. There is no early exit, so best, average, and worst case are all O(n²). Selection Sort cannot get faster on friendly data — it simply is not adaptive.
Comparisons vs Swaps, Side by Side
For an array of n = 5, the scan work is fixed, but the swap work depends only on whether each minimum was already home.
| Pass i | Elements scanned | Comparisons |
|---|---|---|
| 0 | 4 | 4 |
| 1 | 3 | 3 |
| 2 | 2 | 2 |
| 3 | 1 | 1 |
| Total | — | 10 = n(n−1)/2 |
| Pass i | Min already at i? | Swaps |
|---|---|---|
| 0 | No | 1 |
| 1 | Yes | 0 |
| 2 | No | 1 |
| 3 | No | 1 |
| Total | — | ≤ 4 = n−1 |
How One Pass Flows
min_idx = i — treat the first unsorted element as the smallest until proven otherwise.for j in range(i + 1, len(arr)) — walk through every remaining element exactly once.if arr[j] < arr[min_idx]: min_idx = j — remember the index of any new, smaller value you meet.arr[i], arr[min_idx] = arr[min_idx], arr[i] — one swap locks the minimum into its final slot. The sorted region just grew by one.Selection Sort vs Its Cousins
| Property | Selection Sort | Bubble Sort | Insertion Sort | Merge Sort |
|---|---|---|---|---|
| Best case | O(n²) | O(n) | O(n) | O(n log n) |
| Average / worst | O(n²) | O(n²) | O(n²) | O(n log n) |
| Swaps / writes | O(n) — fewest | O(n²) | O(n²) shifts | O(n log n) |
| Extra space | O(1) | O(1) | O(1) | O(n) |
| Stable? | No (by default) | Yes | Yes | Yes |
| Adaptive? | No | Somewhat | Yes — strongly | No |
| Best for… | Costly writes, small data | Teaching only | Small / nearly-sorted | Large general data |
Reach for Selection Sort in the narrow but real case where moving data is the bottleneck — it guarantees the fewest writes of any common sort. For almost everything else, Insertion Sort beats it on small/nearly-sorted inputs (it is adaptive and stable), and an O(n log n) sort wins on anything large.
Golden Rules
min_idx), not the value. Swapping by index
lets you place the minimum with a single exchange after the scan finishes.
if min_idx != i: when you want to skip no-op swaps —
a tiny optimisation that avoids pointless writes (and keeps the swap count truly minimal).