Interval Scheduling Patterns
1. Minimum Meeting Rooms Required
| Approach | Method | Time | Space |
|---|---|---|---|
| Min Heap | Track end times of ongoing meetings | O(n log n) |
O(n) |
| Chronological Ordering | Separate start/end events, sort and scan | O(n log n) |
O(n) |
| Sweep Line | Event-driven: +1 for start, -1 for end | O(n log n) |
O(n) |
| Brute Force | Check max overlaps at each point | O(n^2) |
Not recommended |
Example: Min heap approach
function minMeetingRooms(intervals) {
if (intervals.length === 0) return 0;
// Sort by start time
intervals.sort((a, b) => a[0] - b[0]);
// Min heap to track end times of ongoing meetings
const minHeap = new MinPriorityQueue({ priority: x => x });
minHeap.enqueue(intervals[0][1]); // First meeting's end time
for (let i = 1; i < intervals.length; i++) {
const [start, end] = intervals[i];
// If earliest ending meeting finished before current starts
if (minHeap.front().element <= start) {
minHeap.dequeue(); // Reuse this room
}
// Add current meeting's end time
minHeap.enqueue(end);
}
// Heap size = number of rooms needed
return minHeap.size();
}
// Example: [[0,30],[5,10],[15,20]]
// Sort: [[0,30],[5,10],[15,20]] (already sorted)
// Process [0,30]: heap = [30]
// Process [5,10]: 30 > 5, heap = [10,30]
// Process [15,20]: 10 <= 15, reuse room, heap = [20,30]
// Result: 2 rooms
Example: Chronological ordering
function minMeetingRoomsChronological(intervals) {
const starts = intervals.map(i => i[0]).sort((a, b) => a - b);
const ends = intervals.map(i => i[1]).sort((a, b) => a - b);
let rooms = 0;
let endPointer = 0;
for (let i = 0; i < starts.length; i++) {
// If a meeting ends before or when current starts
if (starts[i] >= ends[endPointer]) {
rooms--; // Free up a room
endPointer++;
}
rooms++; // Allocate room for current meeting
}
return rooms;
}
// Example: [[0,30],[5,10],[15,20]]
// starts: [0, 5, 15]
// ends: [10, 20, 30]
//
// i=0, start=0: rooms=1
// i=1, start=5: 5 < 10, rooms=2
// i=2, start=15: 15 >= 10, rooms=2-1+1=2
// Result: 2 rooms
Example: Sweep line with events
function minMeetingRoomsSweep(intervals) {
const events = [];
// Create start and end events
for (const [start, end] of intervals) {
events.push([start, 1]); // Meeting starts (+1)
events.push([end, -1]); // Meeting ends (-1)
}
// Sort events: by time, then ends before starts (tie-breaking)
events.sort((a, b) => {
if (a[0] !== b[0]) return a[0] - b[0];
return a[1] - b[1]; // -1 before 1 (end before start)
});
let rooms = 0;
let maxRooms = 0;
for (const [time, delta] of events) {
rooms += delta;
maxRooms = Math.max(maxRooms, rooms);
}
return maxRooms;
}
// Example: [[0,30],[5,10],[15,20]]
// Events: [[0,1],[5,1],[10,-1],[15,1],[20,-1],[30,-1]]
// Process: 0→1, 5→2, 10→1, 15→2, 20→1, 30→0
// Max: 2 rooms
Note: When sorting events at the same time, end events must come before start events (-1 before +1). This ensures a meeting ending at time T frees the room before a meeting starting at time T occupies it. Without this tie-breaking, we'd incorrectly count an extra room.
2. Employee Free Time Problem
| Approach | Method | Time | Space |
|---|---|---|---|
| Merge All Intervals | Flatten, sort, merge, find gaps | O(n log n) |
O(n) |
| Priority Queue | K-way merge of sorted lists | O(n log k) |
O(k) |
| Sweep Line | Event processing for busy periods | O(n log n) |
O(n) |
| Two Pointers | If each employee has 1 interval only | O(n log n) |
O(1) |
Example: Flatten and merge approach
function employeeFreeTime(schedule) {
// Flatten all intervals into single array
const intervals = [];
for (const employee of schedule) {
for (const interval of employee) {
intervals.push(interval);
}
}
// Sort by start time
intervals.sort((a, b) => a.start - b.start);
// Merge overlapping intervals
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const last = merged[merged.length - 1];
if (current.start <= last.end) {
// Overlapping or adjacent - merge
last.end = Math.max(last.end, current.end);
} else {
// Gap found - but don't add yet
merged.push(current);
}
}
// Find gaps between merged intervals
const freeTime = [];
for (let i = 1; i < merged.length; i++) {
const gapStart = merged[i - 1].end;
const gapEnd = merged[i].start;
if (gapStart < gapEnd) {
freeTime.push(new Interval(gapStart, gapEnd));
}
}
return freeTime;
}
// Example: [[[1,3],[4,6]], [[2,4]], [[2,5],[9,12]]]
// Flattened: [[1,3],[4,6],[2,4],[2,5],[9,12]]
// Sorted: [[1,3],[2,4],[2,5],[4,6],[9,12]]
// Merged: [[1,6],[9,12]]
// Free time: [[6,9]]
Example: Priority queue k-way merge
function employeeFreeTimePQ(schedule) {
// Min heap: [interval, employeeIndex, intervalIndex]
const pq = new MinPriorityQueue({
priority: x => x[0].start
});
// Initialize with first interval of each employee
for (let i = 0; i < schedule.length; i++) {
if (schedule[i].length > 0) {
pq.enqueue([schedule[i][0], i, 0]);
}
}
const freeTime = [];
let prevEnd = pq.front().element[0].start;
while (!pq.isEmpty()) {
const [interval, empIdx, intIdx] = pq.dequeue().element;
// Found a gap
if (prevEnd < interval.start) {
freeTime.push(new Interval(prevEnd, interval.start));
}
prevEnd = Math.max(prevEnd, interval.end);
// Add next interval from same employee
if (intIdx + 1 < schedule[empIdx].length) {
pq.enqueue([
schedule[empIdx][intIdx + 1],
empIdx,
intIdx + 1
]);
}
}
return freeTime;
}
// More efficient when k (employees) << n (total intervals)
// O(n log k) vs O(n log n)
3. Interval List Intersections
| Approach | Technique | Time | Space |
|---|---|---|---|
| Two Pointers | Compare intervals from both lists | O(m + n) |
O(1) |
| Intersection Logic | max(start1, start2) to min(end1, end2) | Overlaps if start ≤ end | Simple and elegant |
| Advance Pointer | Move pointer of interval that ends first | Ensures no interval missed | Critical for correctness |
| Multiple Lists | Use priority queue for k lists | O(n log k) |
Generalization |
Example: Two pointers intersection
function intervalIntersection(firstList, secondList) {
const result = [];
let i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
const [start1, end1] = firstList[i];
const [start2, end2] = secondList[j];
// Calculate intersection
const startIntersection = Math.max(start1, start2);
const endIntersection = Math.min(end1, end2);
// Valid intersection exists
if (startIntersection <= endIntersection) {
result.push([startIntersection, endIntersection]);
}
// Advance pointer of interval that ends first
if (end1 < end2) {
i++;
} else {
j++;
}
}
return result;
}
// Example:
// A = [[0,2],[5,10],[13,23],[24,25]]
// B = [[1,5],[8,12],[15,24],[25,26]]
//
// Intersections:
// [0,2] ∩ [1,5] = [1,2]
// [5,10] ∩ [1,5] = [5,5]
// [5,10] ∩ [8,12] = [8,10]
// [13,23] ∩ [15,24] = [15,23]
// [24,25] ∩ [15,24] = [24,24]
// [24,25] ∩ [25,26] = [25,25]
// Result: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
Example: Visualizing intersection logic
// Understanding intersection calculation:
function hasIntersection(interval1, interval2) {
const [start1, end1] = interval1;
const [start2, end2] = interval2;
// No intersection cases:
// 1. interval1 completely before interval2: end1 < start2
// 2. interval2 completely before interval1: end2 < start1
// Has intersection if neither case is true:
return !(end1 < start2 || end2 < start1);
}
function getIntersection(interval1, interval2) {
const [start1, end1] = interval1;
const [start2, end2] = interval2;
const start = Math.max(start1, start2);
const end = Math.min(end1, end2);
// Valid if start <= end
return start <= end ? [start, end] : null;
}
// Key insight:
// Intersection start = max of two starts (later one)
// Intersection end = min of two ends (earlier one)
// Valid only if start <= end
// Examples:
// [1,5] ∩ [3,7] = [max(1,3), min(5,7)] = [3,5] ✓
// [1,3] ∩ [5,7] = [max(1,5), min(3,7)] = [5,3] ✗ (5 > 3)
4. Insert Interval and Merge
| Step | Action | Condition | Complexity |
|---|---|---|---|
| 1. Add Before | Add intervals completely before new interval | interval.end < newInterval.start |
Direct copy |
| 2. Merge Overlapping | Merge all overlapping intervals | interval.start <= newInterval.end |
Update boundaries |
| 3. Add After | Add remaining intervals after new interval | interval.start > newInterval.end |
Direct copy |
| In-place Option | Modify input array instead of creating new | More complex pointer management | O(1) space |
Example: Insert and merge interval
function insert(intervals, newInterval) {
const result = [];
let i = 0;
const n = intervals.length;
// 1. Add all intervals before newInterval
while (i < n && intervals[i][1] < newInterval[0]) {
result.push(intervals[i]);
i++;
}
// 2. Merge overlapping intervals
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++;
}
result.push(newInterval);
// 3. Add all intervals after newInterval
while (i < n) {
result.push(intervals[i]);
i++;
}
return result;
}
// Example: intervals = [[1,3],[6,9]], newInterval = [2,5]
// Step 1: No intervals end before 2
// Step 2: [1,3] overlaps, merge to [1,5]
// Step 3: Add [6,9]
// Result: [[1,5],[6,9]]
// Example: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]]
// newInterval = [4,8]
// Step 1: Add [1,2]
// Step 2: Merge [3,5],[6,7],[8,10] -> [3,10]
// Step 3: Add [12,16]
// Result: [[1,2],[3,10],[12,16]]
Example: Alternative approach with three passes
function insertAlternative(intervals, newInterval) {
const result = [];
let [newStart, newEnd] = newInterval;
let inserted = false;
for (const [start, end] of intervals) {
if (end < newStart) {
// Current interval completely before new
result.push([start, end]);
} else if (start > newEnd) {
// Current interval completely after new
if (!inserted) {
result.push([newStart, newEnd]);
inserted = true;
}
result.push([start, end]);
} else {
// Overlapping - expand new interval
newStart = Math.min(newStart, start);
newEnd = Math.max(newEnd, end);
}
}
// If not inserted yet, add at end
if (!inserted) {
result.push([newStart, newEnd]);
}
return result;
}
// Single pass through intervals
// Track whether merged interval has been added
5. Task Scheduler Problem (Greedy)
| Approach | Strategy | Formula | Complexity |
|---|---|---|---|
| Greedy Max Frequency | Schedule most frequent task first with cooldown | max(n, (maxFreq-1)*cooldown + tiesCount) |
O(n) |
| Heap Simulation | Use max heap + cooldown queue | Simulate actual scheduling | O(n log 26) |
| Greedy Formula | Calculate without simulation | Slots needed = max of tasks or formula | O(n) |
| Idle Slots | Count idle time needed | (maxFreq-1) * cooldown - other tasks |
Alternative view |
Example: Greedy formula approach
function leastInterval(tasks, n) {
// Count frequency of each task
const freq = new Array(26).fill(0);
for (const task of tasks) {
freq[task.charCodeAt(0) - 'A'.charCodeAt(0)]++;
}
// Find max frequency
const maxFreq = Math.max(...freq);
// Count how many tasks have max frequency
const maxCount = freq.filter(f => f === maxFreq).length;
// Calculate minimum intervals
// Pattern: (A _ _ A _ _ A) for maxFreq=3, cooldown=2
// Partitions: (maxFreq - 1)
// Each partition size: (n + 1) where n is cooldown
// Last partition: maxCount tasks
const partitions = maxFreq - 1;
const partitionSize = n + 1;
const emptySlots = partitions * partitionSize;
const availableSlots = emptySlots + maxCount;
const idles = Math.max(0, availableSlots - tasks.length);
return tasks.length + idles;
}
// Alternative cleaner formula:
function leastIntervalFormula(tasks, n) {
const freq = new Array(26).fill(0);
for (const task of tasks) {
freq[task.charCodeAt(0) - 'A'.charCodeAt(0)]++;
}
const maxFreq = Math.max(...freq);
const maxCount = freq.filter(f => f === maxFreq).length;
// Either all tasks fit, or need idle slots
return Math.max(
tasks.length,
(maxFreq - 1) * (n + 1) + maxCount
);
}
// Example: tasks = ["A","A","A","B","B","B"], n = 2
// Frequency: A=3, B=3
// maxFreq = 3, maxCount = 2
// Formula: (3-1)*(2+1) + 2 = 2*3 + 2 = 8
// Schedule: A B _ A B _ A B
// Result: 8
Example: Heap simulation approach
function leastIntervalHeap(tasks, n) {
// Count frequencies
const freq = {};
for (const task of tasks) {
freq[task] = (freq[task] || 0) + 1;
}
// Max heap of frequencies
const maxHeap = new MaxPriorityQueue({ priority: x => x });
for (const count of Object.values(freq)) {
maxHeap.enqueue(count);
}
let time = 0;
while (!maxHeap.isEmpty()) {
const temp = [];
// Try to schedule n+1 tasks (or until heap empty)
for (let i = 0; i < n + 1; i++) {
if (!maxHeap.isEmpty()) {
const count = maxHeap.dequeue().element;
if (count > 1) {
temp.push(count - 1);
}
}
}
// Add remaining tasks back to heap
for (const count of temp) {
maxHeap.enqueue(count);
}
// If heap not empty, full cycle; else just processed tasks
time += maxHeap.isEmpty() ? temp.length + 1 : n + 1;
}
return time;
}
// Simulates actual scheduling cycle by cycle
// Each cycle: schedule up to (n+1) different tasks
// More intuitive but slower than formula approach
Note: Task Scheduler formula derivation: With max frequency F and cooldown N, we need F-1 complete "frames" of size N+1 (to satisfy cooldown), plus the final batch of tasks with max frequency. The answer is
max(tasks.length, (F-1)*(N+1) + maxCount) because if we have enough task variety, no idle time is needed.
Summary: Interval Scheduling Patterns
- Minimum Meeting Rooms: Min heap tracking end times, or chronological ordering of starts/ends, or sweep line with events - all O(n log n)
- Min Heap Pattern: Sort by start, maintain heap of end times, reuse room if earliest end ≤ current start, heap size = rooms needed
- Chronological Pattern: Separate sorted start/end arrays, two pointers, increment rooms on start, decrement on end
- Sweep Line Events: Create [time, delta] events, sort with tie-breaking (ends before starts), track running count, record max
- Employee Free Time: Flatten all intervals, sort, merge overlapping, gaps between merged = free time - O(n log n)
- K-way Merge: Priority queue merging k sorted lists - O(n log k) more efficient when k << n
- Interval Intersection: Two pointers, intersection = [max(start1,start2), min(end1,end2)], valid if start ≤ end, advance pointer of interval ending first
- Insert Interval: Three phases: add before, merge overlapping, add after - O(n) single pass
- Merge While Inserting: Expand new interval boundaries while overlapping, track insertion flag to avoid duplicates
- Task Scheduler Greedy: Formula:
max(tasks.length, (maxFreq-1)*(cooldown+1) + maxCount)- O(n) time - Scheduler Intuition: Most frequent task creates frames, other tasks fill frames, maxCount tasks with same max frequency go together in final frame
- Heap Simulation: Max heap of frequencies, each cycle schedule up to cooldown+1 tasks, decrement and re-add - more intuitive but slower
- Common Pattern: Sort intervals, use greedy or data structure (heap/events), handle overlaps, O(n log n) typical complexity