Why Sorting Matters
Sorting feels like busywork until you notice it unlocks binary search, deduplication, and half the algorithms you’ll ever write.
The problem
You've got a list of 50,000 customer emails and you need two things from it: a way to check "is this email already in the list?" fast, and a way to strip out the duplicates someone's messy import created. You wire up the obvious version. It works on your test data of ten rows. On the real file, the page spins for eight seconds and your laptop fan spins up.
The frustrating part is that nothing about the task is hard. Checking membership and removing duplicates are easy ideas. They only feel slow because the data is a shapeless jumble. Give it some structure first and both problems nearly solve themselves. That structure is sorted order, and it's the quiet prerequisite behind a huge fraction of the algorithms you'll ever write.
A first attempt
On unsorted data, every question means scanning. To dedup, you compare each element against every element you've already kept:
def dedup_naive(nums):
result = []
for x in nums:
if x not in result: # this membership check is itself a full scan
result.append(x)
return resultEvery x not in result walks the whole result list. That's a loop inside a loop:
O(n²). For 50,000 items that's 2.5 billion comparisons. And searching the raw list for
one email is O(n) every single time — do it for every incoming email and you're back to
O(n²) again. The data's lack of order is taxing you on every single operation.
The insight
Pay the ordering cost once, up front, and every later operation gets cheap. Sorting
takes O(n log n) — more than a single scan, but you do it a single time. After that:
- Duplicates sit next to each other, so one pass removes them.
- Membership becomes binary search —
O(log n)per lookup instead ofO(n). - The min and max are just the first and last elements.
- "Are any two values close?" only needs to compare neighbours.
Sorting isn't the answer to a problem. It's the setup that makes a dozen other problems trivial. That's why it earns its own module.
How it works
Here's the pattern almost every sorting-based solution follows.
Sort the data once
Spend O(n log n) to put everything in order. This is your one investment.
Exploit adjacency
In sorted data, related values are neighbours. Equal values touch. Close values are near. So most questions collapse to a single left-to-right pass comparing each element to the one before it.
Or exploit the order for search
If instead of a pass you need repeated lookups, binary search each one in O(log n),
because sorted order lets you throw away half the array per comparison.
Watch how the shapeless list turns into something you can just walk:
raw: [ 5 1 5 3 1 2 3 ] duplicates scattered, no structure
sorted: [ 1 1 2 3 3 5 5 ] dupes now adjacent → keep first of each run
unique: [ 1 2 3 5 ] one clean pass, O(n)The code
Sort first, then a single pass removes duplicates. Every language ships a fast
O(n log n) sort, so lean on it.
def unique_sorted(nums):
nums.sort() # O(n log n)
result = []
for x in nums: # O(n) single pass
if not result or result[-1] != x:
result.append(x)
return resultfunction uniqueSorted(nums: number[]): number[] {
nums.sort((a, b) => a - b); // numeric sort, O(n log n)
const result: number[] = [];
for (const x of nums) {
if (result.length === 0 || result[result.length - 1] !== x) {
result.push(x);
}
}
return result;
}import java.util.Arrays;
int[] uniqueSorted(int[] nums) {
Arrays.sort(nums); // O(n log n)
int k = 0;
for (int x : nums) { // O(n) single pass
if (k == 0 || nums[k - 1] != x) nums[k++] = x;
}
return Arrays.copyOf(nums, k);
}#include <stdlib.h>
int cmp(const void *a, const void *b) {
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y); // safe, no overflow
}
// sorts in place, compacts unique values to the front, returns new length
int unique_sorted(int *nums, int n) {
qsort(nums, n, sizeof(int), cmp); // O(n log n)
int k = 0;
for (int i = 0; i < n; i++) {
if (k == 0 || nums[k - 1] != nums[i]) nums[k++] = nums[i];
}
return k;
}#include <vector>
#include <algorithm>
using namespace std;
vector<int> uniqueSorted(vector<int> nums) {
sort(nums.begin(), nums.end()); // O(n log n)
nums.erase(unique(nums.begin(), nums.end()), nums.end());
return nums;
}Complexity
| Aspect | Cost | Why |
|---|---|---|
| Time | O(n log n) | dominated by the one sort; the pass is O(n) |
| Space | O(n) | for the output; in-place variants reach O(1) |
Compare that to the naive O(n²) dedup: at 50,000 items you go from ~2.5 billion
operations down to under a million. Same idea, sorted first.
When to use it
Sorting is preprocessing, not a goal
Reach for a sort whenever a problem gets easier once related items are neighbours: finding
duplicates, closest pairs, grouping, or setting up repeated binary searches. The rule of
thumb: sort if you'll query the data many times, or if the answer depends on relative
order rather than original position. If you only need one lookup, a single O(n) scan may
beat paying O(n log n) to sort.
Practice
Recap
- Sorting is a one-time
O(n log n)investment that makes dedup, search, min/max, and grouping cheap or trivial. - After sorting, related values are adjacent — so most problems reduce to a single pass or a binary search.
- Sort when you'll reuse the ordered data; skip it for one-off lookups where a scan wins.
How is this guide?
Last updated on
Searching Across Two Arrays
Finding the median of two sorted arrays without merging them is a classic — and binary search cracks it in log time.
The Simple (Slow) Sorts
Bubble, selection, and insertion sort are the ones you’d invent yourself — and understanding why they’re slow teaches what fast sorts avoid.