Merge Intervals
Given a collection of ranges, combine every group that touches or overlaps into a single range and return what's left.
Unsorted, any pair might overlap โ so the first move is to sort by start time.
1def merge(intervals):2 intervals.sort(key=lambda x: x[0])3 out = []4 for start, end in intervals:5 if out and start <= out[-1][1]:6 out[-1][1] = max(out[-1][1], end)7 else:8 out.append([start, end])9 return outRead the 13 steps as text
- 1Unsorted, any pair might overlap โ so the first move is to sort by start time.
- 2Now every interval that could overlap the current one has already been seen.
- 3Take 1โ3.
- 4Nothing open reaches 1. 1โ3 starts a new range.
- 5Take 2โ6.
- 62 is not past 3, so they touch โ 1โ3 becomes 1โ6.
- 7Take 8โ10.
- 8Nothing open reaches 8. 8โ10 starts a new range.
- 9Take 9โ12.
- 109 is not past 10, so they touch โ 8โ10 becomes 8โ12.
- 11Take 15โ18.
- 12Nothing open reaches 15. 15โ18 starts a new range.
- 135 ranges collapsed into 3: 1โ6, 8โ12, 15โ18.
The idea
In arbitrary order, an interval can overlap anything โ so deciding whether to merge means comparing against everything. That's the O(nยฒ) trap.
Sorting by start time destroys that possibility, and it's worth being precise about why. Once starts are in order, any interval that could overlap the current one must have started earlier, and among those only the most recent output range can still be open. Every earlier one ended before it.
So the comparison shrinks from "all of them" to "the last one I emitted". Overlap is then a single test: does the new interval start at or before the last one ends?
When they do overlap, the merged range keeps the earlier start automatically โ sorting guaranteed it โ and takes the later of the two ends. That `max` is the part people drop, and it's needed whenever one interval is entirely swallowed by another.
The approach
- 1Sort the intervals by start.
- 2Walk through them, keeping a list of merged output.
- 3If the current interval starts at or before the end of the last output interval, they touch โ extend that interval's end to the larger of the two.
- 4Otherwise there's a genuine gap, so append the current interval as a new one.
Complexity
The sort dominates; the merge pass itself is linear. The space is the output, or O(log n) of sort overhead if you're allowed to merge in place.
Code
def merge(intervals):
intervals.sort(key=lambda x: x[0])
out = []
for start, end in intervals:
if out and start <= out[-1][1]:
out[-1][1] = max(out[-1][1], end)
else:
out.append([start, end])
return outWhat goes wrong
- โขWriting `out[-1][1] = end` instead of taking the max. An interval fully contained inside the previous one then shrinks it, silently losing coverage.
- โขUsing `<` rather than `<=` for the overlap test. Ranges that merely touch at a boundary are almost always meant to merge; check which the problem wants and say so.
- โขSorting by end time. It's the right sort for the maximum-non-overlapping-intervals problem and the wrong one here.