TL;DR
Sort by start, sweep once, extending or starting a block β O(n log n) time, O(n) space for the output.
Approach 1 β Brute force: merge pairs until nothing changes
Scan for any overlapping pair, fuse it, and restart. Repeat until a full scan finds no overlap. Closed intervals [a, b] and [c, d] overlap when a <= d and c <= b.
from typing import List
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
items = [list(it) for it in intervals]
changed = True
while changed:
changed = False
out: List[List[int]] = []
for a, b in items:
merged_in = False
for block in out:
if a <= block[1] and block[0] <= b:
block[0] = min(block[0], a)
block[1] = max(block[1], b)
merged_in = True
changed = changed or True
break
if not merged_in:
out.append([a, b])
if len(out) == len(items):
changed = False
items = out
return sorted(items, key=lambda it: it[0])
Complexity: each pass is O(n^2) and a pass can trigger another (merging two blocks may make a third overlap the result), so worst case is O(n^2) per round with up to O(n) rounds β O(n^3) in pathological chains. Space O(n).
At n = 10^4, quadratic-or-worse repeated scanning is hopeless next to a single sort.
Approach 2 β Sort + linear sweep
The insight: sorted by start, every interval that should merge into the current block arrives consecutively. While the incoming interval starts at or before the blockβs end, it belongs to the block; the moment one starts later, the block is final β nothing after it can reach back, because all later starts are even bigger.
from typing import List
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda it: it[0])
merged: List[List[int]] = []
for start, end in intervals:
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
Walkthrough on [[1,3],[2,6],[8,10],[15,18]]:
- Sort by start β unchanged.
[1,3]: output empty β new block. merged = [[1,3]].
[2,6]: 2 <= 3 β extend: end becomes max(3, 6) = 6. merged = [[1,6]].
[8,10]: 8 <= 6 is false β new block. merged = [[1,6],[8,10]].
[15,18]: 15 <= 10 is false β new block. merged = [[1,6],[8,10],[15,18]]. Matches the expected output.
And the nesting case [[1,10],[2,3]]: [2,3] has 2 <= 10 β end becomes max(10, 3) = 10 β the max keeps the block from shrinking.
Complexity: O(n log n) time (the sort dominates the O(n) sweep), O(n) space for the output (sorting in place otherwise).
Variant worth knowing: if coordinates were huge streams of events rather than a small list, the same result falls out of a sweep line over +1/-1 events at starts and ends β the merged intervals are the maximal stretches where the running count is positive. For this problem the sort-and-sweep above is strictly simpler.
Common pitfalls
- Writing the extend as
merged[-1][1] = end instead of max(...) β a nested interval like [2,3] inside [1,10] would wrongly shrink the block.
- Using
start < merged[-1][1] β closed intervals that touch, like [1,4] and [4,5], must merge, so the test is <=.
- Forgetting to sort first and sweeping the raw input β the βonly neighbors matterβ property exists only after sorting by start.
- Sorting by end (or by start descending) out of habit from other interval problems β the sweepβs invariant depends specifically on ascending starts.
Pattern takeaway
Sort by start, then maintain one growing βcurrent blockβ and ask a single question per interval: does it reach the block (start <= block_end) or not? Extend with max of ends, or seal the block and start fresh. This sort-and-sweep with a max-extend is the backbone that Insert Interval, Meeting Rooms, and most other interval problems specialize.