Intervals — merge, insert, schedule
📖 Walk me through it — plain English
An interval is just a pair of numbers [start, end] that marks a stretch on a line — think "this meeting runs from 1 to 3" or "this highlighter covers pages 2 through 6." Two intervals overlap when they share any point: [1,3] and [2,6] overlap because 2 falls inside 1-to-3. The job in merge intervals is to glue every group of overlapping intervals into one bigger interval, so a tangle of ranges becomes a clean, non-overlapping list.
Here's the one trick that makes it easy: sort the intervals by their start value first. Once they're lined up left-to-right by where they begin, an interval can only ever overlap with the one immediately before it — there's no way for it to reach back past that. So we keep an output list, and for each interval we ask one question: does my start fall at or before the end of the last interval I kept? If yes, they touch — stretch that last interval's end to cover us. If no, there's a gap — start a fresh interval.
Analogy: imagine merging overlapping highlighter strokes on a page. You scan left to right. If your next stroke begins before the previous one ended, it's one continuous mark, so you just extend the previous mark's right edge. If it begins past the previous one, you lift the pen and start a brand-new stroke.
Let's trace merge([[1,3], [2,6], [8,10]]). They're already sorted by start. We build out, the result list, one interval at a time.
Why it works: after sorting, anything that overlaps a given interval must sit right next to it, so checking only "the last interval I kept" is enough — we never have to look back further. Why the speed: the sort is the slow part at O(n log n) (n = number of intervals), and the single left-to-right pass is just O(n) on top, so the sort dominates the total. We use O(n) extra space for the output list — and building a fresh list (instead of editing the input while looping over it) is what keeps the classic "mutating-while-iterating" bug from biting you.
An interval is just a [start, end] pair, and almost every interval problem is secretly the same question: "which of these ranges touch each other?" The universal first move is sort by start time — once sorted, any overlap can only be with a neighbor, so a single left-to-right pass settles everything. The overlap test is one line: [a,b] and [c,d] overlap when a <= d and c <= b — i.e. they miss only if one ends before the other starts.
The vocabulary, defined once
Before the patterns, pin down every word so nothing later is a mystery. Each term here is something interviewers will say out loud, so being able to name them back is half the battle.
- Interval — an ordered pair [start, end] with start <= end, naming a continuous stretch on a number line (a time window, a page range, a segment). The two numbers are its endpoints; the left one is the start (or "open"/"arrival") and the right one is the end (or "close"/"departure").
- Overlap — two intervals overlap when they share at least one point. Formally, [a,b] and [c,d] overlap exactly when a <= d and c <= b. The cleaner way to remember it: they do not overlap only if one finishes strictly before the other starts (b < c or d < a); negate that and you get the overlap condition.
- Sort-by-start — ordering the whole list ascending by the start value ([1,9],[2,3],[8,10] sorts to [1,9],[2,3],[8,10]). This is the setup move for merge and insert: it guarantees that as you scan left to right, every interval begins no earlier than the one before, so any overlap must be with what you most recently kept.
- The sweep / merge technique — "sweep" just means walking through the sorted items once, left to right, carrying a small amount of running state (here, the last interval you kept). You never go backwards. That single forward pass is what turns a quadratic "compare everything to everything" idea into a linear scan.
- Sweep line / events — a generalization of the sweep where you don't walk the intervals themselves but a sorted list of events on the timeline: a +1 event at each start, a -1 event at each end. Processing events in time order lets you track "how many intervals are active right now" without ever comparing pairs.
- Min-heap of ends — a priority queue that always hands back its smallest element first. For scheduling, we keep the end times of meetings currently using a room in a min-heap so the earliest-finishing room is always one peek away. ("Min-resources" / "min-rooms" is the pattern of finding the fewest concurrent resources needed to cover everything — see below.)
On-ramp: sort first, then sweep
If you remember only five words from this lesson, make them "sort first, then sweep." Almost every interval task collapses to those two phases. Sort imposes an order so neighbors are the only things that can interact. Sweep walks that order once, keeping a tiny bit of state, and decides each item by comparing it to what came just before. The only thing that changes between problems is (a) which key you sort on (start vs. end) and (b) what state you carry during the sweep (the last kept interval, a running count, or a heap of end times). Get those two choices right and the code writes itself.
- "Merge overlapping ___"
- "Insert a range, then merge"
- "Meetings / rooms / calendar"
- "Min resources to cover all ___"
- Anything with [start, end] pairs
- Merge — sort by start, fold overlaps
- Insert — before / overlap / after buckets
- Non-overlapping — greedy by end time
- Min rooms — sweep line / min-heap of ends
The overlap test, spelled out
In the merge code below you'll see the condition written as s <= out[-1][1] — "my start is at or before the last kept end." That's a special case of the general overlap test that works because we sorted by start: once sorted, the new interval's start is already >= the previous start, so we only need to check the other half of the condition. The full, order-independent test is a <= d and c <= b for [a,b] vs [c,d]. Read it as two simultaneous demands: the first interval must start no later than the second ends, and the second must start no later than the first ends. If either demand fails, there's a clean gap between them.
There are infinitely many ways two ranges can overlap, but only two ways they can miss: the first ends before the second starts (b < c), or the second ends before the first starts (d < a). "Overlap" is simply "not a miss," so by De Morgan's law it is not(b < c or d < a) = b >= c and d >= a = a <= d and c <= b. When endpoints touching counts as overlap ([1,2] & [2,3]), use <= as shown; when touching should not count, switch the merge guard to strict <. Always confirm which convention the problem wants.
def merge(intervals):
intervals.sort(key=lambda x: x[0]) # sort by START
out = []
for s, e in intervals:
if out and s <= out[-1][1]: # overlaps prev
out[-1][1] = max(out[-1][1], e)
else:
out.append([s, e])
return out
function merge(iv: number[][]): number[][] {
iv.sort((a, b) => a[0] - b[0]);
const out: number[][] = [];
for (const [s, e] of iv) {
const last = out[out.length - 1];
if (last && s <= last[1]) last[1] = Math.max(last[1], e);
else out.push([s, e]);
}
return out;
}
One subtlety the trace above quietly handled: when we extend the last interval we take max(out[-1][1], e), not just e. That matters for a swallowed interval like [1,9] followed by [2,3] — the new end 3 is smaller, so blindly writing e would shrink a range that already covered it. max keeps the farther-right edge and absorbs the smaller interval entirely.
Insert into a sorted interval list
The insert variant gives you an already-sorted, non-overlapping list plus one new interval, and asks for the merged result. Because the list is already sorted you can skip the sort entirely and walk it in three phases — think of them as three buckets relative to the new interval [ns, ne]:
- Before — every interval that ends strictly before the new one starts (end < ns). These can't touch it; copy them over unchanged.
- Overlap — every interval that touches the new one (start <= ne and end >= ns). Don't emit these individually; instead grow [ns, ne] to swallow them by taking ns = min(ns, start) and ne = max(ne, end). After the overlap run, push the single grown interval once.
- After — every interval that starts strictly after the new one ends (start > ne). Copy the rest over unchanged.
def insert(intervals, new):
ns, ne = new
out, i, n = [], 0, len(intervals)
# 1) before: ends before new starts
while i < n and intervals[i][1] < ns:
out.append(intervals[i]); i += 1
# 2) overlap: grow [ns, ne] to swallow them
while i < n and intervals[i][0] <= ne:
ns = min(ns, intervals[i][0])
ne = max(ne, intervals[i][1]); i += 1
out.append([ns, ne])
# 3) after: copy the rest
while i < n:
out.append(intervals[i]); i += 1
return out
function insert(iv: number[][], nw: number[]): number[][] {
let [ns, ne] = nw;
const out: number[][] = [];
let i = 0, n = iv.length;
while (i < n && iv[i][1] < ns) out.push(iv[i++]); // before
while (i < n && iv[i][0] <= ne) { // overlap
ns = Math.min(ns, iv[i][0]);
ne = Math.max(ne, iv[i][1]); i++;
}
out.push([ns, ne]);
while (i < n) out.push(iv[i++]); // after
return out;
}
Minimum meeting rooms (min-resources)
The min-rooms / min-resources pattern asks: given a pile of meetings, what is the fewest number of rooms (servers, lanes, platforms) you need so that no two overlapping meetings share one? The answer is exactly the maximum number of meetings that are ever in progress at the same instant — the peak concurrency. There are two clean ways to compute it, and both reduce to "sort, then sweep."
- Min-heap of end times. Sort meetings by start. Walk them in order, keeping a min-heap of the end times of meetings currently occupying a room. For each new meeting, if the earliest-ending room (the heap's smallest end) finishes at or before this meeting's start, that room is free — pop it and reuse it. Either way, push the new meeting's end. The heap's largest size reached is the room count. Cost: O(n log n).
- Sweep line of events. Don't track rooms at all — track the timeline (this is the box just below). Emit +1 at every start and -1 at every end, sort events by time, sweep, and report the peak running sum.
import heapq
def min_rooms(meetings):
meetings.sort(key=lambda x: x[0]) # sort by START
heap = [] # end times of busy rooms
for s, e in meetings:
if heap and heap[0] <= s: # earliest room is free
heapq.heappop(heap)
heapq.heappush(heap, e) # occupy a room until e
return len(heap) # peak concurrency = rooms
// MinHeap is a standard min-priority-queue (peek = smallest)
function minRooms(m: number[][]): number {
m.sort((a, b) => a[0] - b[0]); // sort by START
const heap = new MinHeap(); // end times of busy rooms
for (const [s, e] of m) {
if (heap.size && heap.peek() <= s) heap.pop(); // free a room
heap.push(e); // occupy until e
}
return heap.size; // peak concurrency = rooms
}
For "minimum meeting rooms," don't track rooms — track the timeline. Emit a +1 event at every start and a -1 at every end, sort all events by time, and sweep: the running sum is meetings in progress, and its peak is the answer. The same trick solves "max concurrent" anything — calls, downloads, trains in a station.
Recognition signals & pitfalls
In an interview the hard part is rarely the code — it's noticing this is an interval problem and choosing the right sort key. Use these tells, then sidestep the classic traps.
- The input is a list of [start, end] pairs (times, ranges, segments).
- The ask mentions "overlap," "merge," "conflict," "fit," or "rooms/resources."
- It's about scheduling or how many at once — peak concurrency.
- Removing the fewest items to make things non-overlapping (greedy by end).
- Sorting by the wrong key. Merge/insert sort by START; non-overlapping & activity-selection sort by END. Mixing them silently gives wrong answers.
- Boundary touching. Does [1,2] & [2,3] count as overlap? Decide <= vs < up front — it flips off-by-one results.
- Forgetting max on the end. A swallowed interval like [2,3] inside [1,9] shrinks the range if you assign e instead of max(...).
- Mutating while iterating. Build a fresh output list; editing the input as you loop is the classic bug.
- Sort dominates: O(n log n) time, O(n) output.
- Clarify whether touching endpoints count as overlap ([1,2] & [2,3]) — ask before coding.
- Merging sorts by START; non-overlapping / scheduling sorts by END. Don't mix them up.
- Build a fresh output list — mutating the input while iterating is the classic bug.
Go deeper (optional): for the greedy "non-overlapping intervals" / activity-selection cousin (keep the most meetings, or remove the fewest to de-conflict), the move flips to sort by END and greedily keep each interval whose start is past the last kept end — see the greedy lesson for why earliest-finish-first is optimal. LeetCode's "Merge Intervals," "Insert Interval," "Non-overlapping Intervals," and "Meeting Rooms II" are the canonical four.
Implement the sort-then-sweep merge — run it live: