πŸ“… Intervals

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.

🧠 1 · How to think (and invent it)

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.

Watch it get invented β€” Merge Intervals

  1. Start brute. Compare every pair, union overlaps β†’ O(nΒ²) and fiddly.
  2. The leap. Sort by start. Now overlapping intervals sit next to each other. Carry a running "current merged" interval; if the next one starts within it, stretch the end; otherwise the current one is closed β€” push it and open a new one.
  3. Why sorting makes it safe. Once an interval doesn't overlap the running merge, no later interval can either (they all start even later) β€” so the running merge is final the moment a gap appears.
  4. Payoff. O(n log n) to sort, then O(n) to sweep.
[1,3] & [2,6] overlap β†’ [1,6] [8,10] [15,18]
Sorted by start, overlaps are neighbors; the sweep extends the running interval or starts a fresh one at each gap.
nextvs currentactioncurrent after
[2,6]2 ≀ 3overlap β†’ extend end[1,6]
[8,10]8 > 6gap β†’ push [1,6], open new[8,10]
[15,18]15 > 10gap β†’ push [8,10], open new[15,18]

🎯 2 · Attack plan

The one decision that matters Sort by START when you merge, detect overlaps, or insert. Sort by END when you greedily want the most non-overlapping intervals (the earliest-ending one leaves the most room). Get this wrong and an otherwise-correct sweep gives the wrong answer.
AskIf yes β†’Pattern
Combine overlapping intervals, or insert one?sort by start, sweep & extendMerge (Β§4.1)
Max concurrency? fewest rooms? capacity over time?+1/βˆ’1 events, sort by timeSweep line (Β§4.2)
Intersect two sorted interval lists?a pointer in each, advance the one ending firstIntersection (Β§4.3)
Keep the most non-overlapping (or fewest removals/arrows)?sort by end, greedily keepGreedy by end (Β§4.4)
Anti-patterns & clarifications Β· Touching = overlap? Is [1,2] and [2,3] an overlap? It depends on closed/open endpoints β€” ask; for merging, usually yes (≀), for free-time gaps, usually no.
Β· Weighted interval scheduling (max total weight of non-overlapping) is not greedy β€” it's DP. Greedy-by-end only maximizes the count.

πŸ”Ž 3 Β· Recognition map

Words in the promptReach 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

🧩 4 · The four core patterns

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.

4.1 Β· Merge overlapping (sort by start)

β€œSort by start; extend the running interval or open a new one at each gap.”

πŸ”‘ Invariant β€” why it's safe After sorting by start, the last interval in the output is the only one a new interval could possibly overlap (all earlier outputs end before it and start before the newcomer). So a single comparison against the tail decides extend-vs-push, and closed intervals are never reopened.
// 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;
}
🎀 Interview lens Probed: sort-by-start, the 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).

4.2 Β· Sweep line β€” +1 / βˆ’1 events

β€œ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.

peak = 2 rooms +1+1 βˆ’1βˆ’1
The running height is the number of intervals open at that instant; its maximum is the min rooms / peak load.
πŸ”‘ Invariant β€” why it's safe Sorting events by time and accumulating Β±1 means the counter, at any moment, equals the number of intervals currently open. At a shared timestamp, process end (βˆ’1) before start (+1) so an interval ending at 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;
}
🎀 Interview lens Probed: the end-before-start tie-break and why; the running max. Β· Escalation: meeting rooms β†’ "Car Pooling" (capacity check) β†’ "My Calendar I/II/III" (allow up to k overlaps) β†’ the Skyline problem.

4.3 Β· Intersection of two sorted lists

β€œ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.

πŸ”‘ Invariant β€” why it's safe At each step, 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;
}
🎀 Interview lens Probed: the [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.

4.4 Β· Greedy by end β€” fit the most

β€œ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.

one long interval blocks everything earliest-ending picks fit 3 βœ“
Sorting by end, then taking each compatible interval, leaves the maximum room for the rest.
πŸ”‘ Invariant β€” the exchange argument Among all compatible choices, the interval that ends earliest leaves the most room for what follows. So greedily picking it is never worse than any other choice β€” you can always swap an optimal solution's first pick for the earliest-ending one without losing any later picks. (This holds for unweighted count only.)
// 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
}
🎀 Interview lens Probed: why sort by end (the exchange argument), and that it maximizes count, not weight. Β· Escalation: non-overlapping β†’ "Minimum Arrows to Burst Balloons" β†’ "Maximum Number of Events That Can Be Attended"; and "now it's weighted" β†’ DP.

⏱️ 5 · Complexity

PatternTimeSpaceNote
Merge / Greedy / Sweep lineO(n log n)O(n)sort dominates; the sweep itself is O(n)
Insert into a sorted listO(n)O(n)already sorted β†’ one pass
Intersection of two sorted listsO(m + n)O(1)two pointers, inputs pre-sorted
The cost story Sorting is the price of admission (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).

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe 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).
πŸ”— This pattern, elsewhere "Sort, then sweep" is the meta-move from sorting.html in its purest form. Min meeting rooms has a twin solution with a heap of end times β€” sweep-line and heap are two views of the same idea. Interval intersection is the two-sequence two-pointer. Greedy-by-end is classic activity selection; the weighted version becomes DP.
βž• Difference arrays & interval sets β€” Car Pooling (1094) Β· Zero Array I (3355) Β· My Calendar II (731) Β· Range Module (715)
Two cheap sweep-line tools: a difference array (1094, 3355) and a sorted list of disjoint intervals (715, 731).
// 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); }
}
⚠️ Traps 1094: drop-off is exclusive (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.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Merge overlapping56 Merge Intervals Β· 57 Insert Interval Β· 763 Partition Labels Β· 715 Range ModuleMed
Sweep line253 Meeting Rooms II Β· 1854 Max Population Β· 1094 Car Pooling Β· 3355 Zero Array (difference array) Β· 218 The Skyline ProblemMed β†’ Hard
Intersection986 Interval List Intersections Β· 436 Find Right Interval Β· 731 My Calendar IIMed
Greedy selection435 Non-overlapping Intervals Β· 452 Min Arrows Β· 646 Max Length ChainMed

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
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.

Write these cold (no peeking)

For each: write "sort by start or end? what's the overlap test?" as a comment before the loop.