Intervals
Interval problems give you pairs [start, end] representing ranges and ask you to merge, insert, or remove them — the key insight is almost always to sort by start or end time first, then sweep linearly.
- Replaces brute-force O(n²) overlap checks with O(n log n) by sorting once then doing one greedy pass
- Four modes: merge overlapping intervals, insert a new interval into a sorted list, remove minimum to make non-overlapping, find free gaps within a window
- Reach for it when the problem involves merging ranges, inserting a range, or minimizing overlapping removals
Quick Revision - Must Do Problems
If short on time, solve only these. They cover ~90% of interval patterns.
| # | Problem | Pattern | Why It's Essential | Time to Solve |
|---|---|---|---|---|
| 2 | Insert Interval [B75-57] | Three-Phase Scan | No sort needed — three distinct phases cover every case | 20 min |
| 3 | Non-overlapping Intervals [B75-435] | Sort by End + Greedy | Sort by end, keep earliest-ending — classic greedy proof | 20 min |
| 4 | Find Available Windows | Merge + Gap Walk | Covers clip + sort + merge + gap inversion — most complete interval problem | 25 min |
Practice Tips
How to Identify an Interval Problem
- Input is a list of
[start, end]pairs representing ranges, meetings, or bookings - Problem asks to merge, count, insert, or minimize removal of overlapping ranges
- Phrases like "meeting rooms", "overlapping intervals", "insert and merge", "remove minimum to make non-overlapping"
Intervals vs. Other Techniques
| If you see... | Use... | Not... |
|---|---|---|
| Merge overlapping ranges | Sort by start, sweep | Brute force O(n²) comparison |
| Insert into sorted interval list | Three-phase scan | Re-sorting the full list |
| Minimum removals for non-overlap | Sort by end, greedy keep | Sort by start (wrong greedy choice) |
Interview Approach
- Clarify: does "touching" count as overlapping? (
[1,4],[4,5]— usually yes) - Decide sort key: sort by start for merge/insert style, sort by end for greedy removal
- For merge: one pass with a result list — extend the last interval's end on overlap
- For insert: three-phase scan — before / overlapping / after — no re-sorting needed
- For minimum removal: sort by end; keep intervals that end earliest; count every skip as a removal
4 Main Patterns
Pattern 1: Sort by Start + Merge
// Sort by start time; if current starts after last ends → add; else extend last end
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> result = new ArrayList<>();
for (int[] iv : intervals) {
if (result.isEmpty() || result.get(result.size()-1)[1] < iv[0]) {
result.add(iv); // no overlap
} else {
result.get(result.size()-1)[1] = Math.max(result.get(result.size()-1)[1], iv[1]); // merge
}
}
Pattern 2: Three-Phase Scan (Insert into Sorted)
// Phase 1: intervals ending before new starts → add as-is
// Phase 2: intervals overlapping with new → expand new interval's bounds
// Phase 3: remaining intervals → add as-is
int i = 0, n = intervals.length;
while (i < n && intervals[i][1] < newInterval[0]) {
result.add(intervals[i]);
i++;
}
while (i < n && intervals[i][0] <= newInterval[1]) {
int curStart = intervals[i][0];
int curEnd = intervals[i][1];
newInterval[0] = Math.min(newInterval[0], curStart);
newInterval[1] = Math.max(newInterval[1], curEnd);
i++;
}
result.add(newInterval);
while (i < n) {
result.add(intervals[i]);
i++;
}
Pattern 3: Sort by End + Greedy Keep
// Sort by end; always keep the interval that ends earliest
// When two overlap, remove the current one (it ends later by definition after sorting)
Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
int removed = 0, lastKeptEnd = Integer.MIN_VALUE;
for (int[] iv : intervals) {
int curStart = iv[0];
int curEnd = iv[1];
if (curStart >= lastKeptEnd) {
lastKeptEnd = curEnd; // no overlap — keep, advance end
}
else {
removed++; // overlap — remove current (ends later)
}
}
Pattern 4: Merge + Gap Walk
// Clip → sort by start → merge → walk gaps between merged intervals
// Step 1: clip bookings to window
List<int[]> inWindow = new ArrayList<>();
for (int[] booking : booked) {
int clippedStart = Math.max(booking[0], winStart);
int clippedEnd = Math.min(booking[1], winEnd);
if (clippedStart < clippedEnd) {
inWindow.add(new int[]{clippedStart, clippedEnd});
}
}
inWindow.sort((a, b) -> a[0] - b[0]);
// Step 2: merge overlapping bookings (same as Pattern 1)
List<int[]> merged = new ArrayList<>();
int[] cur = inWindow.get(0).clone();
for (int i = 1; i < inWindow.size(); i++) {
int[] next = inWindow.get(i);
if (next[0] <= cur[1]) {
cur[1] = Math.max(cur[1], next[1]);
} else {
merged.add(cur);
cur = next.clone();
}
}
merged.add(cur);
// Step 3: walk gaps — every space between merged bookings is free
int cursor = winStart;
for (int[] booking : merged) {
if (booking[0] > cursor) {
result.add(new int[]{cursor, booking[0]}); // gap before this booking
}
cursor = Math.max(cursor, booking[1]);
}
if (cursor < winEnd) {
result.add(new int[]{cursor, winEnd}); // trailing gap
}
General Templates
Overlap Check Reference
No overlap (a ends before b starts): a[1] < b[0]
Overlap (touching counts): a[1] >= b[0] (i.e., NOT a[1] < b[0])
Problems in this guide
| # | Title |
|---|---|
| 1 | Merge Intervals |
| 2 | Insert Interval ⭐ |
| 3 | Non-overlapping Intervals ⭐ |
| 4 | Find Available Windows ⭐ |
Problem 1: Merge Intervals [B75-56]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 15 min | Sort by Start + Merge |
Merge all overlapping intervals and return the non-overlapping result.
Example:
Input: [[1,3],[2,6],[8,10],[15,18]] → [[1,6],[8,10],[15,18]]
Input: [[1,4],[4,5]] → [[1,5]]
Key Insight:
- Sort by start time. For each interval: if it starts after the last merged interval ends, there is no overlap — add it. Otherwise, extend the last interval's end to the maximum of both ends. Touching intervals (
[1,4],[4,5]) do overlap — use<not<=in the non-overlap check. - Problem 4 (Find Available Windows) contains this exact pattern as its Step 2. If you practice Problem 4, you have already solved this.
Solution:
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> result = new ArrayList<>();
for (int[] iv : intervals) {
if (result.isEmpty() || result.get(result.size()-1)[1] < iv[0]) {
result.add(iv);
}
else {
result.get(result.size()-1)[1] = Math.max(result.get(result.size()-1)[1], iv[1]);
}
}
return result.toArray(new int[0][]);
}
Complexity:
| Time | O(n log n) — sort dominates |
| Space | O(n) — result list |
Problem 2: Insert Interval ⭐ MUST DO [B75-57]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20 min | Three-Phase Scan |
Given a sorted list of non-overlapping intervals, insert a new interval and merge if necessary.
Example:
Input: intervals = [[1,3],[6,9]], newInterval = [2,5] → [[1,5],[6,9]]
Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]
Key Insight:
- Three phases: (1) add all intervals that end before the new one starts — no overlap, (2) merge all intervals that overlap with the new one by expanding its boundaries, (3) add all remaining intervals. No sorting needed since input is already sorted.
Solution:
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();
int i = 0, n = intervals.length;
while (i < n && intervals[i][1] < newInterval[0]) {
result.add(intervals[i]); // phase 1: before new
i++;
}
while (i < n && intervals[i][0] <= newInterval[1]) { // phase 2: overlapping
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.add(newInterval);
while (i < n) {
result.add(intervals[i]); // phase 3: after new
i++;
}
return result.toArray(new int[0][]);
}
Complexity:
| Time | O(n) — single pass |
| Space | O(n) — result list |
Problem 3: Non-overlapping Intervals ⭐ MUST DO [B75-435]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20 min | Sort by End + Greedy |
Return the minimum number of intervals to remove so the rest are non-overlapping.
Example:
Input: [[1,2],[2,3],[3,4],[1,3]] → 1 (remove [1,3])
Input: [[1,2],[1,2],[1,2]] → 2
Key Insight:
- Sort by end time. Greedy: always keep the interval that ends earliest — it leaves the most room for future intervals. When two intervals overlap, remove the one with the later end time (which is always the current one, since we sorted by end). Track the end of the last kept interval.
Solution:
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[1] - b[1]); // sort by end time
int removed = 0, end = Integer.MIN_VALUE;
for (int[] iv : intervals) {
if (iv[0] >= end) {
end = iv[1]; // no overlap — keep this interval
}
else {
removed++; // overlap — remove current (it ends later)
}
}
return removed;
}
Complexity:
| Time | O(n log n) |
| Space | O(1) |
Problem 4: Find Available Windows ⭐ MUST DO
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 25 min | Merge + Gap Walk |
Given a list of booked intervals and a search window [winStart, winEnd), return all free time gaps within the window.
Example:
Input: booked = [[1,3],[5,8],[6,10]], winStart = 0, winEnd = 12
clip → [[1,3],[5,8],[6,10]] (all within window)
sort → [[1,3],[5,8],[6,10]]
merge → [[1,3],[5,10]]
gaps: [0,1], [3,5], [10,12]
Output: [[0,1],[3,5],[10,12]]
Input: booked = [[2,6]], winStart = 0, winEnd = 10
Output: [[0,2],[6,10]]
Key Insight:
- Three-step pipeline that extends Merge Intervals (Problem 1): (1) clip all bookings to the search window — anything outside is irrelevant, (2) sort by start and merge overlaps exactly like Problem 1, (3) invert the merged list by walking gaps — every space between consecutive merged bookings is free time, plus any leading or trailing space. Practicing this problem covers the full Sort+Merge template plus the gap-walk pattern, making Problem 1 redundant as a standalone must-do.
Solution:
public List<int[]> findAvailableWindows(int[][] booked, int winStart, int winEnd) {
List<int[]> result = new ArrayList<>();
if (booked == null || booked.length == 0) {
result.add(new int[]{winStart, winEnd});
return result;
}
// Step 1: clip bookings to window
List<int[]> inWindow = new ArrayList<>();
for (int[] booking : booked) {
int clippedStart = Math.max(booking[0], winStart);
int clippedEnd = Math.min(booking[1], winEnd);
if (clippedStart < clippedEnd) {
inWindow.add(new int[]{clippedStart, clippedEnd});
}
}
if (inWindow.isEmpty()) {
result.add(new int[]{winStart, winEnd});
return result;
}
inWindow.sort((a, b) -> a[0] - b[0]);
// Step 2: merge overlapping bookings
List<int[]> merged = new ArrayList<>();
int[] cur = inWindow.get(0).clone();
for (int i = 1; i < inWindow.size(); i++) {
int[] next = inWindow.get(i);
if (next[0] <= cur[1]) {
cur[1] = Math.max(cur[1], next[1]); // extend current
} else {
merged.add(cur);
cur = next.clone();
}
}
merged.add(cur);
// Step 3: walk gaps between merged bookings
int cursor = winStart;
for (int[] booking : merged) {
if (booking[0] > cursor) {
result.add(new int[]{cursor, booking[0]}); // gap before this booking
}
cursor = Math.max(cursor, booking[1]);
}
if (cursor < winEnd) {
result.add(new int[]{cursor, winEnd}); // trailing gap
}
return result;
}
Complexity:
| Time | O(n log n) — sort dominates |
| Space | O(n) — clipped + merged lists |
Quick Reference Table
| # | Problem | Pattern | Key Technique | Time |
|---|---|---|---|---|
| 1 | Merge Intervals [B75-56] | Sort+Merge | Sort by start; extend last.end on overlap | O(n log n) |
| 2 | Insert Interval ⭐ [B75-57] | Three-Phase | Before / merge / after — no sort needed | O(n) |
| 3 | Non-overlapping ⭐ [B75-435] | Greedy | Sort by end; keep earliest-ending | O(n log n) |
| 4 | Find Available Windows ⭐ | Merge + Gap Walk | Clip → sort → merge → invert gaps | O(n log n) |
When to Use Each Pattern
Sort by Start + Merge
- Input is unsorted intervals; need to collapse all overlaps into a flat non-overlapping list
- Check
last.end < current.startfor no overlap (strict<— touching intervals do overlap) - Extend
last.end = max(last.end, current.end)whenever overlap is detected
Three-Phase Scan (Insert into Sorted)
- Input is already sorted and non-overlapping; inserting exactly one new interval
- Phase 1 boundary:
intervals[i][1] < newInterval[0]— interval ends before new one starts - Phase 2 boundary:
intervals[i][0] <= newInterval[1]— interval starts before new one ends - O(n) — no sorting needed because input is pre-sorted
Sort by End + Greedy Keep
- Minimization problem: remove the fewest intervals to eliminate all overlaps
- Sort by end gives the greedy choice property (earliest-deadline-first)
- Only update
end = iv[1]when you KEEP an interval, not when you remove it
Merge + Gap Walk
- Problem gives booked/occupied intervals; asks for free/available time within a window
- Clip first — any booking outside the window is irrelevant and causes off-by-one bugs if kept
- After merging, walk gaps: track
cursor = winStart, advance it tobooking[1]after each merged interval; any space beforebooking[0]is a free gap
Common Mistakes to Avoid
Overlap Condition Mistakes
- Wrong no-overlap check in merge — use strict
<(last.end < current.start); using<=incorrectly treats touching intervals like[1,4],[4,5]as non-overlapping when they should be merged - Insert interval: wrong phase 2 condition — use
intervals[i][0] <= newInterval[1](not strict<); strict<would miss touching intervals that need merging
Greedy Sort Key Mistakes
- Non-overlapping: sorting by start instead of end — sorting by start does not give the greedy choice property; you must sort by end time for the earliest-deadline-first strategy to work correctly
- Non-overlapping: updating
endon removal — only updateend = iv[1]in the keep branch (if), never in the removal branch (else); updating on removal uses a later end and breaks the tracking
Gap Walk Mistakes
- Skipping the clip step — bookings that extend outside the window must be clipped before sorting and merging; keeping them causes gaps to be missed or boundaries to be wrong
- Forgetting the trailing gap — after walking all merged bookings, check
if (cursor < winEnd)and add the final gap; it is easy to emit every internal gap and miss the one after the last booking - Updating
cursoron gap instead of booking end — always advancecursor = Math.max(cursor, booking[1])after processing a booking, not when emitting a gap; themaxguards against bookings fully contained within a previous one