Interval Scheduling: Practice Problems with Solutions
Welcome to the practice problems for interval scheduling. If you need a refresher on the code, the code templates have the patterns in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.
Recommended Study Order
Start with Meeting Rooms to see overlap detection in its purest form. Merge Intervals and Insert Interval teach the sorting-by-start branch of the pattern, where intervals are combined rather than selected. Non-overlapping Intervals flips the selection greedy into a removal count. Meeting Rooms II and Car Pooling add the sweep line, which counts concurrency instead of choosing intervals. Finish with Course Schedule III, which combines the greedy with a heap and is the hardest problem in this set.
Easy Problems
1. Meeting Rooms
LeetCode 252 | Difficulty: Easy
Brief: Return true if a person can attend every meeting without any two meetings overlapping.
Why this pattern: This is overlap detection in its purest form. After a sort by start time, any conflict must involve neighboring meetings, so one pass decides.
Key Insight: Overlap can only occur between neighboring meetings after sorting. Compare each meeting with the one before it.
Visual:
graph TD
I["[0,30], [5,10], [15,20]"] --> S["Sort by start time"]
S --> C["[5,10] starts at 5, previous meeting ends at 30"]
C --> F["5 < 30, they overlap, return false"]
Code:
var canAttendMeetings = function(intervals) {
// Sorting by start time means conflicts can only occur
// between neighboring meetings.
intervals.sort((a, b) => a.start - b.start);
for (let i = 1; i < intervals.length; i++) {
// A meeting that starts before the previous one ends
// overlaps it.
if (intervals[i].start < intervals[i - 1].end) {
return false;
}
}
return true;
};The solution sorts by start time and checks neighbors only. After the sort, any overlap must involve adjacent meetings, so a single pass detects every conflict. The comparison is <, not <=, because a meeting that starts exactly when the previous one ends fits in the same day.
Medium Problems
2. Merge Intervals
LeetCode 56 | Difficulty: Medium
Brief: Merge all overlapping intervals into a list of non-overlapping intervals.
Why this pattern: This is the combining branch of interval scheduling. Sorting by start time means each interval can only touch the last merged one.
Key Insight: When the current interval starts at or before the last merged end, widen the last merged interval instead of adding a new one.
Visual:
graph TD
I["[1,3], [2,6], [8,10], [15,18]"] --> S["Sort by start time"]
S --> M["[2,6] overlaps [1,3], widen to [1,6]"]
M --> A["[8,10] does not overlap, add"]
A --> B["[15,18] does not overlap, add"]
B --> R["Result: [1,6], [8,10], [15,18]"]
Code:
var merge = function(intervals) {
if (intervals.length === 0) return [];
// Sorting by start time means any overlap with a previous
// interval always involves the last entry in the result.
intervals.sort((a, b) => a.start - b.start);
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const last = merged[merged.length - 1];
const current = intervals[i];
if (current.start <= last.end) {
// Overlap: widen the merged interval to cover both.
last.end = Math.max(last.end, current.end);
} else {
// No overlap: the current interval starts a new block.
merged.push(current);
}
}
return merged;
};The trick is that sorting by start time makes each new interval comparable only with the last merged one. The result list never needs to be rescanned, because every interval that could overlap the current one is already inside the last merged entry.
3. Insert Interval
LeetCode 57 | Difficulty: Medium
Brief: Insert a new interval into a sorted, non-overlapping list and merge as needed.
Why this pattern: Insertion is the merge rule applied to a single interval. The overlap check decides where the new interval lands and how far it grows.
Key Insight: Everything before the new interval is copied untouched, everything that overlaps it is absorbed, everything after goes in as is.
Visual:
graph TD
I["[1,3], [6,9] and new [2,5]"] --> A["[1,3] ends at 3, after 2, so it overlaps"]
A --> M["Merge [1,3] and [2,5] into [1,5]"]
M --> B["[6,9] starts at 6, after 5, add as is"]
B --> R["Result: [1,5], [6,9]"]
Code:
var insert = function(intervals, newInterval) {
const result = [];
let i = 0;
const n = intervals.length;
// Intervals that end before the new interval starts are
// untouched and go straight into the result.
while (i < n && intervals[i][1] < newInterval[0]) {
result.push(intervals[i]);
i++;
}
// Merge every interval that overlaps the new one. The
// merged range widens to cover all of them.
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);
// Everything after the merged range goes in as is.
while (i < n) {
result.push(intervals[i]);
i++;
}
return result;
};Insertion is merge with a single seed interval. The three passes split the work cleanly: copy everything before the new interval, absorb everything that overlaps it, then copy the rest. Because the input is already sorted, none of the three passes ever looks backward.
4. Non-overlapping Intervals
LeetCode 435 | Difficulty: Medium
Brief: Return the minimum number of intervals to remove so the rest do not overlap.
Why this pattern: Keeping the maximum non-overlapping set is activity selection in reverse. Each overlap forces a choice, and the earliest-finishing interval is always the one to keep.
Key Insight: The interval that ends earliest is the safest to keep. When a conflict appears, remove the later-ending interval, not the earlier one.
Visual:
graph TD
I["[1,2], [2,3], [3,4], [1,3]"] --> S["Sort by end time"]
S --> P["Keep [1,2], last end 2"]
P --> Q["[2,3] starts at 2, no overlap, keep, last end 3"]
Q --> R["[3,4] starts at 3, no overlap, keep, last end 4"]
R --> X["[1,3] starts at 1, overlaps, remove"]
X --> D["Answer: 1 removal"]
Code:
var eraseOverlapIntervals = function(intervals) {
// Sorting by end time makes the earliest-finishing interval
// the safest one to keep.
intervals.sort((a, b) => a.end - b.end);
let removed = 0;
let lastEnd = -Infinity;
for (const interval of intervals) {
if (interval.start < lastEnd) {
// This interval overlaps the one we kept. Since it
// ends later, removing it loses nothing.
removed++;
} else {
lastEnd = interval.end;
}
}
return removed;
};This problem counts removals, so the greedy flips into a decision about which interval to drop. When the current interval overlaps the kept one, remove the current one, because it ends later and is therefore the riskier of the two for everything that follows. The count of removals is the answer directly, without ever constructing the kept set.
5. Meeting Rooms II
LeetCode 253 | Difficulty: Medium
Brief: Return the minimum number of conference rooms needed to host every meeting.
Why this pattern: This is the resource allocation variant. Instead of choosing intervals, the sweep counts how many overlap at their peak.
Key Insight: Match every start against the earliest available end. A new room is needed only when the start arrives before that end.
Visual:
graph TD
I["Meetings: 0-30, 5-10, 15-20"] --> S["starts: 0, 5, 15 and ends: 10, 20, 30"]
S --> P1["start 0 < end 10, open a room, count 1"]
P1 --> P2["start 5 < end 10, open a room, count 2"]
P2 --> P3["start 15 >= end 10, the first room is free, reuse"]
P3 --> R["Answer: 2 rooms"]
Code:
var minMeetingRooms = function(intervals) {
// Sorting starts and ends independently turns the problem
// into merging two timelines, one for meetings beginning
// and one for meetings ending.
const starts = intervals.map(i => i.start).sort((a, b) => a - b);
const ends = intervals.map(i => i.end).sort((a, b) => a - b);
let rooms = 0;
let endIdx = 0;
for (const start of starts) {
if (start < ends[endIdx]) {
// A meeting starts before the earliest room frees
// up, so a new room is needed.
rooms++;
} else {
// The earliest-ending meeting is done, so its room
// can be reused.
endIdx++;
}
}
return rooms;
};The two-pointer sweep works because the earliest-free end is always ends[endIdx]. A new room is only needed when a start arrives before that end. The count needs no heap, and the two arrays are sorted independently, which is the part that confuses people who try to keep each interval intact.
6. Car Pooling
LeetCode 1094 | Difficulty: Medium
Brief: Return true if the car can complete every trip without exceeding its passenger capacity.
Why this pattern: This is the same concurrency counting as Meeting Rooms II, applied to a moving car. Pickups and dropoffs are the start and end events.
Key Insight: Turn every trip into two events, a pickup and a dropoff, then process locations in order and track the running passenger count.
Visual:
graph TD
I["Trips: 2 from 1 to 5, 3 from 3 to 7, capacity 4"] --> E["Events: +2 at 1, +3 at 3, -2 at 5, -3 at 7"]
E --> P["After location 1, 2 on board"]
P --> Q["After location 3, 5 on board"]
Q --> F["5 exceeds capacity 4, return false"]
Code:
var carPooling = function(trips, capacity) {
// Each trip becomes two events: a pickup that adds passengers
// and a dropoff that removes them.
const events = [];
for (const [passengers, from, to] of trips) {
events.push([from, passengers]);
events.push([to, -passengers]);
}
// Dropoffs sort before pickups at the same location, so a
// seat frees before the next passenger claims it.
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
let onBoard = 0;
for (const [, change] of events) {
onBoard += change;
if (onBoard > capacity) {
return false;
}
}
return true;
};The sweep-line trick turns trips into events and processes them in location order. Dropoffs sort before pickups at the same location, so a seat frees before the next passenger claims it. The running count at any point is the number of passengers on board, and the moment it exceeds capacity the trip set is invalid.
Hard Problems
7. Course Schedule III
LeetCode 630 | Difficulty: Hard
Brief: Return the maximum number of courses you can take, given each course’s duration and deadline.
Why this pattern: This combines the deadline greedy with a max heap. When a deadline is missed, removing the longest course restores feasibility with minimal loss.
Key Insight: Take every course in deadline order, and when the total time overshoots a deadline, drop the longest course taken so far.
Visual:
graph TD
I["[100,200], [200,1300], [1000,1250], [2000,3200]"] --> S["Sort by deadline"]
S --> P["Take every course, time 3300"]
P --> O["3300 exceeds deadline 3200, drop the longest course, 1000"]
O --> T["Time 2300, 3 courses kept"]
T --> R["Answer: 3 courses"]
Code:
var scheduleCourse = function(courses) {
// A course with an earlier deadline must be considered first,
// because postponing it is never helpful.
courses.sort((a, b) => a[1] - b[1]);
// The max heap holds the durations of taken courses so the
// longest one can be dropped when time runs out.
const maxHeap = new MaxPriorityQueue();
let time = 0;
for (const [duration, deadline] of courses) {
time += duration;
maxHeap.enqueue(duration);
if (time > deadline) {
// Dropping the longest course frees the most time
// while removing only one course.
time -= maxHeap.dequeue().element;
}
}
return maxHeap.size();
};This is greedy plus a heap. Sorting by deadline means each course is decided in deadline order, and when a deadline is missed, dropping the longest course already taken frees the most time while sacrificing only one course. The heap keeps the longest course on top so the drop is instant, and its size at the end is the answer.
These seven problems cover the full range of interval scheduling. Overlap detection in Meeting Rooms, combining in Merge Intervals and Insert Interval, greedy selection in Non-overlapping Intervals, concurrency counting in Meeting Rooms II and Car Pooling, and the heap-augmented greedy in Course Schedule III. If the weighted variant interests you, it is covered in the concept guide and code templates . From here, move to the next pattern and come back to this page in a few days to see what you still remember.