Merge Intervals
Sort by start, then only compare with the last one.
- Brute force
- O(n²) — compare every pair, repeatedly
- With the pattern
- O(n log n) — sort, then one linear pass
- Extra space
- O(n) — output (O(log n)–O(n) for the sort)
In plain English
“You're combining everyone's meeting times onto one calendar. If you lay the meetings out in order of start time, you only ever need to ask, "does this meeting start before the last block I wrote down ends?" If yes, stretch that block; if no, start a new one.”
Why it's fast
Unsorted, any interval might overlap any other, so you'd compare all pairs (O(n²)). After sorting by start, an interval can only overlap the most recent merged block — everything earlier already ended. That turns merging into a single linear pass after an O(n log n) sort.
Spot it when the problem says…
- list of intervals [start, end]
- merge overlapping
- insert a new interval
- minimum number of rooms / arrows / removals
- free time / gaps between meetings
- intersection of two interval lists
Visualize
Merge overlapping intervals
Merge all overlapping intervals and return the non-overlapping result.
Unsorted, overlaps are hard to see — they could be anywhere.
1def merge(intervals):2 intervals.sort(key=lambda iv: iv[0])3 merged = [intervals[0]]4 for start, end in intervals[1:]:5 last = merged[-1]6 if start <= last[1]: # overlaps7 last[1] = max(last[1], end) # stretch it8 else:9 merged.append([start, end]) # gap: new block10 return mergedHow to apply it
- 01Sort intervals by start (or by end for greedy 'max non-overlapping' questions).
- 02Start the output with the first interval.
- 03For each next interval: if
start <= last_end, extendlast_end = max(last_end, end). - 04Otherwise append it as a new block.
Variations you'll meet
Merge overlapping
The visualizer's example.
def merge(intervals: list[list[int]]) -> list[list[int]]: intervals.sort(key=lambda iv: iv[0]) out = [intervals[0][:]] for start, end in intervals[1:]: if start <= out[-1][1]: # overlaps last block out[-1][1] = max(out[-1][1], end) else: out.append([start, end]) return outMax non-overlapping (sort by end)
Greedy: always keep the interval that finishes earliest. Answers Non-overlapping Intervals and Minimum Arrows.
def erase_overlap_intervals(intervals: list[list[int]]) -> int: intervals.sort(key=lambda iv: iv[1]) kept, last_end = 0, float("-inf") for start, end in intervals: if start >= last_end: kept += 1 last_end = end return len(intervals) - keptSweep line / min rooms
Turn each interval into +1 at start and -1 at end, sort events, track the running max. A min-heap of end times works too.
Two-list intersection
Two pointers over two sorted lists; overlap is [max(starts), min(ends)]; advance the one that ends first.
Common mistakes
- Forgetting to sort first.
- Setting
last_end = endinstead ofmax(last_end, end)— a short interval inside a long one shrinks the block. - Getting touching intervals wrong: is
[1,2]and[2,3]an overlap? Read the problem (<=vs<). - Mutating the input lists you appended to the output, aliasing bugs when the caller reuses them.
Practice ladder
Easy → hard. Tick them off as you go.
Think you've got it?
Let the AI interviewer throw a Merge Intervals problem at you and grade how you explain it.