~/searching/intervals
Intervals
Sort ranges by where they start, then sweep once: each range either joins the one before it or starts a new one.
Sort intervals by start. Walk through them, keeping the last merged interval: extend it when the next one starts inside it, otherwise close it and start a new one.
You have ranges (times, positions, bookings) and need their union, their overlaps, how many overlap at once, or how many to drop so none overlap.
O(n log n)
O(n)
You’ll recognise it when
- The input is a list of ranges:
[start, end]pairs for meetings, bookings, time slots, segments of a line. - You need the union (“which times is somebody busy?”), the overlaps, or the gaps between them.
- The question asks how many things happen at the same time (rooms, servers, platforms), or how few you must remove so nothing overlaps.
- Checking every pair would work, but that’s O(n²) and
nis 10⁵.
It’s close to greedy scheduling, and many interval problems are exactly that: after sorting, one pass with a simple rule decides everything. The trick is choosing what to sort by, the start or the end.
The idea
Think of a week of meetings written on sticky notes in random order, and you want to know the stretches when you’re busy. Shuffled, it’s hopeless: the meeting that overlaps Monday 9:00 could be anywhere in the pile. So you sort the notes by start time. Now you walk through them once, holding one “busy block” in your hand. Each note either starts before your block ends (stretch the block) or after it (put the block down for good and start a new one).
The key insight: once the notes are sorted by start, an interval can only overlap the block you’re holding. Every earlier block ended before something that started later than it, so nothing after can reach back to it.
How it works
We’ll merge a list of closed intervals: [1, 3] covers 1, 3 and everything between. That means [1, 3] and [3, 5] touch at 3, and they merge. (If your intervals are half-open, like meetings where one ending at 3 frees the room for one starting at 3, change <= to < and they stay separate.)
Scroll through the steps and the graphic follows along. You can also press play, step with the arrow keys, or edit the intervals: try one inside another, several identical ones, or a chain of touching ones.
- The intervals arrive in any order. Here
[1, 3]and[3, 5]belong together but sit far apart in the list. - Sort by start. Now every interval that could join a block comes right after it.
- Put the first interval into
merged. The last entry ofmerged, calledlast, is the only one that can still grow. - Take the next interval,
startandend. - Compare
startwithlast[1]. Ifstart <= last[1]they share at least one point.[3, 5]starts exactly where[1, 3]ends: touching, so it counts. - Merge:
last[1] = max(last[1], end).lastgrows to[1, 5]. - Gap:
[9, 12]starts after 5, so[1, 5]is final. Append[9, 12]as the newlast. [10, 11]sits inside[9, 12]. Themaxkeeps the end at 12; writinglast[1] = endwould shrink it to 11.- After the last interval,
mergedholds the answer:[1, 5],[9, 12],[15, 20].
Why it’s correct: after each step, merged is exactly the union of the intervals seen so far, and every entry except last ends before the current start. Since later intervals start even further right, an entry that isn’t last can never be touched again. So when a gap appears, closing last for good is safe.
def merge(intervals):
"""Merge overlapping closed intervals. Touching ones, like [1, 3] and [3, 5], merge too."""
if not intervals:
return []
intervals = sorted(intervals)
merged = [list(intervals[0])]
for start, end in intervals[1:]:
last = merged[-1]
if start <= last[1]:
last[1] = max(last[1], end)
else:
merged.append([start, end])
return merged#include <algorithm>
#include <array>
#include <vector>
using namespace std;
// Merge overlapping closed intervals. Touching ones, like [1, 3] and [3, 5], merge too.
vector<array<int, 2>> merge(vector<array<int, 2>> intervals) {
if (intervals.empty()) return {};
sort(intervals.begin(), intervals.end());
vector<array<int, 2>> merged = {intervals[0]};
for (size_t i = 1; i < intervals.size(); i++) {
auto [start, end] = intervals[i];
auto& last = merged.back();
if (start <= last[1]) {
last[1] = max(last[1], end);
} else {
merged.push_back({start, end});
}
}
return merged;
}import java.util.*;
class Intervals {
// Merge overlapping closed intervals. Touching ones, like [1, 3] and [3, 5], merge too.
static List<int[]> merge(int[][] intervals) {
List<int[]> merged = new ArrayList<>();
if (intervals.length == 0) return merged;
int[][] sorted = intervals.clone();
Arrays.sort(sorted, (a, b) -> Integer.compare(a[0], b[0]));
merged.add(sorted[0].clone());
for (int i = 1; i < sorted.length; i++) {
int start = sorted[i][0], end = sorted[i][1];
int[] last = merged.get(merged.size() - 1);
if (start <= last[1]) {
last[1] = Math.max(last[1], end);
} else {
merged.add(new int[]{start, end});
}
}
return merged;
}
}Why it’s O(n log n)
Sorting takes O(n log n). The sweep looks at each interval once and does O(1) work on it, so it’s O(n). Sorting dominates.
| part | time |
|---|---|
| sort by start | O(n log n) |
| sweep | O(n) |
| total | O(n log n) |
The output can hold up to n intervals, so space is O(n), plus whatever the sort uses. If the input is already sorted, the whole thing is O(n).
Common mistakes
Forgetting the max
An interval nested inside last ends earlier than last does. Assigning its end directly shrinks the block, and a later interval that should merge looks like it’s after a gap.
last[1] = end # ✗ [1, 10] then [2, 3] gives [1, 3]
last[1] = max(last[1], end) # ✓ the end only ever grows
Comparing with the previous input interval
The interval that matters is the merged block, not the one just before in sorted order. With [1, 10], [2, 3], [5, 6], the previous interval [2, 3] ends before 5, but [1, 10] still covers it.
if start <= intervals[i - 1][1]: # ✗ misses long intervals further back
if start <= merged[-1][1]: # ✓ compare with the block you're building
Not deciding what touching means
[1, 3] and [3, 5] merge with <= and stay apart with <. Closed ranges (pages 1–3 and 3–5 share page 3) want <=. Half-open ranges like meeting times (a 1–3 meeting and a 3–5 meeting can use the same room) want <. Settle it before writing the comparison.
Changing the caller’s intervals
In Python, appending the input’s own lists to merged and then editing last[1] also changes the caller’s data. Copy each interval when you append it.
merged.append(interval) # ✗ later edits change the input too
merged.append([start, end]) # ✓ a fresh list
Variations
- Insert an interval. The list is already sorted and non-overlapping, and one new interval arrives. Copy the intervals that end before it starts, then fold every interval that overlaps it into it with
minandmax, then copy the rest. One pass, O(n), no sort. - Meeting rooms (most overlap at once). Turn each meeting into two events,
(start, +1)and(end, -1), sort them, and keep a running sum: its peak is the number of rooms. For half-open meetings, sort an end before a start at the same time, so a room is freed before it’s reused. Or sort by start and keep a min-heap of end times: pop the earliest end if it’s<= start, push the new end; the heap’s largest size is the answer. - Fewest removals so none overlap. This one sorts by end. Keep the interval that ends first, skip everything that starts before it ends, repeat. Finishing early leaves the most room for the rest, so the kept count is the maximum, and the answer is
nminus it. - Intersection of two lists. Given two sorted, non-overlapping lists, use two pointers. The overlap of
aandbis[max(a0, b0), min(a1, b1)]when that’s non-empty. Then advance whichever interval ends first, since it can’t overlap anything else. - Small integer coordinates. When the ends are integers up to about 10⁶, skip sorting: add +1 at each start and −1 just after each end in a difference array, and a prefix sum gives how many intervals cover every point.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
You get a list of meeting times and must find the fewest rooms so that no two overlapping meetings share a room. Which approach fits?
The number of rooms is the most meetings running at one moment, and a sweep over start and end events measures exactly that. Merging tells you how many separate busy stretches there are: three meetings that all overlap merge into one block but need three rooms.
-
2
What does this print?
out = []for s, e in sorted([[3, 5], [1, 3], [6, 7]]):if out and s <= out[-1][1]:out[-1][1] = max(out[-1][1], e)else:out.append([s, e])print(out)Sorted,
[3, 5]starts at 3, where[1, 3]ends, and<=treats touching as overlapping, so they merge into[1, 5].[6, 7]starts after 5, so it stays separate: only a shared point merges, not a gap of one. With<you’d get the second answer. -
3
This merge forgets the
max. What does it print?out = []for s, e in sorted([[1, 10], [2, 3], [4, 5]]):if out and s <= out[-1][1]:out[-1][1] = eelse:out.append([s, e])print(out)[2, 3]is nested in[1, 10], andout[-1][1] = eshrinks the block’s end from 10 to 3. Then[4, 5]starts after 3 and looks like it’s past a gap. Withmax(out[-1][1], e)the answer would be[[1, 10]]. -
4
Meetings are half-open: one ending at 5 frees its room for one starting at 5. What does this print?
meetings = [(1, 5), (5, 10), (2, 6)]events = []for s, e in meetings:events += [(s, 1), (e, -1)]cur = best = 0for _, d in sorted(events):cur += dbest = max(best, cur)print(best)Tuples sort by time, then by the second value, so at time 5 the
-1of(1, 5)comes before the+1of(5, 10): the room is freed before it’s reused. The peak is 2. If starts came first at equal times, the count would briefly reach 3. -
5
A list of intervals is sorted and has no overlaps. You insert one new interval and return the merged list. What’s the best worst-case time?
Binary search finds where the new interval goes in O(log n), but it can swallow almost every other interval, and building the result takes O(n) anyway. One pass (copy the ones before, fold in the overlapping ones, copy the rest) is optimal. Sorting again would be O(n log n) for nothing.