A learning sheet β almost every interval problem starts the same way: sort by one endpoint, then sweep once. Picking which endpoint is the whole game.
An interval is [start, end]. The universal first move is to sort β almost always by start (to merge/detect overlaps) or by end (to greedily fit the most). Once sorted, related intervals become adjacent, and one linear sweep does the work.
Memorize the overlap test and you'll never fumble a condition: [a,b] and [c,d] overlap iff a β€ d and c β€ b. After sorting by start, it simplifies to "the next one starts before the current one ends": next.start β€ cur.end.
O(nΒ²) and fiddly.O(n log n) to sort, then O(n) to sweep.| next | vs current | action | current after |
|---|---|---|---|
| [2,6] | 2 β€ 3 | overlap β extend end | [1,6] |
| [8,10] | 8 > 6 | gap β push [1,6], open new | [8,10] |
| [15,18] | 15 > 10 | gap β push [8,10], open new | [15,18] |
| Ask | If yes β | Pattern |
|---|---|---|
| Combine overlapping intervals, or insert one? | sort by start, sweep & extend | Merge (Β§4.1) |
| Max concurrency? fewest rooms? capacity over time? | +1/β1 events, sort by time | Sweep line (Β§4.2) |
| Intersect two sorted interval lists? | a pointer in each, advance the one ending first | Intersection (Β§4.3) |
| Keep the most non-overlapping (or fewest removals/arrows)? | sort by end, greedily keep | Greedy by end (Β§4.4) |
[1,2] and [2,3] an overlap? It depends on closed/open endpoints β ask; for merging, usually yes (β€), for free-time gaps, usually no.| Words in the prompt | Reach for |
|---|---|
| "merge overlapping", "insert interval", "employee free time" | Merge Β§4.1 |
| "minimum meeting rooms", "max concurrent", "car pooling", "my calendar booking" | Sweep line Β§4.2 |
| "intersection of two interval lists" | Intersection Β§4.3 |
| "non-overlapping intervals", "erase fewest", "minimum arrows", "max events you can attend" | Greedy by end Β§4.4 |
Each: a one-liner, a π invariant, a template, a π€ interview lens. Notice Β§4.1 sorts by start; Β§4.4 sorts by end β that's the crux.
βSort by start; extend the running interval or open a new one at each gap.β
// Merge Intervals
function merge(intervals: number[][]): number[][] {
intervals.sort((a, b) => a[0] - b[0]); // by START
const out: number[][] = [];
for (const [s, e] of intervals) {
const last = out[out.length - 1];
if (last && s <= last[1]) last[1] = Math.max(last[1], e); // overlap β extend
else out.push([s, e]); // gap β new interval
}
return out;
}
// Insert Interval into a sorted, non-overlapping list β three phases
function insert(intervals: number[][], n: number[]): number[][] {
const out: number[][] = []; let i = 0, m = intervals.length;
while (i < m && intervals[i][1] < n[0]) out.push(intervals[i++]); // entirely before
while (i < m && intervals[i][0] <= n[1]) { // overlapping β absorb
n = [Math.min(n[0], intervals[i][0]), Math.max(n[1], intervals[i][1])]; i++;
}
out.push(n);
while (i < m) out.push(intervals[i++]); // entirely after
return out;
}
s β€ last.end overlap test, and clarifying touching/closed endpoints. Β· Escalation: merge β insert interval β "Employee Free Time" (merge everyone's intervals, then report the gaps).βTurn each interval into a +1 at start and a β1 at end; sweep the timeline.β
Setup: concurrency questions ("how many overlap at once?") become a running counter over time-ordered events. The peak counter is the answer.
t frees capacity for one starting at t β otherwise you over-count by one.// Minimum Meeting Rooms β peak concurrency via events
function minMeetingRooms(intervals: number[][]): number {
const events: [number, number][] = [];
for (const [s, e] of intervals) { events.push([s, +1]); events.push([e, -1]); }
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // ties: β1 (end) before +1 (start)
let cur = 0, best = 0;
for (const [, delta] of events) { cur += delta; best = Math.max(best, cur); }
return best;
}
βOverlap = [max(starts), min(ends)]; advance whoever ends first.β
Setup: two interval lists, each sorted and internally non-overlapping. A pointer in each; the one that ends earlier can't intersect anything further, so it moves on. This is the two-sequence two-pointer wearing an interval costume.
A[i] and B[j] are the earliest-ending unprocessed intervals of each list. Their overlap (if any) is [max(starts), min(ends)]. Advancing the one with the smaller end is safe β it can never overlap a later interval of the other list.function intervalIntersection(A: number[][], B: number[][]): number[][] {
const out: number[][] = []; let i = 0, j = 0;
while (i < A.length && j < B.length) {
const lo = Math.max(A[i][0], B[j][0]);
const hi = Math.min(A[i][1], B[j][1]);
if (lo <= hi) out.push([lo, hi]); // non-empty overlap
if (A[i][1] < B[j][1]) i++; else j++; // drop the one ending first
}
return out;
}
[max(start), min(end)] overlap formula and the "advance the smaller end" rule. Β· Escalation: two lists β combine with merge for messier inputs β range/segment problems.βWant the most non-overlapping? Keep the one that ends earliest.β
Setup: to maximize how many intervals you keep (or minimize removals/arrows), sort by end and greedily take each interval that starts after the last kept one ends.
// Non-overlapping Intervals β fewest removals to make the rest disjoint
function eraseOverlapIntervals(intervals: number[][]): number {
intervals.sort((a, b) => a[1] - b[1]); // by END
let end = -Infinity, kept = 0;
for (const [s, e] of intervals) {
if (s >= end) { kept++; end = e; } // compatible β keep it
}
return intervals.length - kept; // the rest must be removed
}
| Pattern | Time | Space | Note |
|---|---|---|---|
| Merge / Greedy / Sweep line | O(n log n) | O(n) | sort dominates; the sweep itself is O(n) |
| Insert into a sorted list | O(n) | O(n) | already sorted β one pass |
| Intersection of two sorted lists | O(m + n) | O(1) | two pointers, inputs pre-sorted |
O(n log n)); everything after is a single linear sweep. If the input is already sorted (insert, intersection), you skip straight to O(n).| Bug | The input that exposes it |
|---|---|
| Forgetting to sort first. | [[2,6],[1,3]] β merging in input order misses the overlap. |
| Sorting by start when greedy needs end. | [[1,100],[2,3],[3,4]] β by-start keeps 1; by-end keeps 2. Wrong key = wrong answer. |
| Sweep line: not processing end before start at equal times. | [[0,1],[1,2]] β over-counts to 2 rooms instead of 1. |
| Mishandling touching endpoints (closed vs open). | [[1,2],[2,3]] β merge or not? Clarify; <= merges, < doesn't. |
Mutating the last reference vs pushing a copy. | Aliasing the input array β later sorts/edits corrupt the output. |
| Applying greedy to a weighted version. | Intervals with values β greedy-by-end gives max count, not max weight (needs DP). |
// 1094 Car Pooling β difference array over positions
function carPooling(trips: number[][], capacity: number): boolean {
const diff: number[] = new Array(1002).fill(0);
for (const [n, from, to] of trips) { diff[from] += n; diff[to] -= n; } // they leave AT 'to' (exclusive)
let load = 0;
for (const d of diff) { load += d; if (load > capacity) return false; }
return true;
}
// 3355 Zero Array Transformation I β coverage count per index must be >= nums[i]
function isZeroArray(nums: number[], queries: number[][]): boolean {
const diff: number[] = new Array(nums.length + 1).fill(0);
for (const [l, r] of queries) { diff[l]++; diff[r + 1]--; }
let cur = 0;
for (let i = 0; i < nums.length; i++) { cur += diff[i]; if (nums[i] > cur) return false; }
return true;
}
// 731 My Calendar II β allow double booking, reject triple
class MyCalendarTwo {
private booked: number[][] = [];
private overlaps: number[][] = []; // regions already booked twice
book(s: number, e: number): boolean {
for (const [a, b] of this.overlaps) if (s < b && a < e) return false; // would be a 3rd booking
for (const [a, b] of this.booked) {
const lo = Math.max(s, a), hi = Math.min(e, b);
if (lo < hi) this.overlaps.push([lo, hi]);
}
this.booked.push([s, e]);
return true;
}
}
// 715 Range Module β sorted disjoint half-open intervals [l, r)
class RangeModule {
private iv: number[][] = [];
addRange(l: number, r: number): void {
const out: number[][] = []; let placed = false;
for (const [a, b] of this.iv) {
if (b < l) out.push([a, b]); // strictly before (touching merges)
else if (a > r) { if (!placed) { out.push([l, r]); placed = true; } out.push([a, b]); }
else { l = Math.min(l, a); r = Math.max(r, b); } // overlaps or touches β absorb
}
if (!placed) out.push([l, r]);
this.iv = out;
}
removeRange(l: number, r: number): void {
const out: number[][] = [];
for (const [a, b] of this.iv) {
if (b <= l || a >= r) out.push([a, b]);
else { if (a < l) out.push([a, l]); if (b > r) out.push([r, b]); } // one interval can split in two
}
this.iv = out;
}
queryRange(l: number, r: number): boolean { return this.iv.some(([a, b]) => a <= l && r <= b); }
}
diff[to] -= n). 3355: each query may skip cells, so the test is exactly nums[i] β€ coverage[i]. 731: check overlaps first; intervals are half-open (s < b && a < e). 715: merge touching ranges (b < l to skip); a query needs ONE interval covering it.Own each pattern and these are in range. Bold = Hard tier.
| Pattern | Representative LeetCode | Tier |
|---|---|---|
| Merge overlapping | 56 Merge Intervals Β· 57 Insert Interval Β· 763 Partition Labels Β· 715 Range Module | Med |
| Sweep line | 253 Meeting Rooms II Β· 1854 Max Population Β· 1094 Car Pooling Β· 3355 Zero Array (difference array) Β· 218 The Skyline Problem | Med β Hard |
| Intersection | 986 Interval List Intersections Β· 436 Find Right Interval Β· 731 My Calendar II | Med |
| Greedy selection | 435 Non-overlapping Intervals Β· 452 Min Arrows Β· 646 Max Length Chain | Med |
Cover the right column (blurred β hover/tap to reveal). Say your answer out loud first.
| Prompt | Answer |
|---|---|
State the overlap condition for [a,b] and [c,d]. | a β€ d and c β€ b. Sorted by start, it reduces to next.start β€ cur.end. |
| When do you sort by start vs by end? | By start to merge / detect overlaps / insert; by end to greedily keep the most non-overlapping. |
| In the sweep line, what's the critical tie-break? | At equal timestamps, process the end (β1) before the start (+1) β else you over-count concurrency by one. |
| Why does merge only compare against the last output interval? | Sorted by start, all earlier outputs end before the tail; only the tail can overlap a newcomer. |
| In two-list intersection, which pointer advances? | The interval that ends first β it can't overlap any later interval of the other list. |
| Why does greedy sort by end (the exchange argument)? | The earliest-ending interval leaves the most room; swapping it into an optimal solution never loses a later pick. |
| When does the greedy break? | When intervals are weighted (maximize total value, not count) β that's DP, not greedy. |
| Car Pooling β what does the difference array store, and why is drop-off exclusive? | diff[from] += n, diff[to] β= n; a running sum gives the load at each stop. Passengers leave at to before anyone boards there. |
| My Calendar II β the two lists? | booked (all accepted) and overlaps (already double-booked). Reject if the new interval hits overlaps; otherwise add its intersections with booked to overlaps. |
merge + insertminMeetingRooms (events, end-before-start)intervalIntersectioneraseOverlapIntervals (sort by end)For each: write "sort by start or end? what's the overlap test?" as a comment before the loop.