Merge a list of overlapping intervals, and justify the key you sorted by
Sorting by start time is what reduces the merge to a single scan carrying one scalar, the end of the open block; sorting by end breaks it, and sorting by end is instead correct for maximum non-overlapping selection. Both run in O(n log n), and equal timestamps decide the answer.
What the interviewer is scoring
- Whether the sort key is derived from the state the scan must carry, rather than asserted as the standard approach
- Does the candidate take the maximum of the two ends when merging, or assume intervals are never nested
- That closed versus half-open semantics are raised before the code is written, not after a failing case
- Whether they can name a different interval problem where a different sort key is correct, and say why
- Does the candidate reach for a sweep line when the question moves from merging to counting concurrency
Answer
What the sort is buying
Overlap is a pairwise relation, so the naive formulation compares every interval against every other and costs O(n²). Sorting by start time collapses that. After sorting, when the scan reaches interval i, every interval with an earlier start has already been consumed, and the only fact about all of them that can still matter is the furthest right edge of the block currently open. Everything else has been summarised away. That is the justification, and it is a claim about state: sorting by start reduces the history you must carry from a set of intervals to a single integer.
Sorting by end does not have that property, and the counterexample is short. Take [0,2], [5,6], [1,10]. By ascending end that is exactly the order given. The scan opens [0,2], sees [5,6] starting after 2, closes and emits [0,2], then sees [1,10] which overlaps the block it just emitted. The failure is structural: with ends ascending, a later interval can still begin earlier than anything you have seen, so no single scalar summarises the past.
Arrays.sort(iv, Comparator.comparingInt(a -> a[0]));
List<int[]> out = new ArrayList<>();
int[] cur = iv[0].clone();
for (int i = 1; i < iv.length; i++) {
if (iv[i][0] <= cur[1]) { // closed intervals: touching merges
cur[1] = Math.max(cur[1], iv[i][1]); // max, because [1,10] then [2,3] is nested
} else {
out.add(cur);
cur = iv[i].clone();
}
}
out.add(cur); // the last block is never closed by a successor
Two lines in there are where implementations die. Math.max rather than a plain assignment is the nested-interval case, and it survives most hand-written test data because people write examples that overlap partially. Appending cur after the loop is the other, since nothing inside the loop ever emits the final block.
Cost is O(n log n) for the sort plus a linear pass, with O(n) for the output and O(1) working state. If the caller can tolerate mutation and the array is already start-sorted, the merge is genuinely in place.
Sorting by end, and when that is the right key
The reason to insist the sort key is derived rather than remembered is that a neighbouring problem wants the opposite key. Selecting the largest set of pairwise non-overlapping intervals is greedy on earliest end time, because finishing early is what leaves the most room for everything after it, and that greedy choice survives an exchange argument. Merging wants earliest start; scheduling wants earliest finish. Same input type, different key, and the key follows from what the algorithm needs to be true at each step.
Counting concurrency with a sweep
Change the question to "how many meeting rooms do you need" and you no longer want merged blocks — you want the maximum number of intervals live at any instant. Separate the endpoints, sort each list, and sweep them together, treating an end before a start when times coincide.
int[] starts = ..., ends = ...; // both sorted ascending, length n
int rooms = 0, best = 0, e = 0;
for (int s = 0; s < n; s++) {
while (ends[e] <= starts[s]) { rooms--; e++; } // a room freed at t is reusable at t
rooms++;
best = Math.max(best, rooms);
}
The end pointer needs no bounds check, and the reason is a small counting argument worth having ready: if e ever reached s + 1 there would be s + 1 intervals whose end is at or before starts[s], hence s + 1 intervals whose start is strictly below starts[s], which is impossible when starts[s] is the (s+1)-th smallest start. Two sorts and two linear pointers gives O(n log n) time and O(n) space.
Equal timestamps are not a detail
The <= in the merge and the <= in the sweep are both semantic decisions that the problem statement usually leaves implicit, and they change the output. On closed intervals, [1,2] and [2,3] share the instant 2, so they merge into [1,3] and they need two rooms. On half-open [1,2) and [2,3) they are disjoint, so they stay separate and one room suffices. Nothing in the data tells you which convention applies; the domain does. Meeting times and calendar slots are almost always half-open, while inclusive integer ranges like line numbers or version ranges are usually closed.
Ask, or state your assumption and move on — either is a strong signal, and silently picking one is the thing that gets marked down. Under an event-list formulation the same decision reappears as a tie-break rule in the comparator, ordering end events before start events at identical times, which is easy to get wrong precisely because it looks like a stylistic choice rather than a correctness one.
Name the sort key as a consequence of the state your scan carries, and settle closed versus half-open before writing the first comparison. Both are places where the code looks equally reasonable either way and only one answer is right.
Likely follow-ups
- How would you insert a single new interval into an already-merged list without re-sorting?
- The input arrives as a stream too large to sort. What changes?
- Given employee calendars, find the free slots common to all of them. What is the cost?
- Why does maximum non-overlapping selection sort by end time when merging sorts by start?
Related questions
- Return every distinct triplet in the array that sums to zero.mediumAlso on sorting4 min
- Given a binary tree, decide whether it is a valid binary search tree.mediumAlso on invariants5 min
- Given package weights in a fixed order, find the smallest ship capacity that delivers them all within D daysmediumAlso on invariants5 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumAlso on invariants6 min
- What actually happens during a context switch, and why should an application developer care?mediumAlso on scheduling5 min
- How do you turn an effort estimate into a delivery date you would defend, and what do you do when the sales lead has already promised an earlier one?hardAlso on scheduling5 min
- How do you turn what the business tells you into a domain model that holds up once the exceptions arrive?hardAlso on invariants7 min
- Field data says your page has an INP of 400 ms. How do you find the cause and fix it?hardAlso on scheduling4 min