Sort Intervals by Start (Stable Selection Sort)
Given an array of intervals where each interval is [start, end], sort them by start value in ascending order. When two intervals share the same start, keep their original relative order (a stable sort). A plain selection sort swaps the minimum directly into place, which can jump it past an equal-valued interval and break that guarantee. Instead, shift every interval between the old and new positions over by one instead of swapping, so equal starts never change order. Return the sorted array.
Constraints
- 1 ≤ intervals.length ≤ 500
- 0 ≤ intervalsi[0] ≤ intervalsi[1] ≤ 104
Example
intervals = [[3, 4], [1, 2], [1, 5], [2, 3]][[1, 2], [1, 5], [2, 3], [3, 4]]Explanation Both intervals starting at 1 keep their original order — [1, 2] stays before [1, 5] because it appeared first in the input.
In plain terms
- Stable sort
- A sort where elements that compare equal keep the same relative order they had before sorting.
- Shift (instead of swap)
- Moving the found minimum into place by sliding every element in between forward one slot, rather than trading places directly with one other element.
Find the minimum start, then shift it into place — never swap
Scan all 4 intervals — the smallest start is 1, first seen at index 1 ([1, 2]).
What happens in this step
intervals = [[3,4], [1,2], [1,5], [2,3]], sortedEnd = 0, min start so far = 3 (index 0, [3,4]) idx 1: [1,2] start 1 < 3 → new min = [1,2] at index 1 idx 2: [1,5] start 1 < 1 → no (tie — index 1 found first, keeps priority) idx 3: [2,3] start 2 < 1 → no [1,2] wins with the strictly-earlier tie-break rule: the first index with the smallest start is kept.
Steps to visualize
- Scan the unsorted region for the interval with the smallest start value, preferring the earliest one found on ties.
- Remove that interval and shift every interval between its old spot and sortedEnd forward by one.
- Insert the minimum at index sortedEnd — every interval it passed keeps its original relative order.
- Move sortedEnd forward and repeat.
Walk through the code
Same walkthrough, now with the code. Press Next to move one step and watch which lines run.
Scan all 4 intervals — the smallest start is 1, first seen at index 1 ([1, 2]).
What happens in this step
intervals = [[3,4], [1,2], [1,5], [2,3]], sortedEnd = 0, min start so far = 3 (index 0, [3,4]) idx 1: [1,2] start 1 < 3 → new min = [1,2] at index 1 idx 2: [1,5] start 1 < 1 → no (tie — index 1 found first, keeps priority) idx 3: [2,3] start 2 < 1 → no [1,2] wins with the strictly-earlier tie-break rule: the first index with the smallest start is kept.
Solution
function sortIntervalsStable(intervals) {
const arr = intervals.map((interval) => interval.slice());
for (let sortedEnd = 0; sortedEnd < arr.length - 1; sortedEnd++) {
let minIndex = sortedEnd;
for (let scan = sortedEnd + 1; scan < arr.length; scan++) {
if (arr[scan][0] < arr[minIndex][0]) {
minIndex = scan;
}
}
if (minIndex !== sortedEnd) {
const minInterval = arr[minIndex];
for (let shift = minIndex; shift > sortedEnd; shift--) {
arr[shift] = arr[shift - 1];
}
arr[sortedEnd] = minInterval;
}
}
return arr;
}- Time
- O(n^2)
- Space
- O(n)
Test cases
| Input | Expected | Covers |
|---|---|---|
intervals = [[3, 4], [1, 2], [1, 5], [2, 3]] | [[1, 2], [1, 5], [2, 3], [3, 4]] | example from the docstring |
intervals = [[1, 2], [2, 3], [3, 4]] | [[1, 2], [2, 3], [3, 4]] | already sorted with no duplicate starts |
intervals = [[5, 6], [5, 1], [5, 9]] | [[5, 6], [5, 1], [5, 9]] | every interval shares the same start, order must be fully preserved |
intervals = [[3, 1], [2, 1], [1, 1]] | [[1, 1], [2, 1], [3, 1]] | distinct starts in fully reverse order |
intervals = [[4, 8]] | [[4, 8]] | smallest valid input, a single interval |
intervals = [[2, 5], [2, 1]] | [[2, 5], [2, 1]] | two intervals tied on start keep their original order |
intervals = [[4, 1], [1, 9], [4, 2], [1, 3]] | [[1, 9], [1, 3], [4, 1], [4, 2]] | two tied groups that must each preserve their own relative order |