Sorting as a pattern
Sorting first is often the cheapest way to make a problem easy. Once data is ordered, duplicates sit together, overlaps are adjacent and events happen in time order, so a single scan finishes the job.
Learning goals
- Recognise problems that become a linear scan after an O(n log n) sort
- Group equal or equivalent items by sorting on a key
- Merge intervals and count overlapping events with a sweep
- Write correct custom sort keys and tie-breakers
Prerequisites
Checking a stack of receipts for duplicates is hard when they are shuffled and easy once they are sorted by date: duplicates end up next to each other.
Sort then scan
Many problems that look like they need comparing all pairs become a single pass once the data is sorted. The sort costs O(n log n) and the scan O(n), which is usually acceptable, and it uses no extra hash structures.
Examples: find any duplicate (compare neighbours), the minimum difference between two elements (it is between neighbours), and 3Sum (sort, then two pointers for each first element).
1static int minimumDifference(int[] arr) {2int[] a = arr.clone();3Arrays.sort(a);4int best = Integer.MAX_VALUE;5for (int i = 1; i < a.length; i++) {6best = Math.min(best, a[i] - a[i - 1]); // the closest pair is always adjacent after sorting7}8return best;9}
Worst
O(n log n)
Space
O(1) to O(n)
Group equals
To group items that are “the same” under some definition, sort them by a key that captures that definition, or map each item to a canonical key. Anagrams share the same sorted letters, so the sorted string is a natural key.
Sorting the whole list groups equal keys together (O(n log n)); a HashMap<key, List> groups them without sorting the list (expected O(n), plus the cost of computing each key).
1static Collection<List<String>> groupAnagrams(String[] words) {2Map<String, List<String>> groups = new HashMap<>();3for (String w : words) {4char[] key = w.toCharArray();5Arrays.sort(key); // canonical form: letters in order6groups.computeIfAbsent(new String(key), k -> new ArrayList<>()).add(w);7}8return groups.values();9}
Meeting intervals
Sort intervals by start. Then two intervals overlap only if one starts before the previous one ends, so a single scan can merge them: extend the last merged interval or start a new one.
Enter your own start, end pairs in the animation. The top lane shows the intervals in sorted order; the bottom lane shows the merged result growing.
Worst
O(n log n)
Space
O(n)
Merge overlapping intervals (sort by start, then scan)
Interactive · edit the inputinput
In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour.
1public static List<int[]> merge(int[][] intervals) {2Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); // sort by start3List<int[]> merged = new ArrayList<>();4for (int[] cur : intervals) {5int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1);6if (last != null && cur[0] <= last[1]) {7last[1] = Math.max(last[1], cur[1]); // overlap: extend8} else {9merged.add(cur); // gap: start a new interval10}11}12return merged;13}
Live state
intervals
5
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | intervals=5 | In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour. |
| 2 | L2 | intervals=5 | Sorted by start: O(n log n). Now one left-to-right scan is enough. |
| 3 | L6 | start=1 end=3 last end=none | Nothing merged yet: this interval starts the list. |
| 4 | L9 | merged=1 | Append [1, 3] as a new merged interval. |
| 5 | L6 | start=2 end=6 last end=3 | [2, 6] starts before the last merged interval ends, so they overlap. |
| 6 | L7 | last end=6 | Extend the last interval: end = max(3, 6) = 6. |
| 7 | L6 | start=8 end=10 last end=6 | [8, 10] starts after 6: there is a gap. |
| 8 | L9 | merged=2 | Append [8, 10] as a new merged interval. |
| 9 | L6 | start=9 end=12 last end=10 | [9, 12] starts before the last merged interval ends, so they overlap. |
| 10 | L7 | last end=12 | Extend the last interval: end = max(10, 12) = 12. |
| 11 | L6 | start=15 end=18 last end=12 | [15, 18] starts after 12: there is a gap. |
| 12 | L9 | merged=3 | Append [15, 18] as a new merged interval. |
| 13 | L12 | return=[1,6] [8,12] [15,18] | 5 intervals became 3. O(n log n) for the sort plus O(n) for the scan. |
Sweep sorted events
A sweep line moves through time and processes events in sorted order: each start adds one to a running count, each end subtracts one. The maximum running count is the maximum number of simultaneous events, which is the number of meeting rooms needed.
Sorting starts and ends separately and walking them with two pointers avoids building event objects. With half-open intervals [start, end), an end at time t frees its room before a start at the same t takes one, which is why the loop uses <=.
Worst
O(n log n)
Space
O(n)
Minimum meeting rooms (sweep sorted events)
Interactive · edit the inputmeetings
starts (sorted)
ends (sorted)
Only the times matter, not which meeting is which. Sort starts and ends separately and sweep left to right.
1public static int minRooms(int[][] meetings) { // [start, end), end is exclusive2int n = meetings.length;3int[] starts = new int[n], ends = new int[n];4for (int i = 0; i < n; i++) { starts[i] = meetings[i][0]; ends[i] = meetings[i][1]; }5Arrays.sort(starts);6Arrays.sort(ends);7int rooms = 0, best = 0, e = 0;8for (int s = 0; s < n; s++) {9while (ends[e] <= starts[s]) { e++; rooms--; } // meetings that ended free a room10rooms++;11best = Math.max(best, rooms);12}13return best;14}
Live state
rooms
0
best
0
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | rooms=0 best=0 | Only the times matter, not which meeting is which. Sort starts and ends separately and sweep left to right. |
| 2 | L11 | time=0 rooms=1 best=1 | A meeting starts at 0: 1 room in use. Peak so far 1. |
| 3 | L11 | time=5 rooms=2 best=2 | A meeting starts at 5: 2 rooms in use. Peak so far 2. |
| 4 | L9 | time=10 rooms=1 best=2 | A meeting ended at 10 ≤ 10: its room is free again. |
| 5 | L11 | time=10 rooms=2 best=2 | A meeting starts at 10: 2 rooms in use. Peak so far 2. |
| 6 | L11 | time=15 rooms=3 best=3 | A meeting starts at 15: 3 rooms in use. Peak so far 3. |
| 7 | L9 | time=22 rooms=2 best=3 | A meeting ended at 20 ≤ 22: its room is free again. |
| 8 | L11 | time=22 rooms=3 best=3 | A meeting starts at 22: 3 rooms in use. Peak so far 3. |
| 9 | L13 | return=3 | 3 rooms needed: the maximum number of meetings overlapping at any moment. O(n log n). |
Sorting with custom key
Real problems sort by derived keys: by end time for interval scheduling, by frequency then value, by distance from a point. Build the comparator from Comparator.comparing and chain tie-breakers with thenComparing.
Sorting by end time is the key step in the greedy “maximum non-overlapping intervals” algorithm: always keep the interval that finishes first. Sorting by start time would be wrong for that problem.
1// most frequent first, ties broken by smaller value2Integer[] boxed = Arrays.stream(nums).boxed().toArray(Integer[]::new);3Map<Integer, Long> freq = Arrays.stream(nums).boxed()4.collect(Collectors.groupingBy(x -> x, Collectors.counting()));5Arrays.sort(boxed, Comparator.<Integer, Long>comparing(freq::get).reversed()6.thenComparing(Comparator.naturalOrder()));78// interval scheduling: sort by end time9Arrays.sort(intervals, Comparator.comparingInt(iv -> iv[1]));
Practice ladder
Now try it yourself
Easy
- Contains duplicate (sort version)
- Minimum absolute difference
- Meeting rooms (can attend all?)
Medium
- Merge intervals
- Meeting rooms II
- Group anagrams
- Non-overlapping intervals
Advanced
- Insert interval
- Employee free time
- Minimum number of arrows to burst balloons
Key takeaways
- An O(n log n) sort often turns a quadratic pair search into a linear scan.
- Sort intervals by start to merge them; sweep sorted starts and ends to count overlaps.
- Choose the sort key deliberately and break ties explicitly with thenComparing.