Merge Intervals
Given a list of intervals, merge all overlapping ones and return the non-overlapping result.
- 1 <= intervals.length <= 10⁴
- intervals[i].length == 2
- 0 <= starti <= endi <= 10⁴
Intuition
Merge intervals combines all overlapping intervals in an unsorted list. Comparing every pair is O(n²) and also awkward, because merging two intervals can create a new overlap with a third.
Sorting removes that complication entirely:
- Sort by start time, and every interval that can merge with the current one appears immediately next to it — so a single left-to-right pass suffices.
After sorting, an interval either overlaps the one being built or begins entirely after it. There is no case where a later interval reaches back to merge with an earlier one.
Walk the sorted list keeping a current interval. If the next interval starts at or before the current end, extend the current end to max(currentEnd, nextEnd); otherwise the current interval is finished — append it and start a new one.
Taking the maximum of the ends is essential. One interval can fully contain another, as with [1, 10] and [2, 3], and blindly assigning the next interval's end would shrink the merged range.
The overlap test must be <=, since touching intervals like [1, 3] and [3, 5] merge into [1, 5] for this problem.
The final interval must be appended after the loop, since nothing follows to trigger its append. Omitting that step silently drops the last interval — a bug that only shows in the output's length.
Sorting dominates the cost at O(n log n), with the merge pass itself O(n).
Almost every interval problem starts with a sort, and the choice of key is the whole puzzle. Sort by start when you're merging or inserting — it guarantees that anything overlapping the current block appears next, so one pass suffices. Sort by end when you're packing the most non-overlapping items in (the classic greedy scheduling move). If you can't decide, ask what the greedy choice is: merging cares about what begins next, scheduling cares about what frees up soonest.
Approach
Before reading on: price up what the direct approach costs here, then ask what sorting by one endpoint makes obvious that was hidden before. Aim for O(n log n) time and O(n) space.
Sort by start time
Sorting places every mergeable interval adjacent to its partner, so one pass suffices and no later interval reaches back to an earlier one.
Carry a current interval
Walk the sorted list holding the interval being built. Each new interval either extends it or begins a new one.
Extend on overlap
If the next interval starts at or before the current end, merge by extending. Use <= — touching intervals like [1, 3] and [3, 5] merge into [1, 5].
Take the maximum end
Extend to max(currentEnd, nextEnd). One interval can contain another, as [1, 10] contains [2, 3], and assigning blindly would shrink the range.
Start fresh on a gap
When the next interval begins after the current end, the current one is complete — append it and begin a new current interval.
Append the final interval
Add the last interval after the loop ends. Nothing follows to trigger it, and omitting this silently drops one interval from the output.
Cost of the approach
Sorting dominates at O(n log n) time, with the merge pass O(n). Space is O(n) for the output, or O(log n) beyond it.
Solution & live demo
Common pitfalls
Sorting by end time
intervals.sort(key=lambda x: x[1])
intervals.sort(key=lambda x: x[0])
With [[1, 10], [2, 3], [4, 5]], sorting by end gives [2,3], [4,5], [1,10] — the wide interval that swallows both arrives last, so neither earlier block gets merged into it. Sorting by start makes overlaps adjacent.
Extending with end instead of max
if start <= res[-1][1]:
res[-1][1] = endif start <= res[-1][1]:
res[-1][1] = max(res[-1][1], end)A fully-contained interval shrinks the block it lands in: [1, 10] followed by [2, 3] becomes [1, 3], silently dropping everything from 3 to 10. Sorting by start orders the left edges only — it says nothing about which right edge is larger.
Using < and missing exact touches
if start < res[-1][1]:
if start <= res[-1][1]:
[1, 4] and [4, 5] share the endpoint 4, and LeetCode counts that as overlapping — the expected answer is [1, 5], not two intervals. Strict < treats touching as disjoint.
Edge cases
start <= last.end is true at equality, so adjacent intervals merge into [1,5].
max(last.end, end) keeps the larger end, so the inner interval is absorbed.
No start <= last.end ever holds, so each interval is appended unchanged.