Coding Interview Patterns Module 7 – Sorting as a tool
Sort first: what sorting buys you
Why sorting first turns duplicate, closest-pair and pair-sum problems into one scan, how to keep original indices, and which sorts are stable.
What you will learn
- Recognise problems that become simple once the input is sorted
- Prove that the two closest values are neighbours after sorting
- Sort indices or (value, index) pairs to keep the original positions
- Choose a stable sort in Python, JavaScript, Java, C++ and Go when ties must keep their order
Before you start
What you’ll learn · Before you start
What you will learn
- Recognise problems that become simple once the input is sorted
- Prove that the two closest values are neighbours after sorting
- Sort indices or (value, index) pairs to keep the original positions
- Choose a stable sort in Python, JavaScript, Java, C++ and Go when ties must keep their order
Before you start
On this page
Sorting costs O(n log n) once, and it buys an order you can reason about. Four things follow from that order, and between them they cover a large share of coding-round problems. Equal values end up side by side, so repeats and groups show up in one scan. Close values end up side by side too, so the two closest values are always neighbours. The order lets two pointers and binary search rule out many candidates with each comparison. And it gives greedy algorithms an order to decide in, such as smallest first or earliest deadline first. The price is the original order. When the answer needs positions, they have to travel through the sort, and when equal items must keep their order, the sort has to be stable. This lesson solves one problem completely, in five languages, and then deals with both costs.
What sorting buys
Each gain turns a question about every pair into a question about neighbours, or about one side of a comparison. The cue is usually in the wording of the problem:
| The problem asks | What sorting gives | Then |
|---|---|---|
| Is any value repeated? How many distinct values are there? | Equal values side by side | One scan, each value against the next |
| The two closest values, or the smallest difference | Close values side by side | The n − 1 neighbour gaps |
| Two values with a given sum, or three | Small values at one end, large ones at the other | Two pointers moving inwards |
| How many values lie between a and b? | Every value in order | Two binary searches |
| Merge overlapping bookings, or find the most at one time | Bookings, or their start and end events, in time order | One sweep from left to right |
| Give tasks to workers, or pair items cheaply | Smallest to largest | A greedy rule, and a proof that it is safe |
Two habits make the move pay. First, say what it costs. The sort is O(n log n): for n = 2 × 10⁵ that is about 3.5 million comparisons, well inside a typical time limit, as the lesson on reading constraints worked out. Second, say what it destroys. A sorted array has lost the order the input came in, which means the positions and the order among equal items. Both can be kept, as the later sections of this lesson show, but only if you plan for them before you sort.
A problem solved in full: the closest departures
The problem
A delivery network logs every van that leaves one of its depots, as the second of the day it left, from 0 to 86,399. The log arrives in the order the depots report, not in time order. Find the two departures that are closest in time: their positions in the log, and the gap in seconds. If several pairs share the smallest gap, any one of them is a right answer. A busy day has up to 50,000 departures.
Two logs serve as examples. The morning log, positions 0 to 7, is
[30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700], and its answer is positions 0 and 3, 75 seconds
apart. The noon log, [43200, 43260, 43200], has two vans that left in the same second, so its answer is positions
0 and 2, 0 seconds apart.
The brute force, and why it is too slow
Compare every pair. That is n(n − 1)/2 gaps, and it is right by construction, because no pair is skipped: it is the plan the faster one will be checked against. For 50,000 departures it computes 1,249,975,000 gaps. The lesson on constraints budgets CPython at roughly 10⁷ simple steps a second, which makes this about two minutes of work; compiled code, at roughly 10⁸ a second, would still take more than ten seconds.
Sort, then compare neighbours
Put the times in order, , and take any pair and with . Their gap is the sum of the gaps between the neighbours from position i to position j:
No term of the sum is negative, so the sum is at least its first term. Every pair therefore has a pair of neighbours that is at least as close, and the smallest of the n − 1 neighbour gaps is the smallest gap of all. That short argument is the proof, and it is worth saying in a round, because it is what makes skipping every other pair safe. The cost is the sort, O(n log n), and one scan, O(n).
The answer needs positions, and sorting the times on their own would lose them. So sort the positions instead, with each position’s time as its sort key. The sorted list then reads “position 7 first, then 1, then 4”, and every entry still says where it was in the log:
The morning log before and after sorting
Text description of the diagram
The diagram has two columns of eight boxes. Each box shows a departure's position in the log (#0 to #7) and its time in seconds after midnight.
- The left column, "As reported", is the log in the order the depots reported it: #0 30125, #1 29830, #2 31400, #3 30050, #4 29955, #5 31310, #6 30990 and #7 29700. The two departures that turn out to be closest, #0 and #3, are highlighted; two other departures sit between them in the log.
- The right column, "Sorted by time", holds the same eight departures from earliest to latest: #7 29700, #1 29830, #4 29955, #3 30050, #0 30125, #6 30990, #5 31310 and #2 31400.
- Beside the right column, a bracket joins each departure to the next one and is labelled with the gap between them in seconds: 130, 125, 95, 75, 865, 320 and 90. These seven gaps are the only ones the sorted plan computes.
- The smallest gap, 75 seconds, joins #3 (30050) and #0 (30125), which are now neighbours; their bracket is drawn thicker. The line under the columns says: closest, #0 and #3, 75 s apart, now neighbours.
A dry run on the morning log
Sorted by time, the positions are 7, 1, 4, 3, 0, 6, 5 and 2, as in the right-hand column above. The scan looks at each pair of neighbours once and keeps the smallest gap so far, with the two positions that have it:
| Pair | Gap (s) | Best so far |
|---|---|---|
| #7 and #1 | 130 | 130, #1 and #7 |
| #1 and #4 | 125 | 125, #1 and #4 |
| #4 and #3 | 95 | 95, #3 and #4 |
| #3 and #0 | 75 | 75, #0 and #3 |
| #0 and #6 | 865 | 75 |
| #6 and #5 | 320 | 75 |
| #5 and #2 | 90 | 75 |
Seven gaps instead of 28, and the answer is positions 0 and 3, 75 seconds apart. In the log those two were three places apart; after sorting they sit side by side.
The code in five languages
Each version holds both plans, the brute force and sort first, and prints both answers for both logs:
Python · closest/closest.py
"""The two van departures closest in time: every pair, or sort first and compare neighbours only."""
# Seconds after midnight of each departure, in the order the depots reported them: positions 0 to 7.
MORNING = [30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700]
# Two vans that left in the same second (positions 0 and 2).
NOON = [43200, 43260, 43200]
def closest_brute(times):
"""Every pair of positions, n(n - 1)/2 gaps. Returns (gap, first position, second position)."""
best = None
for i in range(len(times)):
for j in range(i + 1, len(times)):
gap = abs(times[i] - times[j])
if best is None or gap < best[0]:
best = (gap, i, j)
return best
def closest_sorted(times):
"""Sort the positions by time, then compare each departure with the next one only: n - 1 gaps."""
order = sorted(range(len(times)), key=lambda i: times[i])
best = None
for a, b in zip(order, order[1:]):
gap = times[b] - times[a]
if best is None or gap < best[0]:
best = (gap, min(a, b), max(a, b))
return best
def show(name, times):
print(f"{name}, {len(times)} departures")
for plan, find in (("every pair", closest_brute), ("sorted first", closest_sorted)):
gap, i, j = find(times)
print(f" {plan}: positions {i} and {j}, {gap} s apart")
if __name__ == "__main__":
show("Morning", MORNING)
show("Noon", NOON) Output
Morning, 8 departures every pair: positions 0 and 3, 75 s apart sorted first: positions 0 and 3, 75 s apart Noon, 3 departures every pair: positions 0 and 2, 0 s apart sorted first: positions 0 and 2, 0 s apart
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 closest.py
JavaScript · closest/closest.mjs
// The two van departures closest in time: every pair, or sort first and compare neighbours only.
// Seconds after midnight of each departure, in the order the depots reported them: positions 0 to 7.
const MORNING = [30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700];
// Two vans that left in the same second (positions 0 and 2).
const NOON = [43200, 43260, 43200];
// Every pair of positions, n(n - 1)/2 gaps. Returns [gap, first position, second position].
function closestBrute(times) {
let best = null;
for (let i = 0; i < times.length; i++) {
for (let j = i + 1; j < times.length; j++) {
const gap = Math.abs(times[i] - times[j]);
if (best === null || gap < best[0]) best = [gap, i, j];
}
}
return best;
}
// Sort the positions by time, then compare each departure with the next one only: n - 1 gaps.
// Without the comparator, sort() would compare the numbers as strings.
function closestSorted(times) {
const order = times.map((_, i) => i).sort((a, b) => times[a] - times[b]);
let best = null;
for (let k = 0; k + 1 < order.length; k++) {
const [a, b] = [order[k], order[k + 1]];
const gap = times[b] - times[a];
if (best === null || gap < best[0]) best = [gap, Math.min(a, b), Math.max(a, b)];
}
return best;
}
function show(name, times) {
console.log(`${name}, ${times.length} departures`);
for (const [plan, find] of [["every pair", closestBrute], ["sorted first", closestSorted]]) {
const [gap, i, j] = find(times);
console.log(` ${plan}: positions ${i} and ${j}, ${gap} s apart`);
}
}
show("Morning", MORNING);
show("Noon", NOON); Output
Morning, 8 departures every pair: positions 0 and 3, 75 s apart sorted first: positions 0 and 3, 75 s apart Noon, 3 departures every pair: positions 0 and 2, 0 s apart sorted first: positions 0 and 2, 0 s apart
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node closest.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Java · compiled/Closest.java
import java.util.Arrays;
import java.util.Comparator;
// The two van departures closest in time: every pair, or sort first and compare neighbours only.
public class Closest {
record Answer(int gap, int first, int second) {}
// Every pair of positions, n(n - 1)/2 gaps.
static Answer closestBrute(int[] times) {
Answer best = null;
for (int i = 0; i < times.length; i++) {
for (int j = i + 1; j < times.length; j++) {
int gap = Math.abs(times[i] - times[j]);
if (best == null || gap < best.gap()) best = new Answer(gap, i, j);
}
}
return best;
}
// Sort the positions by time, then compare each departure with the next one only: n - 1 gaps.
// An int[] cannot be sorted with a comparator, so the positions are boxed as Integer.
static Answer closestSorted(int[] times) {
Integer[] order = new Integer[times.length];
for (int i = 0; i < order.length; i++) order[i] = i;
Arrays.sort(order, Comparator.comparingInt(i -> times[i]));
Answer best = null;
for (int k = 0; k + 1 < order.length; k++) {
int a = order[k], b = order[k + 1];
int gap = times[b] - times[a];
if (best == null || gap < best.gap()) best = new Answer(gap, Math.min(a, b), Math.max(a, b));
}
return best;
}
static void show(String name, int[] times) {
System.out.println(name + ", " + times.length + " departures");
Answer[] answers = {closestBrute(times), closestSorted(times)};
String[] plans = {"every pair", "sorted first"};
for (int p = 0; p < 2; p++) {
Answer x = answers[p];
System.out.println(" " + plans[p] + ": positions " + x.first() + " and " + x.second() + ", " + x.gap() + " s apart");
}
}
public static void main(String[] args) {
// Seconds after midnight of each departure, in the order the depots reported them: positions 0 to 7.
show("Morning", new int[] {30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700});
// Two vans that left in the same second (positions 0 and 2).
show("Noon", new int[] {43200, 43260, 43200});
}
} C++ · compiled/closest.cpp
// The two van departures closest in time: every pair, or sort first and compare neighbours only.
#include <algorithm>
#include <cstdlib>
#include <numeric>
#include <print>
#include <string_view>
#include <vector>
struct Answer {
int gap = -1; // -1: nothing compared yet
int first = 0;
int second = 0;
};
// Every pair of positions, n(n - 1)/2 gaps.
Answer closest_brute(const std::vector<int>& times) {
Answer best;
for (int i = 0; i < std::ssize(times); ++i) {
for (int j = i + 1; j < std::ssize(times); ++j) {
int gap = std::abs(times[i] - times[j]);
if (best.gap < 0 || gap < best.gap) best = {gap, i, j};
}
}
return best;
}
// Sort the positions by time, then compare each departure with the next one only: n - 1 gaps.
Answer closest_sorted(const std::vector<int>& times) {
std::vector<int> order(times.size());
std::iota(order.begin(), order.end(), 0); // 0, 1, 2, ...
std::sort(order.begin(), order.end(), [&](int a, int b) { return times[a] < times[b]; });
Answer best;
for (std::size_t k = 0; k + 1 < order.size(); ++k) {
int a = order[k], b = order[k + 1];
int gap = times[b] - times[a];
if (best.gap < 0 || gap < best.gap) best = {gap, std::min(a, b), std::max(a, b)};
}
return best;
}
void show(std::string_view name, const std::vector<int>& times) {
std::println("{}, {} departures", name, times.size());
for (auto [plan, answer] : {std::pair{"every pair", closest_brute(times)}, std::pair{"sorted first", closest_sorted(times)}}) {
std::println(" {}: positions {} and {}, {} s apart", plan, answer.first, answer.second, answer.gap);
}
}
int main() {
// Seconds after midnight of each departure, in the order the depots reported them: positions 0 to 7.
show("Morning", {30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700});
// Two vans that left in the same second (positions 0 and 2).
show("Noon", {43200, 43260, 43200});
} Go · compiled/closest.go
// The two van departures closest in time: every pair, or sort first and compare neighbours only.
package main
import (
"cmp"
"fmt"
"slices"
)
type answer struct{ gap, first, second int }
// closestBrute compares every pair of positions: n(n - 1)/2 gaps.
func closestBrute(times []int) answer {
best := answer{gap: -1}
for i := range times {
for j := i + 1; j < len(times); j++ {
gap := max(times[i]-times[j], times[j]-times[i])
if best.gap < 0 || gap < best.gap {
best = answer{gap, i, j}
}
}
}
return best
}
// closestSorted sorts the positions by time, then compares each departure with the next one only: n - 1 gaps.
func closestSorted(times []int) answer {
order := make([]int, len(times))
for i := range order {
order[i] = i
}
slices.SortFunc(order, func(a, b int) int { return cmp.Compare(times[a], times[b]) })
best := answer{gap: -1}
for k := 0; k+1 < len(order); k++ {
a, b := order[k], order[k+1]
if gap := times[b] - times[a]; best.gap < 0 || gap < best.gap {
best = answer{gap, min(a, b), max(a, b)}
}
}
return best
}
func show(name string, times []int) {
fmt.Printf("%s, %d departures\n", name, len(times))
for _, plan := range []struct {
name string
answer answer
}{{"every pair", closestBrute(times)}, {"sorted first", closestSorted(times)}} {
fmt.Printf(" %s: positions %d and %d, %d s apart\n", plan.name, plan.answer.first, plan.answer.second, plan.answer.gap)
}
}
func main() {
// Seconds after midnight of each departure, in the order the depots reported them: positions 0 to 7.
show("Morning", []int{30125, 29830, 31400, 30050, 29955, 31310, 30990, 29700})
// Two vans that left in the same second (positions 0 and 2).
show("Noon", []int{43200, 43260, 43200})
} Build and run all three · compiled/closest_session.py
from session import run, versions
# Closest.java runs as a single source file, closest.cpp is built with the sanitizers on, and go run builds closest.go.
versions("java -version", "clang++ --version", "go version")
run("""
java Closest.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined closest.cpp -o closest && ./closest
go run closest.go
""") Output
# openjdk version "25.0.4.1" LTS # Apple clang version 21.0.0 (clang-2100.3.34.2) # go version go1.27.1 darwin/arm64 $ java Closest.java Morning, 8 departures every pair: positions 0 and 3, 75 s apart sorted first: positions 0 and 3, 75 s apart Noon, 3 departures every pair: positions 0 and 2, 0 s apart sorted first: positions 0 and 2, 0 s apart $ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined closest.cpp -o closest && ./closest Morning, 8 departures every pair: positions 0 and 3, 75 s apart sorted first: positions 0 and 3, 75 s apart Noon, 3 departures every pair: positions 0 and 2, 0 s apart sorted first: positions 0 and 2, 0 s apart $ go run closest.go Morning, 8 departures every pair: positions 0 and 3, 75 s apart sorted first: positions 0 and 3, 75 s apart Noon, 3 departures every pair: positions 0 and 2, 0 s apart sorted first: positions 0 and 2, 0 s apart
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 closest_session.py
All five print the same six lines. The differences are in how each language sorts positions by their times:
- Python sorts
range(len(times))withkey=lambda i: times[i], andsorted()leaves the log as it was. - JavaScript needs the comparator
(a, b) => times[a] - times[b]. Without one,sort()converts the elements to strings and compares those, so 10 would come before 9. - Java has no
Arrays.sortthat takes anint[]and a comparator, so the positions go into anInteger[]and are sorted withComparator.comparingInt. - C++ fills a vector with the positions using
std::iota, then sorts it with a lambda that compares their times. - Go sorts a slice of positions with
slices.SortFunc, comparing their times withcmp.Compare.
Check it against the brute force
The brute force is slow, but it is right, which makes it the referee. This program runs the sorted plan on 1,000 random logs of 2 to 9 departures, with small times so that repeats are common, and puts two plausible slips through the same check:
"""1,000 random logs: the sorted plan against the brute force, and two plausible slips the same check catches."""
import random
from closest import closest_brute, closest_sorted
def slip_sorted_places(times):
"""Slip 1: compares neighbours of the sorted times, but reports their places in the sorted list."""
ordered = sorted(times)
best = None
for k in range(len(ordered) - 1):
gap = ordered[k + 1] - ordered[k]
if best is None or gap < best[0]:
best = (gap, k, k + 1)
return best
def slip_index_lookup(times):
"""Slip 2: sorts the times, then finds each one's position with list.index(), which returns the first match."""
ordered = sorted(times)
best = None
for k in range(len(ordered) - 1):
gap = ordered[k + 1] - ordered[k]
if best is None or gap < best[0]:
i, j = times.index(ordered[k]), times.index(ordered[k + 1])
best = (gap, min(i, j), max(i, j))
return best
def is_right(times, answer):
"""Right means: two different positions, really that far apart, and no pair of the log is closer."""
gap, i, j = answer
return i != j and abs(times[i] - times[j]) == gap and gap == closest_brute(times)[0]
rng = random.Random(3)
logs = [[rng.randrange(40) for _ in range(rng.randint(2, 9))] for _ in range(1_000)]
logs.sort(key=len) # shortest logs first, so the first failure found is a small one
for plan in (closest_sorted, slip_sorted_places, slip_index_lookup):
wrong = next((log for log in logs if not is_right(log, plan(log))), None)
verdict = "right on all 1,000 logs" if wrong is None else f"wrong on {wrong}: it says {plan(wrong)}"
print(f"{plan.__name__}: {verdict}") Output
closest_sorted: right on all 1,000 logs slip_sorted_places: wrong on [13, 36, 17]: it says (4, 0, 1) slip_index_lookup: wrong on [25, 25]: it says (0, 0, 0)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The sorted plan is right on every log. The first slip compares the right neighbours but reports their places in the
sorted list instead of their positions in the log: on [13, 36, 17] it names positions 0 and 1, which are 23
seconds apart, not 4. The second sorts the times and then asks index() where each one came from, and on
[25, 25] it names position 0 twice, because index() always returns the first match. The check accepts an answer
when it names two different positions, their times really differ by the reported gap, and no pair of the log is
closer, so any of several equally close pairs passes. The logs are sorted by length first, so the first failure
found is a short one that you can trace by hand. The
lesson on stress testers turns this kind of check into a harness
you can reuse.
How much work sorting saves
Counting comparisons gives numbers that are the same on every computer. Python’s sort compares items only with
<, as its documentation states, so a class that defines __lt__ and counts its calls sees every comparison:
"""Comparisons made by every pair and by sort first, on random departure logs (a fixed seed: the same counts each run)."""
import random
class Departure:
"""A departure time that counts how often the sort compares two of them."""
comparisons = 0
def __init__(self, seconds):
self.seconds = seconds
def __lt__(self, other):
Departure.comparisons += 1
return self.seconds < other.seconds
rng = random.Random(8)
print(f"{'n':>6} {'every pair':>13} {'sort':>7} {'scan':>6} {'fewer':>6}")
for n in (1_000, 10_000, 50_000):
log = [Departure(rng.randrange(86_400)) for _ in range(n)]
Departure.comparisons = 0
log.sort()
pairs, sort, scan = n * (n - 1) // 2, Departure.comparisons, n - 1
print(f"{n:>6,} {pairs:>13,} {sort:>7,} {scan:>6,} {pairs // (sort + scan):>5,}x") Output
n every pair sort scan fewer 1,000 499,500 8,641 999 51x 10,000 49,995,000 120,216 9,999 383x 50,000 1,249,975,000 715,850 49,999 1,632x
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 counts.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
At 50,000 departures the sort made 715,850 comparisons and the scan 49,999 subtractions, against 1,249,975,000 gaps for the brute force: about 1,600 times fewer. The difference widens as n grows, because the brute force grows with n² and the sort only with about n log₂ n. Side by side, with the sorted list of positions as the sorted plan’s extra space:
| Operation | Time | Extra space |
|---|---|---|
| Every pair | O(n²) | O(1) |
| Sort, then scan | O(n log n) | O(n) |
Keeping the original positions
Positions travel through a sort in two ways, and there is one common way to lose them:
"""Three ways to sort departure times and say where each one was in the log; the last one is a slip."""
times = [300, 120, 300, 45] # seconds; positions 0 to 3; positions 0 and 2 share a time
# 1. Sort (time, position) pairs. Tuples compare item by item, so equal times come out in position order.
pairs = sorted((t, i) for i, t in enumerate(times))
print("pairs: ", pairs)
# 2. Sort the positions, using each position's time as the key. The sort is stable: equal times keep log order.
order = sorted(range(len(times)), key=lambda i: times[i])
print("positions: ", order)
# The slip: sort the times, then look each one up again. index() returns the first match, every time.
looked_up = [times.index(t) for t in sorted(times)]
print("index(): ", looked_up) Output
pairs: [(45, 3), (120, 1), (300, 0), (300, 2)] positions: [3, 1, 0, 2] index(): [3, 1, 0, 0]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 positions.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- Sort (value, position) pairs. Tuples compare item by item, so pairs sort by value and then by position. In
Python and C++ (
std::pair) that needs no comparator at all. - Sort the positions by key. This is what the five versions above do. Each position is sorted by its own value, and with a stable sort, equal values keep their order from the log, so ties come out in log order too.
- Never sort the values and look them up again.
index()finds the first match, so both 300s report position 0 and position 2 is lost. Each lookup is also a scan of the list, which makes the whole thing O(n²).
Either correct way costs n extra entries. That is the price of the answer, and it is worth saying when memory is part of the question.
Stable sorts, language by language
A sort is stable when items with equal keys come out in the order they went in. It matters in three common situations: sorting in two steps (by the second key, then stably by the first), sorting positions by their values (ties keep log order), and any output where the order of ties is visible, to a test or to a user. Each language documents which of its sorts promise it:
- Python:
sorted()andlist.sort()are stable, and the built-in sorts have no unstable variant. - JavaScript:
Array.prototype.sort()andtoSorted()are stable; the specification requires it. - Java:
List.sort,Collections.sortandArrays.sorton object arrays are stable.Arrays.sorton a primitive array uses Dual-Pivot Quicksort and promises no stability, which cannot matter for integers: equal integers are identical. - C++:
std::stable_sortandstd::ranges::stable_sortare stable.std::sortandstd::ranges::sortare not guaranteed to be. - Go:
sort.SliceStable,sort.Stableandslices.SortStableFuncare stable.sort.Slice,sort.Sortandslices.SortFuncare not guaranteed to be.
Here is what the difference looks like in practice. A class register of 24 students is in alphabetical order, each with a grade from A to C, and every version sorts it by grade only. A stable sort must leave each grade in alphabetical order, which also means it sorts by grade and then by name in one call. Each program counts how many students end up somewhere other than where a stable sort puts them, and prints the students with grade A:
Python · stability/stability.py
"""A class register in alphabetical order, sorted by grade: does each grade keep the register's order?"""
# (name, grade) in alphabetical order of name, the order of the class register.
REGISTER = [
("Aarav", "B"), ("Bea", "A"), ("Chen", "A"), ("Divya", "B"), ("Elif", "B"), ("Farah", "A"),
("Gopal", "A"), ("Hana", "C"), ("Ishaan", "C"), ("Jonas", "C"), ("Kavya", "B"), ("Leo", "C"),
("Meera", "C"), ("Noor", "B"), ("Omar", "C"), ("Priya", "C"), ("Quinn", "A"), ("Ravi", "B"),
("Sana", "B"), ("Tomas", "A"), ("Uma", "B"), ("Vikram", "C"), ("Wen", "A"), ("Zoya", "A"),
]
# What a stable sort by grade must give: grade A in register order, then grade B, then grade C.
STABLE = [s for grade in "ABC" for s in REGISTER if s[1] == grade]
def report(sort_name, students):
moved = sum(1 for got, want in zip(students, STABLE) if got != want)
grade_a = " ".join(name for name, grade in students if grade == "A")
print(f"{sort_name} ({moved} of {len(students)} moved)")
print(f" grade A: {grade_a}")
report("sorted()", sorted(REGISTER, key=lambda s: s[1]))
in_place = list(REGISTER)
in_place.sort(key=lambda s: s[1])
report("list.sort()", in_place) Output
sorted() (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya list.sort() (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stability.py
JavaScript · stability/stability.mjs
// A class register in alphabetical order, sorted by grade: does each grade keep the register's order?
// [name, grade] in alphabetical order of name, the order of the class register.
const REGISTER = [
["Aarav", "B"], ["Bea", "A"], ["Chen", "A"], ["Divya", "B"], ["Elif", "B"], ["Farah", "A"],
["Gopal", "A"], ["Hana", "C"], ["Ishaan", "C"], ["Jonas", "C"], ["Kavya", "B"], ["Leo", "C"],
["Meera", "C"], ["Noor", "B"], ["Omar", "C"], ["Priya", "C"], ["Quinn", "A"], ["Ravi", "B"],
["Sana", "B"], ["Tomas", "A"], ["Uma", "B"], ["Vikram", "C"], ["Wen", "A"], ["Zoya", "A"],
];
// What a stable sort by grade must give: grade A in register order, then grade B, then grade C.
const STABLE = ["A", "B", "C"].flatMap((grade) => REGISTER.filter((s) => s[1] === grade));
function report(sortName, students) {
const moved = students.filter((s, i) => s !== STABLE[i]).length;
const gradeA = students.filter((s) => s[1] === "A").map((s) => s[0]).join(" ");
console.log(`${sortName} (${moved} of ${students.length} moved)`);
console.log(` grade A: ${gradeA}`);
}
const byGrade = (x, y) => (x[1] === y[1] ? 0 : x[1] < y[1] ? -1 : 1);
report("Array.prototype.sort", [...REGISTER].sort(byGrade));
report("Array.prototype.toSorted", REGISTER.toSorted(byGrade)); Output
Array.prototype.sort (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya Array.prototype.toSorted (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node stability.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Java · compiled/Stability.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.StringJoiner;
// A class register in alphabetical order, sorted by grade: does each grade keep the register's order?
public class Stability {
record Student(String name, char grade) {}
// In alphabetical order of name, the order of the class register.
static final List<Student> REGISTER = List.of(
new Student("Aarav", 'B'), new Student("Bea", 'A'), new Student("Chen", 'A'), new Student("Divya", 'B'),
new Student("Elif", 'B'), new Student("Farah", 'A'), new Student("Gopal", 'A'), new Student("Hana", 'C'),
new Student("Ishaan", 'C'), new Student("Jonas", 'C'), new Student("Kavya", 'B'), new Student("Leo", 'C'),
new Student("Meera", 'C'), new Student("Noor", 'B'), new Student("Omar", 'C'), new Student("Priya", 'C'),
new Student("Quinn", 'A'), new Student("Ravi", 'B'), new Student("Sana", 'B'), new Student("Tomas", 'A'),
new Student("Uma", 'B'), new Student("Vikram", 'C'), new Student("Wen", 'A'), new Student("Zoya", 'A'));
static void report(String sortName, List<Student> students) {
List<Student> stable = new ArrayList<>(); // what a stable sort by grade must give
for (char grade : "ABC".toCharArray()) {
for (Student s : REGISTER) if (s.grade() == grade) stable.add(s);
}
int moved = 0;
StringJoiner gradeA = new StringJoiner(" ");
for (int i = 0; i < students.size(); i++) {
if (!students.get(i).equals(stable.get(i))) moved++;
if (students.get(i).grade() == 'A') gradeA.add(students.get(i).name());
}
System.out.println(sortName + " (" + moved + " of " + students.size() + " moved)");
System.out.println(" grade A: " + gradeA);
}
public static void main(String[] args) {
Comparator<Student> byGrade = Comparator.comparingInt(Student::grade);
List<Student> list = new ArrayList<>(REGISTER);
list.sort(byGrade);
report("List.sort", list);
Student[] array = REGISTER.toArray(new Student[0]);
Arrays.sort(array, byGrade);
report("Arrays.sort on objects", Arrays.asList(array));
}
} C++ · compiled/stability.cpp
// A class register in alphabetical order, sorted by grade: does each grade keep the register's order?
#include <algorithm>
#include <print>
#include <string>
#include <string_view>
#include <vector>
struct Student {
std::string name;
char grade;
bool operator==(const Student&) const = default;
};
// In alphabetical order of name, the order of the class register.
const std::vector<Student> kRegister = {
{"Aarav", 'B'}, {"Bea", 'A'}, {"Chen", 'A'}, {"Divya", 'B'}, {"Elif", 'B'}, {"Farah", 'A'},
{"Gopal", 'A'}, {"Hana", 'C'}, {"Ishaan", 'C'}, {"Jonas", 'C'}, {"Kavya", 'B'}, {"Leo", 'C'},
{"Meera", 'C'}, {"Noor", 'B'}, {"Omar", 'C'}, {"Priya", 'C'}, {"Quinn", 'A'}, {"Ravi", 'B'},
{"Sana", 'B'}, {"Tomas", 'A'}, {"Uma", 'B'}, {"Vikram", 'C'}, {"Wen", 'A'}, {"Zoya", 'A'},
};
void report(std::string_view sort_name, const std::vector<Student>& students) {
std::vector<Student> stable; // what a stable sort by grade must give
for (char grade : {'A', 'B', 'C'}) {
for (const Student& s : kRegister) {
if (s.grade == grade) stable.push_back(s);
}
}
int moved = 0;
std::string grade_a;
for (std::size_t i = 0; i < students.size(); ++i) {
if (students[i] != stable[i]) ++moved;
if (students[i].grade == 'A') grade_a += (grade_a.empty() ? "" : " ") + students[i].name;
}
std::println("{} ({} of {} moved)", sort_name, moved, students.size());
std::println(" grade A: {}", grade_a);
}
int main() {
auto by_grade = [](const Student& x, const Student& y) { return x.grade < y.grade; };
auto a = kRegister;
std::sort(a.begin(), a.end(), by_grade);
report("std::sort", a);
auto b = kRegister;
std::stable_sort(b.begin(), b.end(), by_grade);
report("std::stable_sort", b);
} Go · compiled/stability.go
// A class register in alphabetical order, sorted by grade: does each grade keep the register's order?
package main
import (
"cmp"
"fmt"
"slices"
"sort"
"strings"
)
type student struct {
name string
grade byte
}
// In alphabetical order of name, the order of the class register.
var register = []student{
{"Aarav", 'B'}, {"Bea", 'A'}, {"Chen", 'A'}, {"Divya", 'B'}, {"Elif", 'B'}, {"Farah", 'A'},
{"Gopal", 'A'}, {"Hana", 'C'}, {"Ishaan", 'C'}, {"Jonas", 'C'}, {"Kavya", 'B'}, {"Leo", 'C'},
{"Meera", 'C'}, {"Noor", 'B'}, {"Omar", 'C'}, {"Priya", 'C'}, {"Quinn", 'A'}, {"Ravi", 'B'},
{"Sana", 'B'}, {"Tomas", 'A'}, {"Uma", 'B'}, {"Vikram", 'C'}, {"Wen", 'A'}, {"Zoya", 'A'},
}
func report(sortName string, students []student) {
var stable []student // what a stable sort by grade must give
for _, grade := range []byte("ABC") {
for _, s := range register {
if s.grade == grade {
stable = append(stable, s)
}
}
}
moved := 0
var gradeA []string
for i, s := range students {
if s != stable[i] {
moved++
}
if s.grade == 'A' {
gradeA = append(gradeA, s.name)
}
}
fmt.Printf("%s (%d of %d moved)\n", sortName, moved, len(students))
fmt.Printf(" grade A: %s\n", strings.Join(gradeA, " "))
}
func main() {
a := slices.Clone(register)
sort.Slice(a, func(i, j int) bool { return a[i].grade < a[j].grade })
report("sort.Slice", a)
b := slices.Clone(register)
sort.SliceStable(b, func(i, j int) bool { return b[i].grade < b[j].grade })
report("sort.SliceStable", b)
byGrade := func(x, y student) int { return cmp.Compare(x.grade, y.grade) }
c := slices.Clone(register)
slices.SortFunc(c, byGrade)
report("slices.SortFunc", c)
d := slices.Clone(register)
slices.SortStableFunc(d, byGrade)
report("slices.SortStableFunc", d)
} Build and run all three · compiled/stability_session.py
from session import run, versions
# The same register sorted by grade with each language's sorts: Java, then C++, then Go.
versions("java -version", "clang++ --version", "go version")
run("""
java Stability.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined stability.cpp -o stability && ./stability
go run stability.go
""") Output
# openjdk version "25.0.4.1" LTS # Apple clang version 21.0.0 (clang-2100.3.34.2) # go version go1.27.1 darwin/arm64 $ java Stability.java List.sort (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya Arrays.sort on objects (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya $ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined stability.cpp -o stability && ./stability std::sort (23 of 24 moved) grade A: Zoya Bea Chen Wen Tomas Farah Gopal Quinn std::stable_sort (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya $ go run stability.go sort.Slice (23 of 24 moved) grade A: Quinn Bea Chen Zoya Wen Farah Gopal Tomas sort.SliceStable (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya slices.SortFunc (23 of 24 moved) grade A: Quinn Bea Chen Zoya Wen Farah Gopal Tomas slices.SortStableFunc (0 of 24 moved) grade A: Bea Chen Farah Gopal Quinn Tomas Wen Zoya
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stability_session.py
Every stable sort printed grade A as Bea, Chen, Farah, Gopal, Quinn, Tomas, Wen and Zoya, the register’s order.
std::sort moved 23 of the 24 students. So did Go’s sort.Slice and slices.SortFunc, which printed the same
scrambled order as each other. “Not guaranteed” cuts both ways: on another input an unstable sort can happen to
keep the order, so one passing test proves nothing. When the order of ties matters, use a stable sort, or make the
comparison decide every tie. Comparing on several keys at once is a topic of its own, later in this module.
When sorting first is the wrong move
Sorting is a strong default, not a rule. Four signs point elsewhere:
- The question is about the original sequence: a stretch of consecutive days, a window of the last k events, the first day on which something happens. Sorting destroys the sequence, so these need scans of the input as it is, such as prefix sums and sliding windows.
- One scan with a hash map does the job and keeps the positions: two values with a given sum whose positions you must return, or how often each value occurs. Both take O(n) on average that way.
- The values come from a small range, such as exam marks from 0 to 100. Counting them takes O(n + range), which a later lesson of this module covers.
- Only a few values are needed, such as the k smallest or the median. A heap or a selection algorithm does less work than a full sort.
Exercise
The depot now wants to know how crowded its busiest moment was, which turns the worked problem into a counting
question. Repeated times are the trap: work out [5, 5, 5] by hand before you start.
Exercise · Easy · Python
Count the pairs at the closest gap
The depot wants to know how crowded its busiest moment was: not only the smallest gap between two departures, but how many pairs of departures are exactly that close. Write closest_gap(times) in gaps.py. It returns a tuple (gap, pairs): the smallest difference between two of the times, and the number of pairs of positions i < j whose times differ by exactly that much.
2 <= len(times) <= 100,000, and each time is a second of the day,0 <= times[i] <= 86,399.- Two vans can leave in the same second, and then the smallest gap is 0.
- Leave the caller's list as it was.
closest_gap([40, 7, 25, 31]) # (6, 1): only 25 and 31 are 6 apart
closest_gap([10, 20, 30]) # (10, 2): 10 and 20, and 20 and 30
closest_gap([5, 5, 5]) # (0, 3): each of the three pairs is 0 apart
The sample tests check these cases and a few more, run 300 random lists against a brute force of their own (they print the first list where your answer differs), and give closest_gap 20,000 times twice, stopping it after 200,000 executed lines of Python.
Starter code · gaps.py
def closest_gap(times):
"""Return (smallest gap, number of pairs exactly that far apart) for a list of at least two times."""
# Replace this line with your code.
return 0, 0 The sample tests · test_gaps.py
import random
import sys
from collections import Counter
from gaps import closest_gap
LINE_LIMIT = 200_000
def every_pair(times):
"""A brute force the tests trust: the gap of every pair, then the smallest and how often it occurs."""
gaps = [abs(times[i] - times[j]) for i in range(len(times)) for j in range(i + 1, len(times))]
smallest = min(gaps)
return smallest, gaps.count(smallest)
class TooSlow(Exception):
pass
def within_line_limit(times):
"""Call closest_gap, but stop it after LINE_LIMIT executed lines: a time limit that is the same everywhere."""
executed = 0
def count(frame, event, arg):
nonlocal executed
if event == "line":
executed += 1
if executed > LINE_LIMIT:
raise TooSlow
return count
sys.settrace(count)
try:
return closest_gap(times)
except TooSlow:
raise AssertionError(f"too slow: more than {LINE_LIMIT:,} lines ran; sort first, then one pass") from None
finally:
sys.settrace(None)
def test_prompt_examples():
"""the three examples of the prompt"""
assert closest_gap([40, 7, 25, 31]) == (6, 1)
assert closest_gap([10, 20, 30]) == (10, 2)
assert closest_gap([5, 5, 5]) == (0, 3)
def test_two_times():
"""two times make one pair, in either order"""
assert closest_gap([86_399, 0]) == (86_399, 1)
assert closest_gap([600, 600]) == (0, 1)
def test_runs_of_equal_times():
"""a run of k equal times holds k(k - 1)/2 pairs at gap 0"""
assert closest_gap([7, 1, 7, 4, 7, 4]) == (0, 4)
assert closest_gap([9, 9, 9, 9, 9]) == (0, 10)
def test_input_unchanged():
"""the caller's list keeps its order"""
times = [30, 10, 20, 10]
closest_gap(times)
assert times == [30, 10, 20, 10]
def test_against_brute_force():
"""300 random lists: the same answer as a trusted brute force"""
rng = random.Random(17)
for _ in range(300):
times = [rng.randrange(25) for _ in range(rng.randint(2, 9))]
assert closest_gap(times) == every_pair(times), f"closest_gap({times})"
def test_fast_enough():
"""20,000 times, twice, each within the line limit"""
rng = random.Random(4)
spaced = list(range(0, 60_000, 3)) # 20,000 times, every neighbour 3 apart
rng.shuffle(spaced)
assert within_line_limit(spaced) == (3, 19_999)
crowded = [rng.randrange(100) for _ in range(20_000)] # only 100 different seconds
expected = (0, sum(k * (k - 1) // 2 for k in Counter(crowded).values()))
assert within_line_limit(crowded) == expected A hint
Sort a copy first, as in the lesson. Then work [5, 5, 5] by hand: how many pairs are 0 apart, and how many of them are neighbours in the sorted list? The answer is different when the smallest gap is above 0.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What does it mean for a sort to be stable, and when does it matter?
A stable sort keeps items whose keys are equal in the order they had before the sort. It matters whenever that order carries information: sort a register that is already alphabetical by grade, and a stable sort leaves each grade alphabetical, which is how two sorts in a row, the second key first, give a two-key order. It also matters when ties must come out in a fixed, predictable order. Python’s sort, JavaScript’s Array.prototype.sort, Java’s List.sort and its sort for object arrays, C++ std::stable_sort and Go’s sort.SliceStable and slices.SortStableFunc are stable. C++ std::sort and Go’s sort.Slice and slices.SortFunc are not. For plain integers the question does not arise, because equal integers cannot be told apart.
How do you sort an array but still report each value’s original index?
Make the index travel with the value. Either sort the indices, using each index’s value as the key, such as sorted(range(n), key=lambda i: a[i]) in Python or an Integer[] of indices with Comparator.comparingInt in Java, or sort (value, index) pairs, which also breaks ties by index. Each item of the sorted order then still says where it came from. Do not sort the values and look each one up again with index() or a search: every copy of a repeated value maps to its first position, and each lookup is another scan, O(n²) in all.
Key takeaways
- Sorting costs O(n log n) once and buys order: equal and close values become neighbours, two pointers and binary search can discard candidates, and greedy rules get an order to work in.
- After sorting, the two closest values are neighbours, because the gap of any pair is a sum of neighbour gaps; the n − 1 neighbour gaps decide the answer.
- Keep positions by sorting the positions by their values or by sorting (value, position) pairs. Never sort the values and look them up again.
- Stable: Python’s and JavaScript’s sorts, Java’s
List.sortand object sorts,std::stable_sort,sort.SliceStableandslices.SortStableFunc. Not guaranteed:std::sort,sort.Sliceandslices.SortFunc. - Check a sorted plan against the brute force on many small random inputs, with a check that accepts every correct answer.
References
- Sorting Techniques (Python Software Foundation)
- Built-in functions: sorted() (Python Software Foundation)
- Built-in types: list.sort() (Python Software Foundation)
- ECMAScript Language Specification: Array.prototype.sort, CompareArrayElements and Array.prototype.toSorted (Ecma International (TC39))
- Java SE 25 API: Arrays (Oracle)
- Java SE 25 API: List.sort (Oracle)
- Java SE 25 API: Collections.sort (Oracle)
- Working Draft, Programming Languages: C++ (N4950), [sort] and [stable.sort] (ISO/IEC JTC1/SC22/WG21)
- Go standard library: package sort (The Go Authors)
- Go standard library: package slices (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress