Pattern family: Array / Interval Scheduling / Greedy
Difficulty range: Easy β Hard
Language: Java
- How to identify this pattern β start here
- What is this pattern?
- Core rules
- 2-Question framework
- Variants table
- Universal Java template
- Quick reference cheatsheet
- Common mistakes
- Complexity summary
| If you see this... | It means |
|---|---|
| "merge overlapping intervals" | sort + linear scan merge |
| "insert interval into sorted list" | 3-phase insert + merge |
| "find intersection of two interval lists" | two-pointer intersection |
| "minimum meeting rooms / conference rooms" | sort + min-heap or two-pointer on sorted start/end |
| "maximum CPU load / maximum overlap at any point" | sort + min-heap |
| "employee free time / find gaps" | merge all + find gaps |
| "remove minimum intervals to make non-overlapping" | greedy, keep earliest end |
| "minimum arrows to burst balloons" | greedy, shoot at earliest end |
| "check if any two intervals overlap" | sort + check adjacent |
| "total covered length / union of intervals" | merge + sum lengths |
Brute force: for every pair (i, j) check if they overlap β O(nΒ²).
β Interviewer says "O(n log n)" β cannot check all pairs.
β Key insight: if you SORT by start time, you only need to check
each interval against the PREVIOUS merged interval β O(n) scan.
β Total: O(n log n) sort + O(n) scan = O(n log n).
Signal 1 β Input is a list of intervals [start, end]
Any problem where data is given as pairs representing ranges. The first step is almost always: sort by start time.
Signal 2 β "Overlapping" or "non-overlapping" in the problem statement
Two intervals
[a,b]and[c,d]overlap ifa <= d && c <= b. This is the single overlap check you need to memorise.
Signal 3 β "Minimum rooms / resources needed at the same time"
This asks for the maximum number of simultaneously active intervals. Pattern: sort starts and ends separately, use two-pointer or min-heap.
Signal 4 β "Find free time / gaps between intervals"
Merge all intervals first, then scan for gaps between consecutive merged intervals.
Signal 5 β "Remove minimum intervals" or "minimum arrows"
Greedy: sort by end time, always keep/shoot at the earliest ending interval. This maximises the number of intervals handled by one action.
Problem involves a list of intervals [start, end]?
β
βΌ
What are you asked to do?
β
βββ MERGE overlapping β sort by start, linear scan merge
β
βββ INSERT new interval β 3-phase: add before, merge overlap, add after
β
βββ INTERSECTION of two lists β two-pointer, advance the one that ends first
β
βββ CHECK any overlap exists β sort by start, check adjacent ends
β
βββ MIN ROOMS / MAX OVERLAP at any point
β βββ Heap approach β min-heap of end times, size = rooms needed
β βββ Two-pointer β sort starts[], sort ends[] separately
β
βββ FREE TIME / GAPS β merge all intervals, find gaps between consecutive
β
βββ REMOVE MIN to make non-overlapping β sort by end, greedy keep earliest end
β
βββ MIN ARROWS / MIN POINTS to cover all β sort by end, greedy shoot at end
Core idea: Sort intervals by start time. After sorting, any overlapping intervals are always adjacent. You only need to compare each interval with the last merged result.
Two intervals [a, b] and [c, d] (a β€ c after sorting):
Overlap condition: c <= b (new start β€ current end)
Merge result: [a, max(b, d)]
No overlap: b < c β emit [a,b], start fresh with [c,d]
Visual:
Before sort: [3,5], [1,4], [6,8], [2,3]
After sort: [1,4], [2,3], [3,5], [6,8]
Scan:
curr=[1,4], next=[2,3]: 2<=4 β merge β [1,4] (3 absorbed)
curr=[1,4], next=[3,5]: 3<=4 β merge β [1,5]
curr=[1,5], next=[6,8]: 6>5 β emit [1,5], start [6,8]
Result: [[1,5],[6,8]]
Why sorting works:
After sorting by start, if interval i doesn't overlap with the current merged interval, no future interval (with start β₯ start[i]) can overlap with it either. So you never need to look back.
Rule 1 β Always sort by start time first (unless input is already sorted)
// WRONG β without sorting, non-adjacent intervals may overlap
for (int[] interval : intervals) { ... } // misses [1,4] and [3,5] if not adjacent
// CORRECT
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // sort by start
// Now overlapping intervals are always adjacent β single pass worksRule 2 β Overlap condition: newStart <= currentEnd (not strictly less than)
// WRONG β misses touching intervals like [1,3] and [3,5]
if (intervals[i][0] < end) { merge; } // [3,5] would not merge with [1,3]
// CORRECT β touching intervals (sharing an endpoint) ARE overlapping
if (intervals[i][0] <= end) { merge; }
// [1,3] and [3,5]: 3 <= 3 β merge β [1,5] βRule 3 β For "min rooms" use min-heap of end times, NOT start times
// WRONG β heap of start times tells you nothing about when rooms free up
PriorityQueue<Integer> heap = new PriorityQueue<>();
heap.offer(intervals[i][0]); // meaningless
// CORRECT β heap of END times: peek = earliest room that becomes free
PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // sort by start
for (int[] interval : intervals) {
if (!heap.isEmpty() && heap.peek() <= interval[0]) {
heap.poll(); // room is free β reuse it
}
heap.offer(interval[1]); // assign room, track end time
}
// heap.size() = minimum rooms needed| Answer | Variant |
|---|---|
| Combine overlapping into fewer intervals | Merge Intervals |
| Add one new interval to sorted list | Insert Interval |
| Find common portions of two lists | Interval Intersection |
| Count how many overlap at any moment | Min Rooms / Max CPU Load |
| Find time slots with no intervals | Employee Free Time |
| Remove fewest to eliminate all overlaps | Non-overlapping Intervals |
| Minimum shots/points to cover all | Minimum Arrows |
| Technique | When to use | Key operation |
|---|---|---|
| Sort + linear scan | Merge, detect overlap, count removals | curr.end = max(curr.end, next.end) |
| Sort + min-heap | Min rooms, max CPU load | heap.peek() = earliest free resource |
| Two-pointer | Intersection of two lists, min rooms (alternative) | advance the pointer with smaller end |
| Sort by end + greedy | Non-overlapping, min arrows | keep/shoot at earliest end |
| Merge all + gap scan | Employee free time | find gaps between merged intervals |
Decision shortcut:
- "merge / combine" β sort by start, linear scan
- "insert one interval" β 3-phase: before / overlap / after
- "intersection" β two-pointer on two sorted lists
- "how many rooms / max overlap" β min-heap of end times
- "free time / gaps" β merge all, scan gaps
- "remove min / min arrows" β sort by end, greedy
Common core: sort by start time β compare with previous result
What differs: what you track and what you return
| Variant | Sort by | Data structure | Key condition | Return |
|---|---|---|---|---|
| Merge Intervals | start | result list | newStart <= currEnd |
merged list |
| Insert Interval | already sorted | result list | 3-phase scan | merged list |
| Interval Intersection | already sorted | result list | max(s1,s2) <= min(e1,e2) |
intersection list |
| Check Any Overlap | start | β | adjacent end >= next start |
true/false |
| Min Meeting Rooms | start (heap) / both | min-heap of ends | heap.peek() <= newStart |
heap.size() |
| Max CPU Load | start | min-heap of ends | same as rooms | max heap size |
| Employee Free Time | start | merge result | gap between merged[i].end and merged[i+1].start |
gap list |
| Non-overlapping | end | β | greedy keep earliest end | count removed |
| Min Arrows | end | β | greedy: arrow at curr.end, skip all covered |
arrow count |
// βββ TEMPLATE A: Merge Overlapping Intervals ββββββββββββββββββββββββββββββββββ
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // sort by start
List<int[]> res = new ArrayList<>();
int start = intervals[0][0];
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= end) { // overlap condition
end = Math.max(end, intervals[i][1]); // extend end
} else {
res.add(new int[]{start, end}); // emit merged interval
start = intervals[i][0];
end = intervals[i][1];
}
}
res.add(new int[]{start, end}); // add last interval
return res.toArray(new int[res.size()][]);
}
// βββ TEMPLATE B: Insert Interval (3-phase) ββββββββββββββββββββββββββββββββββββ
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> res = new ArrayList<>();
int i = 0, n = intervals.length;
// Phase 1: add all intervals that end BEFORE newInterval starts
while (i < n && intervals[i][1] < newInterval[0])
res.add(intervals[i++]);
// Phase 2: merge all overlapping intervals into newInterval
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
res.add(newInterval);
// Phase 3: add remaining intervals
while (i < n) res.add(intervals[i++]);
return res.toArray(new int[res.size()][]);
}
// βββ TEMPLATE C: Interval Intersection (Two-pointer) βββββββββββββββββββββββββ
public int[][] intervalIntersection(int[][] A, int[][] B) {
List<int[]> res = new ArrayList<>();
int i = 0, j = 0;
while (i < A.length && j < B.length) {
int start = Math.max(A[i][0], B[j][0]);
int end = Math.min(A[i][1], B[j][1]);
if (start <= end) // valid intersection
res.add(new int[]{start, end});
if (A[i][1] <= B[j][1]) i++; // advance the one ending first
else j++;
}
return res.toArray(new int[res.size()][]);
}
// βββ TEMPLATE D: Min Meeting Rooms (Min-Heap) βββββββββββββββββββββββββββββββββ
public int minMeetingRooms(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // sort by start
PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap of end times
for (int[] interval : intervals) {
if (!heap.isEmpty() && heap.peek() <= interval[0])
heap.poll(); // reuse freed room
heap.offer(interval[1]); // assign room
}
return heap.size();
}
// βββ TEMPLATE D2: Min Meeting Rooms (Two-pointer alternative) βββββββββββββββββ
public int minMeetingRooms(int[] start, int[] end) {
Arrays.sort(start);
Arrays.sort(end);
int i = 0, j = 0, rooms = 0, res = 0;
while (i < start.length) {
if (start[i] < end[j]) {
rooms++;
res = Math.max(res, rooms);
i++;
} else {
rooms--;
j++;
}
}
return res;
}
// βββ TEMPLATE E: Non-overlapping Intervals (Greedy, sort by end) ββββββββββββββ
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[1] - b[1]); // sort by END time
int count = 0;
int prevEnd = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < prevEnd) { // overlap β remove this one
count++;
} else {
prevEnd = intervals[i][1]; // no overlap β keep, update end
}
}
return count;
}
// βββ TEMPLATE F: Minimum Arrows to Burst Balloons ββββββββββββββββββββββββββββ
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1])); // sort by end
int arrows = 1;
int arrowPos = points[0][1]; // shoot at first balloon's end
for (int i = 1; i < points.length; i++) {
if (points[i][0] > arrowPos) { // balloon not hit
arrows++;
arrowPos = points[i][1]; // shoot at this balloon's end
}
}
return arrows;
}
// βββ TEMPLATE G: Employee Free Time (Merge + Gap scan) βββββββββββββββββββββββ
// Input: list of lists of intervals per employee
public List<Interval> employeeFreeTime(List<List<Interval>> schedule) {
List<Interval> all = new ArrayList<>();
for (List<Interval> emp : schedule) all.addAll(emp);
all.sort((a, b) -> a.start - b.start);
List<Interval> merged = new ArrayList<>();
int start = all.get(0).start, end = all.get(0).end;
for (Interval iv : all) {
if (iv.start <= end) end = Math.max(end, iv.end);
else { merged.add(new Interval(start, end)); start = iv.start; end = iv.end; }
}
merged.add(new Interval(start, end));
List<Interval> res = new ArrayList<>();
for (int i = 1; i < merged.size(); i++)
res.add(new Interval(merged.get(i-1).end, merged.get(i).start));
return res;
}SIGNAL IN PROBLEM β VARIANT β KEY CODE
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"merge overlapping intervals" β Sort + scan β sort start; if newStart<=end: end=max(end,newEnd)
"insert interval into sorted list" β 3-phase insert β before / merge-overlap / after
"intersection of two interval lists" β Two-pointer β max(s1,s2)<=min(e1,e2); advance smaller end
"any two intervals overlap?" β Sort + adjacent check β sorted[i].end >= sorted[i+1].start
"min meeting rooms / min resources" β Min-heap of ends β heap.peek()<=newStart β reuse room
"max CPU load / max overlap count" β Min-heap of ends β same as rooms, track max heap size
"employee free time / find gaps" β Merge + gap scan β gap between merged[i].end & merged[i+1].start
"remove min intervals β non-overlapping" β Sort by end, greedy β keep earliest end; count skipped
"min arrows / min points to cover all" β Sort by end, greedy β shoot at currEnd; skip all covered
The overlap rules β burn these into memory:
// Two intervals [a,b] and [c,d] where a <= c (after sorting):
// OVERLAP: c <= b (touching counts as overlap)
// NO OVERLAP: c > b (gap between them)
// MERGE: [a, max(b,d)]
// Insert interval β 3 cases:
// Before: intervals[i][1] < newInterval[0] β add as-is
// Overlap: intervals[i][0] <= newInterval[1] β expand newInterval
// After: remaining after merge β add as-is
// Min rooms β when to REUSE a room:
// heap.peek() <= interval[0] (room freed before or exactly when new meeting starts)// BUG β intervals not sorted, adjacent check misses non-adjacent overlaps
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= end) ... // [3,5] and [1,4] won't be adjacent
// FIX β always sort by start first
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);// BUG β touching intervals [1,3] and [3,5] are NOT merged
if (intervals[i][0] < end) { merge; } // 3 < 3 is false β not merged β WRONG
// FIX β use <= to handle touching endpoints
if (intervals[i][0] <= end) { merge; } // 3 <= 3 is true β merged β [1,5] β// BUG β contained interval shrinks the merged end
// e.g., current=[1,10], next=[2,5]: end = 5 (WRONG β loses [5,10])
end = intervals[i][1];
// FIX β always take max of both ends
end = Math.max(end, intervals[i][1]); // end stays 10 β// BUG β the last merged interval is never emitted (loop ends without adding it)
for (int i = 1; i < intervals.length; i++) {
if (overlap) { merge; }
else { res.add(...); } // last interval added only on overlap detection
}
// If last interval doesn't overlap with anything, it's never added!
// FIX β always add after the loop
res.add(new int[]{start, end}); // add the last pending interval// BUG β room freed at time 5, new meeting starts at 5 β should reuse
if (heap.peek() < interval[0]) heap.poll(); // 5 < 5 is false β new room allocated (WRONG)
// FIX β use <= (room freed at exactly the start time can be reused)
if (heap.peek() <= interval[0]) heap.poll(); // 5 <= 5 is true β reuse β// BUG β sorting by start for non-overlapping removal gives wrong greedy choice
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // sort by start β WRONG for greedy
// FIX β sort by END for all greedy interval problems
Arrays.sort(intervals, (a, b) -> a[1] - b[1]); // sort by end β always correct greedy
// Keeping the interval with earliest end leaves maximum room for future intervals// BUG β if a[1] is very negative and b[1] is very positive, a[1]-b[1] overflows
Arrays.sort(points, (a, b) -> a[1] - b[1]);
// FIX β use Integer.compare() for safety
Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1]));| Problem | Time | Space | Notes |
|---|---|---|---|
| Merge Intervals | O(n log n) | O(n) | sort + single pass |
| Insert Interval | O(n) | O(n) | input already sorted, 3-phase scan |
| Interval Intersection | O(m + n) | O(m + n) | two-pointer, one pass each list |
| Check Any Overlap | O(n log n) | O(1) | sort + single adjacent check |
| Min Meeting Rooms (heap) | O(n log n) | O(n) | sort + heap, heap size β€ n |
| Min Meeting Rooms (two-ptr) | O(n log n) | O(n) | sort both arrays separately |
| Max CPU Load | O(n log n) | O(n) | same as meeting rooms |
| Employee Free Time | O(n log n) | O(n) | merge all employees' intervals |
| Non-overlapping Intervals | O(n log n) | O(1) | sort by end, greedy count |
| Min Arrows to Burst Balloons | O(n log n) | O(1) | sort by end, greedy shoot |
General rules:
- Sorting dominates: all variants are O(n log n) minimum.
- Space is O(n) when building a result list, O(1) extra for in-place greedy problems.
- Heap-based variants add O(n log n) heap operations on top of sorting β same asymptotic.
- Two-pointer approach for min rooms is O(1) extra space vs O(n) for heap.
- When you see "already sorted" in the problem β Insert Interval drops to O(n).
Merge Interval is one of the most frequently tested patterns at FAANG.
Master the overlap condition c <= b and the 3-phase insert β these cover 80% of all interval problems.