Part 3 · Lesson 16 · 22 min read
Selection, Bubble and Insertion Sort
Three simple sorts that show what sorting really does.
On this page
What sorting means
Sorting means putting items in order: numbers from smallest to largest, words in alphabet order, or people by age. It is one of the most useful things you can do to data. Once an array is sorted, equal values sit next to each other, and the smallest and largest values are at the two ends. You can also search a sorted array by halving it again and again (Lesson 28).
In JavaScript you will almost always sort with arr.sort((a, b) => a - b). (Lesson 18 explains why the comparison function (a, b) => a - b is needed.) So why learn to sort by hand? The three simple sorts in this lesson teach three core moves: find the smallest item, swap neighbours, and insert an item into a sorted part. These moves appear inside many interview problems. They also make the O(n log n) sorts in the next two lessons easier to understand. (O(n²) means the work grows with n × n. O(n log n) is much smaller, so it is much faster.)
Stability: keeping ties in order
When two items compare as equal, a stable sort keeps them in the order that they had before sorting. This matters when you sort records (objects with several fields) by one field:
const students = [ { name: "Asha", grade: "B" }, { name: "Ben", grade: "A" }, { name: "Chen", grade: "B" }, { name: "Dara", grade: "A" },];students.sort((a, b) => a.grade.localeCompare(b.grade));console.log(students.map((s) => s.name).join(", "));// Ben, Dara, Asha, Chen — A's first; within each grade, the original order is keptAsha came before Chen and both have a B, so a stable sort keeps Asha first. JavaScript's sort has been guaranteed stable since 2019 (ES2019). There is one more property to know. An in-place sort rearranges the array itself and uses only O(1) extra memory (a fixed small amount that does not grow with n).
Selection sort
Selection sort is a sorting algorithm that repeatedly selects the smallest remaining item and puts it in its place. Idea: find the smallest item and put it first. Then find the smallest of the rest and put it second. Repeat until the array is sorted. (To swap two items means to exchange their positions.)
function selectionSort(arr) { const n = arr.length; for (let i = 0; i < n - 1; i++) { let minIdx = i; for (let j = i + 1; j < n; j++) { // find the smallest in arr[i..] if (arr[j] < arr[minIdx]) minIdx = j; } [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]]; // put it at position i } return arr;}console.log(selectionSort([5, 3, 8, 1, 4])); // [ 1, 3, 4, 5, 8 ]| i | Smallest in arr[i..] | Swap | Array after |
|---|---|---|---|
| 0 | 1 (index 3) | 5 ↔ 1 | [1, 3, 8, 5, 4] |
| 1 | 3 (index 1) | none needed | [1, 3, 8, 5, 4] |
| 2 | 4 (index 4) | 8 ↔ 4 | [1, 3, 4, 5, 8] |
| 3 | 5 (index 3) | none needed | [1, 3, 4, 5, 8] |
After pass i, positions 0..i hold the i + 1 smallest values in their final places.
The inner loop always scans the whole unsorted part. That is (n − 1) + (n − 2) + … + 1 ≈ n²/2 comparisons, even if the array is already sorted. So selection sort is O(n²) in every case. Its one advantage is that it makes at most n − 1 swaps. It is not stable. A swap over a long distance can move an item past an equal item.
Bubble sort and the early exit
Bubble sort is a sorting algorithm that repeatedly compares neighbours and swaps them when they are in the wrong order. Idea: walk through the array, compare each pair of neighbours, and swap any pair that is in the wrong order. After one full pass (one walk through the array), the largest value has “bubbled” to the end, like an air bubble rising in water. Repeat on the rest.
function bubbleSort(arr) { const n = arr.length; for (let pass = 0; pass < n - 1; pass++) { let swapped = false; for (let j = 0; j < n - 1 - pass; j++) { // the last "pass" items are already in place if (arr[j] > arr[j + 1]) { [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; swapped = true; } } if (!swapped) break; // no swaps: already sorted, stop early } return arr;}console.log(bubbleSort([5, 3, 8, 1, 4])); // [ 1, 3, 4, 5, 8 ]console.log(bubbleSort([1, 2, 3, 4, 5])); // [ 1, 2, 3, 4, 5 ] one pass, then stop| Pass | Swaps made | Array after | Fixed at the end |
|---|---|---|---|
| 0 | 5↔3, 8↔1, 8↔4 | [3, 5, 1, 4, 8] | 8 |
| 1 | 5↔1, 5↔4 | [3, 1, 4, 5, 8] | 5, 8 |
| 2 | 3↔1 | [1, 3, 4, 5, 8] | 4, 5, 8 |
| 3 | none → stop early | [1, 3, 4, 5, 8] | all |
In the worst case it is O(n²), like selection sort. But the swapped flag gives it an early exit. If a whole pass makes no swaps, the array is already sorted, so we stop. On an array that is already sorted, that is just one pass, so it takes O(n). Bubble sort is stable, because it swaps only neighbours that are strictly out of order (equal neighbours stay where they are).
Insertion sort
Insertion sort is a sorting algorithm that builds a sorted part one item at a time, by inserting each new item into its correct place. Idea: this is how most people sort playing cards in their hand. Keep a sorted part on the left. Take the next item (called the key) and slide it left past every bigger item until it reaches its place.
Traced: insertion sort on [5, 2, 4, 1]
The first item alone counts as a sorted part of length 1.
▶const arr = [5, 2, 4, 1];2for (let i = 1; i < arr.length; i++) {3 const key = arr[i];4 let j = i - 1;5 while (j >= 0 && arr[j] > key) {6 arr[j + 1] = arr[j];7 j--;8 }9 arr[j + 1] = key;10}11console.log(arr);| arr | [5, 2, 4, 1] |
Why insertion sort loves nearly sorted data
The while loop stops as soon as it finds an item that is not bigger than the key. If the array is already sorted, it stops at once every time. That is n − 1 comparisons in total, so O(n). If every item is only a few places from where it belongs, each key moves only a few places, so the total is still close to O(n). Only a reversed array forces every key all the way to the front, and that costs O(n²).
Insertion sort is very fast on small or nearly sorted arrays, so real sorting functions use it inside. V8 is the JavaScript engine in Chrome and Node.js. Its sort uses an algorithm called TimSort. TimSort sorts small sections with insertion sort and then merges them. The merging step is Lesson 17.
The three side by side
| Selection | Bubble (early exit) | Insertion | |
|---|---|---|---|
| Best case (already sorted) | O(n²) | O(n) | O(n) |
| Worst case (reversed) | O(n²) | O(n²) | O(n²) |
| Extra space | O(1) | O(1) | O(1) |
| Stable? | No | Yes | Yes |
| Swaps / writes | At most n − 1 swaps | Many swaps | Many shifts |
| Best used for | When writes are expensive | Teaching only | Small or nearly sorted arrays |
In the worst case all three are O(n²). So none of them can sort 100,000 items in time (Lesson 12). For that you need the O(n log n) sorts: merge sort and quick sort.
Practice questions
These questions use the moves of the simple sorts. They do not ask you to sort a whole array. Say the time complexity of each answer.
Insert x into the sorted array arr so that it stays sorted.
Hint 1
Hint 2
Show all 2 solutions with explanations
Make the array one item longer. Then walk from the end and move every item that is bigger than x one place right. Put x in the gap.
function insertSorted(arr, x) { let j = arr.length - 1; arr.push(x); // make room (this slot is overwritten when needed) while (j >= 0 && arr[j] > x) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = x; return arr;}console.log(insertSorted([1, 3, 5, 8], 4)); // [ 1, 3, 4, 5, 8 ]console.log(insertSorted([2, 6], 9)); // [ 2, 6, 9 ]console.log(insertSorted([], 7)); // [ 7 ]O(n) in the worst case (x is smaller than everything). It is O(1) when x is the largest. It is in-place, which means it makes no second array.
Find the first index whose value is bigger than x. Then insert x there with splice (an array method that can add or remove items at any position).
function insertSorted(arr, x) { let i = 0; while (i < arr.length && arr[i] <= x) i++; arr.splice(i, 0, x); return arr;}console.log(insertSorted([1, 3, 5, 8], 4)); // [ 1, 3, 4, 5, 8 ]This is also O(n). The search is O(n), and splice shifts the later items (a loop that you cannot see, Lesson 12). Lesson 28 finds the position in O(log n), but the shifting still costs O(n).
Approach 1 is the inner loop of insertion sort. If you understand it, the full sort is easy to write from memory.
Students should stand in non-decreasing order of height (each student is as tall as or taller than the one before). Return how many positions do not match the sorted order.
Hint 1
Hint 2
Show all 2 solutions with explanations
Copy the array, sort the copy with a comparator (the function (a, b) => a - b), and count the differences.
function heightChecker(heights) { const expected = [...heights].sort((a, b) => a - b); let count = 0; for (let i = 0; i < heights.length; i++) { if (heights[i] !== expected[i]) count++; } return count;}console.log(heightChecker([1, 1, 4, 2, 1, 3])); // 3console.log(heightChecker([5, 1, 2, 3, 4])); // 5O(n log n) for the sort. Copying with [...heights] is important, because sort changes the array that it is called on.
Count each height. Then walk the heights from 1 upwards. This walk gives the sorted order, and we compare it with the original as we go.
function heightChecker(heights) { const count = new Array(101).fill(0); for (const h of heights) count[h]++; let mismatches = 0; let h = 1; for (const actual of heights) { while (count[h] === 0) h++; // next height in sorted order if (actual !== h) mismatches++; count[h]--; } return mismatches;}console.log(heightChecker([1, 1, 4, 2, 1, 3])); // 3console.log(heightChecker([1, 2, 3])); // 0O(n + 100) = O(n). This is counting sort, a sorting method that counts how many times each value appears and then writes the values out in order. When the values come from a small range, you can sort without comparing items at all.
Approach 1 is the normal answer. Mention counting sort when the range of values is small. It beats the O(n log n) limit of sorts that compare items. (LeetCode 1051.)
Return the k smallest values of nums in increasing order.
Hint 1
Hint 2
Show all 2 solutions with explanations
Run the outer loop of selection sort only k times. Then return the first k items.
function kSmallest(nums, k) { const a = [...nums]; for (let i = 0; i < k; i++) { let minIdx = i; for (let j = i + 1; j < a.length; j++) { if (a[j] < a[minIdx]) minIdx = j; } [a[i], a[minIdx]] = [a[minIdx], a[i]]; } return a.slice(0, k);}console.log(kSmallest([7, 2, 9, 4, 1], 2)); // [ 1, 2 ]console.log(kSmallest([3, 3, 1], 3)); // [ 1, 3, 3 ]O(n × k). This is good when k is tiny (say 3), and worse than sorting when k is large.
Sort a copy and take the first k.
function kSmallest(nums, k) { return [...nums].sort((a, b) => a - b).slice(0, k);}console.log(kSmallest([7, 2, 9, 4, 1], 2)); // [ 1, 2 ]O(n log n), whatever k is.
Compare n × k with n log n. For n = 106, log n is about 20, so selection passes win only when k < 20. Lesson 46 gives an O(n log k) answer with a heap (a structure that always gives you the smallest or largest item quickly).
Return how many swaps bubble sort makes to sort nums.
Hint 1
Hint 2
Show all 2 solutions with explanations
Run bubble sort on a copy and add 1 to a counter for every swap.
function bubbleSwaps(nums) { const a = [...nums]; let swaps = 0; for (let pass = 0; pass < a.length - 1; pass++) { for (let j = 0; j < a.length - 1 - pass; j++) { if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; swaps++; } } } return swaps;}console.log(bubbleSwaps([3, 1, 2])); // 2console.log(bubbleSwaps([5, 3, 8, 1, 4])); // 6O(n²) time.
Count every pair i < j with nums[i] > nums[j]. Each such pair needs exactly one swap of neighbours to fix, and each swap fixes exactly one pair.
function bubbleSwaps(nums) { let inversions = 0; for (let i = 0; i < nums.length; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] > nums[j]) inversions++; } } return inversions;}console.log(bubbleSwaps([3, 1, 2])); // 2console.log(bubbleSwaps([5, 3, 8, 1, 4])); // 6console.log(bubbleSwaps([1, 2, 3])); // 0| Bigger value first | Pairs |
|---|---|
| 5 | (5, 3), (5, 1), (5, 4) |
| 3 | (3, 1) |
| 8 | (8, 1), (8, 4) |
6 inversions = 6 swaps.
Both are O(n²), but Approach 2 explains why the answer is that number. Lesson 17 counts inversions in O(n log n) by changing merge sort slightly.
Write a stable sort that accepts a comparison function, and use it to sort students by grade.
Hint 1
cmp(a, b). The function returns a negative number, zero or a positive number, in the same way as the function you give to JavaScript's sort. (A negative number means a comes first, zero means they are equal, and a positive number means b comes first.)Hint 2
Show the solution with explanations
Replace arr[j] > key with cmp(arr[j], key) > 0.
function insertionSort(arr, cmp) { for (let i = 1; i < arr.length; i++) { const key = arr[i]; let j = i - 1; while (j >= 0 && cmp(arr[j], key) > 0) { // strictly greater: equal items never pass each other arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr;}const students = [ { name: "Asha", grade: "B" }, { name: "Ben", grade: "A" }, { name: "Chen", grade: "B" }, { name: "Dara", grade: "A" },];insertionSort(students, (a, b) => a.grade.localeCompare(b.grade));console.log(students.map((s) => s.name).join(", ")); // Ben, Dara, Asha, ChenIf the condition were >= 0, equal items would jump over each other and the sort would become unstable. Chen would end up before Asha. One character decides stability.
Real sorting functions work with a comparator in this way, and the next two lessons do the same.
Sort nums in ascending order. Constraints: up to 5 × 104 values, each between −5 × 104 and 5 × 104.
Hint 1
Hint 2
Show all 3 solutions with explanations
The algorithm from this lesson.
function sortArray(nums) { for (let i = 1; i < nums.length; i++) { const key = nums[i]; let j = i - 1; while (j >= 0 && nums[j] > key) { nums[j + 1] = nums[j]; j--; } nums[j + 1] = key; } return nums;}console.log(sortArray([5, 2, 3, 1])); // [ 1, 2, 3, 5 ]This is correct, but it is O(n²). For 5 × 104 values in reverse order, that is over a billion shifts. It fails LeetCode's time limit, which is the point of the question.
Count each value at index value + 50000, then write the values back into the array in order.
function sortArray(nums) { const OFFSET = 50000; const count = new Array(100001).fill(0); for (const x of nums) count[x + OFFSET]++; let i = 0; for (let v = 0; v < count.length; v++) { while (count[v]-- > 0) nums[i++] = v - OFFSET; } return nums;}console.log(sortArray([5, 2, 3, 1])); // [ 1, 2, 3, 5 ]console.log(sortArray([5, 1, 1, 2, 0, 0])); // [ 0, 0, 1, 1, 2, 5 ]console.log(sortArray([-3, 4, -50000])); // [ -50000, -3, 4 ]O(n + range) time and O(range) space. The offset (the fixed number we add) makes negative values fit as array indexes.
Use JavaScript's sort with a numeric comparator.
function sortArray(nums) { return nums.sort((a, b) => a - b);}console.log(sortArray([5, 1, 1, 2, 0, 0])); // [ 0, 0, 1, 1, 2, 5 ]O(n log n). In a real interview the interviewer would often forbid this. The next two lessons build merge sort and quick sort to replace it.
What to learn here: simple sorts are O(n²) and too slow for large inputs. Counting sort is a valid O(n) answer when the range is small. Otherwise you need an O(n log n) sort. (LeetCode 912.)
Make it stick
- Describe selection, bubble and insertion sort in one sentence each.
- Write insertion sort from memory and dry-run it on [3, 1, 2].
- Explain what makes a sort stable, with the students example.
- Say which of the three is O(n) on sorted input, and why selection sort is not.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Lesson 17 uses recursion from Lesson 14 to sort much faster. Split the array in half, sort each half, and merge the two sorted halves into one. This is merge sort, and it is O(n log n) in every case.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series