The intervals pattern — merging, scheduling, and overlap problems
A surprising number of problems that look completely different on the surface — merging meetings, scheduling rooms, inserting a new event — collapse into the same shape once the intervals are sorted, which is the one insight that makes all of them tractable.
4 min read
The one setup step that makes every interval problem tractable: sort first
meetings = [(1, 3), (5, 8), (2, 6), (8, 10)]
meetings.sort(key=lambda interval: interval[0]) # sort by START time
# [(1, 3), (2, 6), (5, 8), (8, 10)]Almost every interval problem starts with sorting by start time (occasionally by end time, depending on the specific question) — once sorted, two intervals can only possibly overlap with their immediate neighbors in the sorted order, never with something far away, which is what turns an otherwise all-pairs O(n²) comparison problem into a single O(n log n) sort followed by one O(n) linear pass.
Merging overlapping intervals: the canonical version of this pattern
def merge_intervals(intervals):
intervals.sort(key=lambda i: i[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
last_end = merged[-1][1]
if start <= last_end: # THIS interval overlaps the LAST merged one
merged[-1] = (merged[-1][0], max(last_end, end)) # extend it, don't add a new entry
else:
merged.append((start, end)) # no overlap — a genuinely NEW interval
return merged
merge_intervals([(1, 3), (2, 6), (5, 8), (8, 10)])
# [(1, 10)] — all four overlap in a chain, so they collapse into oneThe check start <= last_end is the entire mechanism: after sorting by start time, if the next interval's start is at or before the last merged interval's end, they overlap (or touch) and should merge into one — extending the last merged interval's end to whichever is larger. If not, the new interval genuinely starts a new, separate group. This single check, applied once per interval in the sorted list, correctly handles chains of overlaps (A overlaps B, which overlaps C, even if A and C don't directly overlap) without any extra logic.
Meeting rooms: does a schedule have ANY conflict at all
def can_attend_all(meetings):
meetings.sort(key=lambda m: m[0])
for i in range(1, len(meetings)):
if meetings[i][0] < meetings[i - 1][1]: # this meeting STARTS before the previous one ENDS
return False # a genuine conflict — one person can't attend both
return TrueAfter sorting by start time, a conflict can only ever occur between adjacent meetings in that sorted order — if meeting i starts before meeting i-1 ends, they overlap, and no valid single-person schedule can attend both. This is the exact same "sort, then compare only neighbors" insight as merging, applied to a yes/no question instead of a merge — the sort is what makes checking only adjacent pairs sufficient, rather than needing to compare every pair of meetings against each other.
Inserting a new interval into an already-sorted, non-overlapping list
def insert_interval(intervals, new_interval):
result = []
i = 0
n = len(intervals)
while i < n and intervals[i][1] < new_interval[0]: # entirely BEFORE the new interval — keep as-is
result.append(intervals[i])
i += 1
while i < n and intervals[i][0] <= new_interval[1]: # OVERLAPS the new interval — merge it in
new_interval = (min(new_interval[0], intervals[i][0]), max(new_interval[1], intervals[i][1]))
i += 1
result.append(new_interval)
while i < n: # entirely AFTER the new interval — keep as-is
result.append(intervals[i])
i += 1
return resultBecause the input is already sorted and non-overlapping (a real, common precondition for this variant of the problem), inserting one new interval only needs three passes: intervals entirely before the new one (copied unchanged), intervals that overlap it (merged into a single growing interval), and intervals entirely after (copied unchanged) — a single O(n) linear scan, since the sorted structure already guarantees which intervals could possibly overlap the new one, without needing to re-sort or re-check the whole list from scratch.
Recognizing the pattern: the actual signal to look for
The recurring signal across all of these: a problem described in terms of ranges, time windows, or "does X overlap with Y" — meetings, bookings, ranges of numbers, even byte ranges in a file — is very likely an interval problem, and the very first, near-automatic move should be sorting by start (or occasionally end) time before reaching for anything more complex. A surprising fraction of interval problems that look intimidating on first read turn out to be exactly this same sort-then-linear-scan shape underneath.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does sorting intervals by start time turn an O(n²) all-pairs comparison into an O(n log n) problem?
2. In the merge-intervals algorithm, what does the check `start <= last_end` actually determine?
3. Why can a meeting-room conflict check get away with comparing only ADJACENT meetings after sorting?