Selection Sort Explained — How It Works, Code & Complexity
Sorting is one of the most fundamental operations in computing. Every time you see a leaderboard ranked by score, a product list sorted by price, or a contact list arranged alphabetically, a sorting algorithm made that happen. Understanding sorting algorithms is not just academic — it's the foundation that explains why some apps feel instant while others crawl, and it's one of the first things technical interviewers probe to see how you think about problems.
Selection sort solves the classic problem of arranging an unsorted list into order. It does it in the most intuitive way possible: scan the list, find the smallest item, move it to the front, then repeat on the shrinking remainder. It's not the fastest algorithm in the world — we'll be honest about that — but it's the perfect algorithm to learn first because its logic is transparent. There are no tricks, no recursion, no magic. Every step does exactly what it looks like.
By the end of this article you'll be able to trace selection sort by hand on any array, write a fully working implementation in Java from memory, explain its time and space complexity to an interviewer, and know exactly when it's a reasonable choice versus when you should reach for something better. Let's build this up from the ground floor.
How Selection Sort Actually Works — The Step-by-Step Logic
Let's use a concrete example before we touch any code. Say you have this array of five numbers:
[64, 25, 12, 22, 11]
Here's what selection sort does on every pass:
Pass 1: Scan the entire array. The smallest value is 11 (at index 4). Swap it with the element at index 0 (which is 64). Array becomes: [11, 25, 12, 22, 64]. The first position is now locked — it's sorted.
Pass 2: Scan from index 1 onwards. The smallest value is 12 (at index 2). Swap it with index 1 (which is 25). Array becomes: [11, 12, 25, 22, 64]. First two positions locked.
Pass 3: Scan from index 2 onwards. Smallest is 22 (at index 3). Swap with index 2. Array: [11, 12, 22, 25, 64].
Pass 4: Scan from index 3. Smallest is 25, already in position. No swap needed.
Notice the pattern: after every pass, the sorted region on the left grows by exactly one element. The algorithm never goes back and revisits the sorted section. That boundary between 'sorted' and 'unsorted' is the heart of selection sort.
public class SelectionSortTrace { public static void main(String[] args) { int[] numbers = {64, 25, 12, 22, 11}; System.out.println("=== Selection Sort — Trace Mode ==="); System.out.print("Original array: "); printArray(numbers); System.out.println(); int arrayLength = numbers.length; // Outer loop: moves the 'sorted boundary' forward one step each pass for (int sortedBoundary = 0; sortedBoundary < arrayLength - 1; sortedBoundary++) { // Assume the first element in the unsorted region is the minimum int minIndex = sortedBoundary; // Inner loop: scan the entire unsorted region to find the true minimum for (int scanner = sortedBoundary + 1; scanner < arrayLength; scanner++) { if (numbers[scanner] < numbers[minIndex]) { minIndex = scanner; // Found a new minimum — update the index } } // Only swap if the minimum isn't already in the correct position if (minIndex != sortedBoundary) { int temp = numbers[sortedBoundary]; // Save the value being displaced numbers[sortedBoundary] = numbers[minIndex]; // Move minimum to sorted boundary numbers[minIndex] = temp; // Put displaced value where minimum was } // Print the array after each pass so we can watch the sort happen System.out.print("After pass " + (sortedBoundary + 1) + ": "); printArray(numbers); System.out.println(" (positions 0-" + sortedBoundary + " sorted)"); } System.out.println(); System.out.print("Final sorted array: "); printArray(numbers); } // Helper method to print an array in a readable format static void printArray(int[] array) { System.out.print("["); for (int i = 0; i < array.length; i++) { System.out.print(array[i]); if (i < array.length - 1) System.out.print(", "); } System.out.print("]"); } }
Original array: [64, 25, 12, 22, 11]
After pass 1: [11, 25, 12, 22, 64] (positions 0-0 sorted)
After pass 2: [11, 12, 25, 22, 64] (positions 0-1 sorted)
After pass 3: [11, 12, 22, 25, 64] (positions 0-2 sorted)
After pass 4: [11, 12, 22, 25, 64] (positions 0-3 sorted)
Final sorted array: [11, 12, 22, 25, 64]
Time and Space Complexity — Why Selection Sort Is Slow (And When That's OK)
Every time you hear someone say an algorithm is 'fast' or 'slow', they're talking about complexity — how the algorithm's workload grows as the input grows. This is measured in Big O notation.
Time Complexity — O(n²)
For an array of n elements, the outer loop runs n-1 times. On the first pass the inner loop does n-1 comparisons, on the second pass n-2, on the third n-3, and so on. Add those up:
(n-1) + (n-2) + (n-3) + ... + 1 = n(n-1)/2
In Big O terms, we drop the constant and lower-order terms: that's O(n²). Double the input size, and the work roughly quadruples. That's why selection sort gets painful on large datasets.
Critically, selection sort always does O(n²) comparisons — even if the array is already sorted. It has no way to detect that early. This is different from insertion sort, which can exit early on a nearly-sorted list.
Space Complexity — O(1)
Selection sort sorts in-place. It only ever uses one extra variable (temp) for the swap, regardless of how large the array is. That's O(1) — constant extra space. This is actually selection sort's secret strength: it's memory-efficient in environments where RAM is precious.
public class ComplexityDemo { public static void main(String[] args) { // Demonstrates that comparison count matches n*(n-1)/2 exactly int[] testSizes = {5, 10, 20, 50}; System.out.println("=== Selection Sort Comparison Count vs n*(n-1)/2 ==="); System.out.printf("%-12s %-20s %-20s%n", "Array Size", "Actual Comparisons", "Formula n*(n-1)/2"); System.out.println("-".repeat(55)); for (int size : testSizes) { // Build a worst-case (reverse-sorted) array of this size int[] reverseArray = new int[size]; for (int i = 0; i < size; i++) { reverseArray[i] = size - i; // [5,4,3,2,1] for size 5 } long comparisonCount = countComparisons(reverseArray); long formulaResult = (long) size * (size - 1) / 2; System.out.printf("%-12d %-20d %-20d%n", size, comparisonCount, formulaResult); } System.out.println(); System.out.println("Key insight: comparisons match EXACTLY — selection sort"); System.out.println("always does n*(n-1)/2 comparisons, no matter the input order."); } // Runs selection sort and counts every comparison made static long countComparisons(int[] array) { int[] copy = array.clone(); // Work on a copy so we don't modify the original long comparisons = 0; int length = copy.length; for (int sortedBoundary = 0; sortedBoundary < length - 1; sortedBoundary++) { int minIndex = sortedBoundary; for (int scanner = sortedBoundary + 1; scanner < length; scanner++) { comparisons++; // Count every time we compare two elements if (copy[scanner] < copy[minIndex]) { minIndex = scanner; } } // Perform the swap int temp = copy[sortedBoundary]; copy[sortedBoundary] = copy[minIndex]; copy[minIndex] = temp; } return comparisons; } }
Array Size Actual Comparisons Formula n*(n-1)/2
-------------------------------------------------------
5 10 10
10 45 45
20 190 190
50 1225 1225
Key insight: comparisons match EXACTLY — selection sort
always does n*(n-1)/2 comparisons, no matter the input order.
Gotchas — The Mistakes Almost Every Beginner Makes
Writing selection sort from scratch in an interview is a classic test. Most candidates understand the concept but trip over two or three implementation details that silently corrupt the result. Here are the real ones.
Mistake 1 — Off-by-one in the outer loop boundary
Students often write sortedBoundary < arrayLength instead of sortedBoundary < arrayLength - 1. When there's only one element left in the unsorted region, it's already in the right place — there's nothing to compare it against. The extra iteration wastes a cycle and can cause an ArrayIndexOutOfBoundsException in the inner loop.
Mistake 2 — Unconditional swapping
A very common version skips the if (minIndex != sortedBoundary) check and always performs the swap. The result is still correct, but you're doing unnecessary work every time the minimum is already in position. More importantly, this breaks stability: you might swap an element with itself unnecessarily in an extended version of the algorithm.
Mistake 3 — Sorting the original instead of a copy
In real code, callers are often surprised when their original array is modified. Java arrays are passed by reference, not by value. If a method receives an array, sorts it using selection sort, and the caller still needs the original order, you've silently corrupted their data. Always clarify whether to sort in-place or return a new sorted array.
public class GotchasFixed { public static void main(String[] args) { int[] original = {29, 10, 14, 37, 13}; System.out.println("=== Demonstrating All Three Gotchas ==="); System.out.print("Original array before sorting: "); printArray(original); // GOTCHA 3 FIX: Sort a clone so the original is preserved int[] sorted = selectionSort(original.clone()); System.out.print("\nOriginal array after sorting: "); printArray(original); // Should be unchanged System.out.print("\nSorted copy: "); printArray(sorted); } static int[] selectionSort(int[] array) { int length = array.length; // GOTCHA 1 FIX: outer loop goes to length-1, not length // When one element remains in the unsorted region, it's already in place for (int sortedBoundary = 0; sortedBoundary < length - 1; sortedBoundary++) { int minIndex = sortedBoundary; for (int scanner = sortedBoundary + 1; scanner < length; scanner++) { if (array[scanner] < array[minIndex]) { minIndex = scanner; } } // GOTCHA 2 FIX: only swap when the minimum is NOT already in position // This avoids a pointless write operation and preserves relative order // in cases where the same value appears more than once if (minIndex != sortedBoundary) { int displaced = array[sortedBoundary]; array[sortedBoundary] = array[minIndex]; array[minIndex] = displaced; } } return array; } static void printArray(int[] array) { System.out.print("["); for (int i = 0; i < array.length; i++) { System.out.print(array[i]); if (i < array.length - 1) System.out.print(", "); } System.out.println("]"); } }
Original array before sorting: [29, 10, 14, 37, 13]
Original array after sorting: [29, 10, 14, 37, 13]
Sorted copy: [10, 13, 14, 29, 37]
| Feature / Aspect | Selection Sort | Bubble Sort | Insertion Sort |
|---|---|---|---|
| Time Complexity (Best) | O(n²) | O(n) | O(n) |
| Time Complexity (Worst) | O(n²) | O(n²) | O(n²) |
| Space Complexity | O(1) | O(1) | O(1) |
| Stable Sort? | No | Yes | Yes |
| Swaps per Pass | At most 1 | Up to n-1 | Up to n-1 |
| Best Use Case | Minimising writes | Teaching only | Nearly-sorted data |
| Detects Sorted Input? | No | Yes (with flag) | Yes |
| Beginner-Friendliness | Very High | Very High | High |
🎯 Key Takeaways
- Selection sort works by repeatedly finding the minimum element in the unsorted region and swapping it to the sorted boundary — never touching the sorted region again.
- Time complexity is always O(n²) regardless of input order — it has no early-exit optimisation, unlike insertion sort which is O(n) on a sorted array.
- Space complexity is O(1) because sorting happens in-place using only one temporary swap variable, making it memory-efficient for constrained environments.
- Selection sort is NOT stable — it can change the relative order of equal elements during long-distance swaps, which matters when sorting objects by one field while preserving order by another.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using
sortedBoundary < arrayLengthin the outer loop — The inner loop tries to accessarray[sortedBoundary + 1]whensortedBoundaryequalsarrayLength - 1, throwing an ArrayIndexOutOfBoundsException — Fix: change the condition tosortedBoundary < arrayLength - 1; when one element remains it's already in its correct position. - ✕Mistake 2: Skipping the
if (minIndex != sortedBoundary)guard — The sort still produces a correct result, but performs pointless swaps when the minimum is already in place, adding unnecessary writes — Fix: wrap the three-line swap block inif (minIndex != sortedBoundary)to skip it entirely when no movement is needed. - ✕Mistake 3: Sorting the passed-in array directly without cloning it — The caller's original array is silently mutated because Java arrays are reference types, not copies — Fix: either document clearly that the sort is in-place, or operate on
array.clone()inside the method and return the new sorted array, leaving the caller's data untouched.
Interview Questions on This Topic
- QWhat is the time complexity of selection sort in the best, average, and worst case — and why is the best case NOT O(n) like insertion sort?
- QIs selection sort a stable sorting algorithm? Can you give a concrete example of two equal elements whose relative order gets swapped?
- QSelection sort makes at most O(n) swaps even though it makes O(n²) comparisons. Name a real-world scenario where minimising the number of writes — not comparisons — is the priority, and explain why selection sort could be the right choice there.
Frequently Asked Questions
When should I actually use selection sort in real code?
Rarely — but it shines when the cost of writing to memory (or disk) is extremely high and comparisons are cheap. Selection sort makes at most n-1 swaps, whereas bubble sort can make O(n²) swaps. For small arrays (under ~20 elements) or embedded systems with tight memory constraints, it's also a pragmatic choice. For everything else, prefer Java's built-in Arrays.sort(), which uses a dual-pivot quicksort.
Does selection sort work on arrays with duplicate values?
Yes, it sorts correctly in the presence of duplicates. However, because it performs long-distance swaps, the relative order of duplicate elements may change — meaning it's not a stable sort. If you need duplicates to retain their original order (e.g., sorting people by age while keeping same-age people in their original name order), use a stable sort like insertion sort or merge sort instead.
Why do we only run the outer loop n-1 times instead of n times?
After n-1 passes, exactly n-1 elements have been placed in their correct sorted positions. That leaves exactly one element remaining in the 'unsorted' region — and one element on its own is, by definition, already sorted. Running an n-th pass would find nothing to compare the last element against, wasting a cycle and risking an out-of-bounds access in the inner loop.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.