Meeting Rooms II
Hand out rooms in start order and keep the end times in a min-heap, so you always know which room frees up first. Solved two ways, counting at every start in O(n²) and a min-heap in O(n log n), with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given a list intervals of n meetings (n is the length of intervals), each as [start, end]. Return the smallest number of
rooms needed so every meeting can take place. A room that frees up at time 5 can host a meeting that
starts at 5.
Example 1
Input: intervals = [[4, 8], [1, 5], [6, 9], [2, 6], [9, 11], [10, 12]]
Output: 3
At time 4, three meetings run at once: [1, 5], [2, 6] and [4, 8]. No moment has more.
Example 2
Input: intervals = [[3, 7], [7, 9]]
Output: 1
The second starts exactly when the first ends, so one room is enough.
Example 3
Input: intervals = [[1, 10], [2, 3], [4, 5]]
Output: 2
[1, 10] keeps one room busy. The two short ones can share the other.
Constraints
Section titled “Constraints”- 1 ≤ n ≤ 10,000
- 0 ≤ start < end ≤ 1,000,000
Clarifying questions
Section titled “Clarifying questions”- Is back to back a clash?
- No. A room free at 5 can take a meeting that starts at 5.
- Are the meetings sorted?
- No. They come in any order.
- Do we need the room plan?
- No, only how many rooms. That number equals the most meetings running at one moment.
- Can meetings be moved?
- No. Each meeting keeps its start and end.
Spot the pattern
Section titled “Spot the pattern”Handing out rooms in start order is natural. For each new meeting, the only question is “has any room freed up?”, and the room that frees up first is the one to check. A min-heap of end times keeps that room on top. That is the heap pattern, used on intervals.
Solutions
Section titled “Solutions”Sorting the start times and the end times separately and sweeping them with two pointers is also O(n log n). It gives the same answer with a different picture.
Count at each start O(n²)
The idea
Section titled “The idea”The number of rooms needed is the most meetings that run at the same moment. That moment is always the start of some meeting, because the count only goes up when a meeting starts.
So for each meeting’s start time start_time, count the meetings running at start_time: those
with start ≤ start_time < end. The largest count is the answer.
Walkthrough
Section titled “Walkthrough”- best = 0
- for start_time, _ in intervals:
- busy = 0
- for start, end in intervals:
- if start <= start_time < end: busy += 1
- best = max(best, busy)
- return best
The most rooms are needed at some moment when a meeting starts. So for each start time, count the meetings running at that moment.
class Solution { // Count at each start: the busiest moment is always some meeting's start. // For each start, count the meetings running then. O(n^2) time, O(1) memory. public int minMeetingRooms(int[][] intervals) { int best = 0; for (int[] meeting : intervals) { int startTime = meeting[0], busy = 0; for (int[] interval : intervals) { // start <= startTime < end: a meeting that ends at startTime has already left. if (interval[0] <= startTime && startTime < interval[1]) busy++; } best = Math.max(best, busy); } return best; }}# Count at each start: the busiest moment is always some meeting's start.# For each start, count the meetings running then. O(n^2) time, O(1) memory.def min_meeting_rooms(intervals: list[list[int]]) -> int: best = 0 for start_time, _ in intervals: # start <= start_time < end: a meeting that ends at start_time has already left. busy = sum(1 for start, end in intervals if start <= start_time < end) best = max(best, busy) return bestpublic class Solution{ // Count at each start: the busiest moment is always some meeting's start. // For each start, count the meetings running then. O(n^2) time, O(1) memory. public int MinMeetingRooms(int[][] intervals) { int best = 0; foreach (var meeting in intervals) { int startTime = meeting[0], busy = 0; foreach (var interval in intervals) { // start <= startTime < end: a meeting that ends at startTime has already left. if (interval[0] <= startTime && startTime < interval[1]) busy++; } best = System.Math.Max(best, busy); } return best; }}// Count at each start: the busiest moment is always some meeting's start.// For each start, count the meetings running then. O(n^2) time, O(1) memory.func minMeetingRooms(intervals [][]int) int { best := 0 for _, meeting := range intervals { startTime, busy := meeting[0], 0 for _, interval := range intervals { // start <= startTime < end: a meeting that ends at startTime has already left. if interval[0] <= startTime && startTime < interval[1] { busy++ } } best = max(best, busy) } return best}// Count at each start: the busiest moment is always some meeting's start.// For each start, count the meetings running then. O(n^2) time, O(1) memory.function minMeetingRooms(intervals: number[][]): number { let best = 0; for (const [startTime] of intervals) { let busy = 0; // start <= startTime < end: a meeting that ends at startTime has already left. for (const [start, end] of intervals) if (start <= startTime && startTime < end) busy++; best = Math.max(best, busy); } return best;}1 Find the work that repeats
The lit line checks if one meeting is running at time
start_time. It sits inside two loops: one over the start times and one over all meetings. One check is 1 step.- best = 0
- for start_time, _ in intervals:
- busy = 0
- for start, end in intervals:
- if start <= start_time < end: busy += 1
- best = max(best, busy)
- return best
2 Count the runs on Example 1
Example 1 has 6 meetings. Each of the 6 start times checks all 6 meetings.
6 × 6 = 36 checks.
start t = 4 t = 1 t = 6 t = 2 t = 9 t = 10 checks 6 6 6 6 6 6 busy 3 1 2 2 1 2 total6 × 6 = 36 checks3 Write the count for any n
n × n = n²Check it with Example 1: 6 × 6 = 36.
4 Keep only the part that grows fastest
There is nothing to drop. The count is already n².
So the time is O(n²).
S Space: count the extra memory the same way
The code keeps
bestandbusy. The space is O(1).bestbusy
Min-heap of end times O(n log n)
The idea
Section titled “The idea”Sort the meetings by start. Keep a min-heap ends with the end time of every room in use. Its top is
the room that frees up first.
For each meeting, in start order:
- If the top room ends no later than this meeting starts, that room is free. Pop it and reuse it.
- Otherwise every room is still busy, because the top one frees up first. Use a new room.
Either way, push this meeting’s end time. At the end, the heap size is the number of rooms.
Why it is correct: a new room is opened only when even the earliest-freeing room is busy. Then all rooms are busy at this start time, so that many meetings truly overlap, and no plan can use fewer.
Walkthrough
Section titled “Walkthrough”- intervals.sort(key=lambda interval: interval[0])
- ends = [] # min-heap of end times
- for start, end in intervals:
- if ends and ends[0] <= start:
- heappop(ends) # that room is free again
- heappush(ends, end)
- return len(ends)
The meetings are in no order. We will hand out rooms in time order.
class Solution { // Min-heap of end times: hand out rooms in start order. The heap top is the // room that frees up first. Sort plus n heap steps: O(n log n) time, O(n) memory. public int minMeetingRooms(int[][] intervals) { java.util.Arrays.sort(intervals, (first, second) -> Integer.compare(first[0], second[0])); java.util.PriorityQueue<Integer> ends = new java.util.PriorityQueue<>(); for (int[] interval : intervals) { // The room that frees up first is free by now: reuse it. if (!ends.isEmpty() && ends.peek() <= interval[0]) ends.poll(); ends.add(interval[1]); } // One end time per room ever opened. return ends.size(); }}# Min-heap of end times: hand out rooms in start order. The heap top is the# room that frees up first. Sort plus n heap steps: O(n log n) time, O(n) memory.def min_meeting_rooms(intervals: list[list[int]]) -> int: intervals.sort(key=lambda interval: interval[0]) ends = [] for start, end in intervals: # The room that frees up first is free by now: reuse it. if ends and ends[0] <= start: heapq.heappop(ends) heapq.heappush(ends, end) # One end time per room ever opened. return len(ends)public class Solution{ // Min-heap of end times: hand out rooms in start order. The heap top is the // room that frees up first. Sort plus n heap steps: O(n log n) time, O(n) memory. public int MinMeetingRooms(int[][] intervals) { System.Array.Sort(intervals, (first, second) => first[0].CompareTo(second[0])); var ends = new System.Collections.Generic.PriorityQueue<int, int>(); foreach (var interval in intervals) { // The room that frees up first is free by now: reuse it. if (ends.Count > 0 && ends.Peek() <= interval[0]) ends.Dequeue(); ends.Enqueue(interval[1], interval[1]); } // One end time per room ever opened. return ends.Count; }}// endHeap is a min-heap of end times, for container/heap.type endHeap []int
func (times endHeap) Len() int { return len(times) }func (times endHeap) Less(i, j int) bool { return times[i] < times[j] }func (times endHeap) Swap(i, j int) { times[i], times[j] = times[j], times[i] }func (times *endHeap) Push(item any) { *times = append(*times, item.(int)) }func (times *endHeap) Pop() any { before := *times last := before[len(before)-1] *times = before[:len(before)-1] return last}
// Min-heap of end times: hand out rooms in start order. The heap top is the// room that frees up first. Sort plus n heap steps: O(n log n) time, O(n) memory.func minMeetingRooms(intervals [][]int) int { sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] < intervals[j][0] }) ends := &endHeap{} for _, interval := range intervals { // The room that frees up first is free by now: reuse it. if ends.Len() > 0 && (*ends)[0] <= interval[0] { heap.Pop(ends) } heap.Push(ends, interval[1]) } // One end time per room ever opened. return ends.Len()}// A small binary min-heap of numbers. TypeScript has none built in.class MinHeap { private values: number[] = []; get size(): number { return this.values.length; } peek(): number { return this.values[0]; } push(value: number): void { const values = this.values; values.push(value); // Move the new value up while it is smaller than its parent. for (let i = values.length - 1; i > 0; ) { const parent = (i - 1) >> 1; if (values[parent] <= values[i]) break; [values[parent], values[i]] = [values[i], values[parent]]; i = parent; } } pop(): number { const values = this.values; const top = values[0]; const last = values.pop()!; if (values.length) { values[0] = last; // Move it down while a child is smaller. for (let i = 0; ; ) { const leftChild = 2 * i + 1; const rightChild = leftChild + 1; let smallest = i; if (leftChild < values.length && values[leftChild] < values[smallest]) { smallest = leftChild; } if (rightChild < values.length && values[rightChild] < values[smallest]) { smallest = rightChild; } if (smallest === i) break; [values[smallest], values[i]] = [values[i], values[smallest]]; i = smallest; } } return top; }}
// Min-heap of end times: hand out rooms in start order. The heap top is the// room that frees up first. Sort plus n heap steps: O(n log n) time, O(n) memory.function minMeetingRooms(intervals: number[][]): number { intervals.sort((first, second) => first[0] - second[0]); const ends = new MinHeap(); for (const [start, end] of intervals) { // The room that frees up first is free by now: reuse it. if (ends.size > 0 && ends.peek() <= start) ends.pop(); ends.push(end); } // One end time per room ever opened. return ends.size;}1 Find the work that repeats
Two parts repeat. The sort compares meetings many times. Then each run of the loop does at most one pop and one push. On a heap of size up to n, each costs about log₂ n steps.
- intervals.sort(key=lambda interval: interval[0])
- ends = [] # min-heap of end times
- for start, end in intervals:
- if ends and ends[0] <= start:
- heappop(ends) # that room is free again
- heappush(ends, end)
- return len(ends)
2 Count the runs on Example 1
Example 1 has 6 meetings. Sorting 6 items takes about 6 × log₂ 6, so about 16 steps. The loop then does 6 pushes and 3 pops, on a heap that never holds more than 3.
About 16 + 9 heap operations.
meeting [1, 5] [2, 6] [4, 8] [6, 9] [9, 11] [10, 12] heap work push push push pop + push pop + push pop + push heap size 1 2 3 3 3 3 3 Write the count for any n
The sort costs n log₂ n. The loop does at most 2 heap operations per meeting, each about log₂ n:
n log₂ n + 2n log₂ n = 3n log₂ nCheck it with Example 1: the loop did 9 heap operations, within the 2 × 6 = 12 allowed. Each was on a heap of at most 3, so it was cheaper than log₂ 6.
4 Keep only the part that grows fastest
Drop the 3. What is left is n log n. Both the sort and the heap work grow at that rate.
So the time is O(n log n).
S Space: count the extra memory the same way
The heap can hold one end time per meeting, when they all overlap. The space is O(n).
ends (up to n end times)
Tested at the limits
Section titled “Tested at the limits”2,000 random lists of 1 to 7 meetings with times from 0 to 10, so back-to-back meetings and overlaps both come up often. Count at each start and the min-heap of end times gave the same answer every time.
| Largest inputs | Result |
|---|---|
| 10,000 meetings (the most allowed), all running at the same moment, so each one needs its own room. Min-heap of end times. | 0.003 s, and about 0.15 MB of extra memory: the heap holds all 10,000 end times, plus the sort. |
| 2,000 meetings, all overlapping. Count at each start. | 0.17 s. At 10,000 meetings it would make 100 million checks. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.