题干与适用场景
给定一个无序数组 intervals,每个元素是整数时间区间 [start, end)。start 包含在会议中,end 不包含;因此一场会议在时间 t 结束时,另一场会议可以在 t 使用同一房间。假设 0 <= intervals.length <= 100000、0 <= start < end <= 1000000000。返回安排全部会议所需的最少会议室数量。
例如,[[0, 30], [5, 10], [15, 20]] 返回 2;[[1, 5], [5, 8]] 返回 1;空数组返回 0。这些约束是本文明确采用的面试契约,不应与某个平台的隐藏约束混用。
公开材料给出了可靠的代表性证据:PracHub 在 2026 年更新了同题题干;interviewing.io 把最少会议室列为可用时间线或优先队列解决的区间题;一则 2026 年 2 月的公开面经记录了双指针、优先队列和有限时间范围数组三种追问。UMass 的算法讲义则给出了区间分组的核心证明:按开始时间处理时,贪心使用的房间数等于最大重叠深度。单篇面经只能证明该候选人的经历,本文不会据此推断普遍频率,也不把题目归属于某一家公司的固定题库。
面试官考察点
第一层是建模。题目问的是资源数量,不是合并区间,也不是选择最多互不冲突会议。最少房间数等于任一时刻同时进行的会议数峰值;这个峰值常称为区间集合的重叠深度。
第二层是端点语义。半开区间下,结束事件必须在同一时刻的开始事件之前处理。若把 start === end 当成重叠,[1, 5) 与 [5, 8) 会被错误算成两间房。
第三层是不变量与证明。分别排序开始时间和结束时间后,指针不再保留“某个结束属于哪场会议”的对应关系。候选人需要解释:只求同时占用数量时,接下来最早发生的是开始还是结束已经足够,会议身份并不重要。
最后是需求变化时的数据结构选择。双数组扫描线最直接地返回数量;若追问具体房间分配、每场会议对应的房间或房间复用记录,就需要保存结束时间与房间编号的最小堆,而不能只留下计数。
回答前需要澄清的问题
- 区间是
[start, end)还是闭区间? 本题采用半开区间,相等端点不冲突。 - 零长度会议是否合法? 本题要求
start < end,不接收[t, t);若业务允许,应先约定它是否占资源。 - 空输入返回什么? 返回
0,避免初始化第一场会议造成越界。 - 只返回数量,还是还要房间分配? 数量可用双数组;分配需要保留会议身份和可复用房间。
- 输入可以修改吗? 下面实现复制开始、结束时间,不改变调用者的数组。
- 时间是否一定是安全整数? 当前上限在 JavaScript 安全整数范围内;更大的时间戳需要重新约定表示方式。
- 输入是否可能无效? 面试题通常保证约束成立。生产代码应在系统边界校验,而不是把校验杂糅进核心算法。
30 秒回答框架
“我会先确认区间是半开区间,所以同一时刻结束的房间可以立即复用。只求最少数量时,我把开始和结束时间分别排序,用两个指针按时间扫描;若下一个开始严格早于最早结束,就增加占用,否则先释放房间,并记录占用峰值。
这个峰值既是下界,也是可达到的上界:同时进行的会议必然需要不同房间,而按开始时间处理时,只有所有现有房间都未结束才会开新房。排序决定总时间是 O(n log n),额外空间是 O(n)。如果追问具体房间分配,我会改用保存结束时间和房间编号的最小堆。”
分步骤深入解答
第一步:用两个有序事件流计算占用峰值
把所有开始时间升序放入 starts,所有结束时间升序放入 ends。startIndex 指向下一个未处理的开始事件,endIndex 指向下一个未处理的结束事件,roomsInUse 表示扫描位置之后仍被占用的房间数。有效区间满足 start < end,所以扫描过程中不会在没有活跃会议时先处理结束事件。
若 starts[startIndex] < ends[endIndex],下一事件是开始:必须新增一个占用,更新峰值。否则,下一事件先按结束处理:释放一个房间。这里有意使用严格小于;端点相等时进入释放分支,随后再处理开始事件,恰好实现 [start, end)。
export function minimumMeetingRooms(
intervals: ReadonlyArray<readonly [number, number]>,
): number {
if (intervals.length === 0) return 0
const starts = intervals.map(([start]) => start).sort((a, b) => a - b)
const ends = intervals.map(([, end]) => end).sort((a, b) => a - b)
let startIndex = 0
let endIndex = 0
let roomsInUse = 0
let maximumRooms = 0
while (startIndex < intervals.length) {
if (starts[startIndex] < ends[endIndex]) {
roomsInUse += 1
maximumRooms = Math.max(maximumRooms, roomsInUse)
startIndex += 1
} else {
roomsInUse -= 1
endIndex += 1
}
}
return maximumRooms
}手算 [[0, 30], [5, 10], [15, 20]]:开始序列为 0, 5, 15,结束序列为 10, 20, 30。扫描到 0、5 时占用从 0 增到 2;10 先释放到 1;15 再增到 2。峰值是 2。
第二步:证明峰值就是最优答案
先证明扫描计数正确。把每个 [start, end) 看成一个 +1 开始事件和一个 -1 结束事件,并按时间排序;同一时刻先处理结束。扫描到任意事件后,累计和恰好等于该时刻之后仍覆盖时间线的区间数,也就是正在占用的会议室数。两个有序数组的指针合并,等价于按这个规则遍历全部事件。
再证明峰值是最优答案。设最大重叠深度为 d。某个时刻有 d 场会议同时进行,任何安排都至少需要 d 间房,这是下界。另一方面,按开始时间处理会议时,只有当前所有房间都被尚未结束的会议占用才会开新房;若开到第 k 间,此刻新会议与另外 k - 1 场会议同时存在,因此 k <= d。所以存在只用 d 间房的安排。下界与可行上界相等,最少房间数就是 d,扫描线返回的正是这个峰值。
创建两个数组是 O(n),两次排序是 O(n log n),合并扫描是 O(n),总时间 O(n log n),额外空间 O(n)。若时间值来自很小且固定的离散范围,可以用差分计数换取 O(n + U) 时间与 O(U) 空间;当时间上界为十亿时,这种优化不合适。
第三步:根据输出要求选择最小堆或差分数组
另一种做法是先按开始时间排序,再用最小堆保存各个占用房间的结束时间。处理新会议前,弹出所有 end <= start 的元素,然后压入新结束时间;堆大小峰值就是答案。它同样需要 O(n log n) 时间和 O(n) 最坏空间。
只求数量时,扫描线更短,也直接暴露“结束先于同刻开始”的规则。最小堆的价值在扩展性:堆元素可从 end 扩展为 { end, roomId }。另外维护一个按编号排序的空闲房间堆,就能在会议结束后回收编号,并把每个原始会议索引映射到具体房间。若题目要求编号最小的可用房间,仅按结束时间取一间还不够,必须把“仍占用”和“已空闲”分开管理。
动态在线预订是另一道题。未来会议逐条到达且可能取消时,重新排序全部区间可能太贵;需要按业务查询设计有序事件表、区间树或日历索引。不要把离线数组题的 O(n log n) 答案直接宣称为在线系统方案。
高质量示范回答
“我先按 [start, end) 解题,所以结束时间等于另一场开始时间时可以复用房间。暴力做法可以逐对检查冲突,但最坏要 O(n^2);这里最多有 10 万场会议,我会排序事件。
我分别得到升序的开始数组和结束数组。两个指针比较下一次事件:开始时间更早就把当前占用加一并更新最大值;结束更早或两者相等就先减一。比如 [[0, 30], [5, 10], [15, 20]] 的占用变化是 1、2、1、2,所以答案是 2。
正确性有两部分。扫描的累计值等于当前活跃区间数,因此最大值是最大重叠深度 d。任意安排在 d 场同时发生时至少需要 d 间房;按开始时间的贪心只有在已有房间全被占用时才增加房间,因此也不会使用超过 d 间。算法最优。时间是 O(n log n),空间是 O(n)。如果需要返回每场会议的房间号,我会保留原始索引,并用结束时间堆和空闲房间编号堆完成分配。”
常见错误
- 错误表现:把题目写成合并区间。失败原因:合并后的区间数量与同时占用峰值没有对应关系。修正方法:对开始、结束事件做扫描并记录活跃数峰值。
- 错误表现:端点相等时先处理开始。失败原因:可立即复用的房间被重复计算。修正方法:在半开区间契约下让结束事件优先。
- 错误表现:使用 JavaScript 默认排序。失败原因:字典序会令
10排在2前面。修正方法:显式传入(a, b) => a - b。 - 错误表现:只返回循环结束时的
roomsInUse。失败原因:最后的占用可能低于过程峰值。修正方法:每次增加占用时维护maximumRooms。 - 错误表现:逐对比较所有会议。失败原因:最坏产生
O(n^2)时间。修正方法:排序后线性合并两个事件流。 - 错误表现:做房间分配时只从堆中弹出一个已结束会议。失败原因:空闲集合和活跃集合会不完整。修正方法:弹出所有
end <= start的房间,并分开维护可用编号。 - 错误表现:未说明区间边界。失败原因:相等端点的测试期望会互相矛盾。修正方法:编码前明确半开或闭区间及同刻事件优先级。
- 错误表现:根据公开题库标签直接写公司归属。失败原因:第三方标签和单个候选人记录不足以证明固定归属。修正方法:证据不足时令
companyName为null,只陈述来源实际支持的范围。
追问及应对
为什么两个排序数组可以丢掉会议对应关系?
目标只依赖每个时刻的活跃会议数量。下一次计数变化由最早的未处理开始和最早的未处理结束决定,与该结束属于哪场会议无关。若要返回分配或单场会议轨迹,对应关系重新变得必要,应换用带会议索引和房间编号的堆。
如果区间改成闭区间 [start, end] 呢?
同一时刻结束与开始会冲突。比较条件应让开始事件先发生,也就是在 start <= end 时增加占用。更稳妥的表述是明确同刻事件优先级,避免只机械修改运算符。
如何返回每场会议的房间编号?
保留原始索引并按开始时间排序。用一个最小堆保存 { end, roomId } 的占用房间;处理会议前,把所有 end <= start 的房间编号移入空闲编号最小堆。若有空闲编号就复用,否则创建新编号,然后记录 assignment[originalIndex] = roomId。
为什么最小房间数等于最大重叠数?
最大重叠数给出不可突破的下界,因为同时发生的会议不能共享房间。按开始时间的贪心只有在所有现有房间都仍被占用时才开房;每次开出的房间数都对应当时同样多的重叠会议,因此不会超过该下界。两边相等即得到最优性。
应该测试哪些边界?
至少覆盖空数组、单区间、完全不重叠、全部重叠、链式端点相等、相同开始时间、相同结束时间、重复区间、输入逆序,以及接近约束上限的随机数据。还可用小规模 O(n^2) 或离散事件基准与优化实现做随机差分测试,但一次性验证代码不应混入生产实现。
若时间范围很小,能否做到线性时间?
可以。用长度与时间域 U 对应的差分数组,在开始位置加一、结束位置减一,再求前缀和峰值,复杂度为 O(n + U)。只有 U 足够小且内存可控才值得使用;当前十亿上限下,排序方案更稳妥。