一、 题目与代码重现
题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
text
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
text
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
示例 3:
text
输入:intervals = [[4,7],[1,4]]
输出:[[1,7]]
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。
限制条件:
1 <= intervals.length <= 10^4intervals[i].length == 20 <= starti <= endi <= 10^4
原始代码
javascript
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
// 如果输入为空或只有一个区间,直接返回
if (intervals.length <= 1) return intervals;
// 1. 按区间的起点(start)升序排序
intervals.sort((a, b) => a[0] - b[0]);
// 2. 初始化结果数组,将第一个区间放入
const result = [intervals[0]];
// 3. 从第二个区间开始遍历
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const last = result[result.length - 1]; // 结果中最后一个区间
// 如果当前区间的起点 <= 上一个合并区间的终点,说明重叠
if (current[0] <= last[1]) {
// 合并:更新上一个区间的终点为两者终点的较大值
last[1] = Math.max(last[1], current[1]);
} else {
// 不重叠,直接加入结果
result.push(current);
}
}
return result;
};
二、 标准解题逻辑步骤
核心思想:排序 + 贪心合并
区间合并问题的本质是将相交的区间归并为一个连续范围。标准解法采用贪心策略,通过排序确保每个区间只与相邻区间比较即可完成全部合并。
详细步骤流程
-
特殊情况处理
若区间数量为 0 或 1,无需合并,直接返回原数组(题目约束至少为 1,但保留此判断更健壮)。 -
按起点升序排序
使用intervals.sort((a, b) => a[0] - b[0])对所有区间按左端点排序。排序后,所有可能重叠的区间必然在排序后的相邻位置,这是后续贪心合并的正确性基础。 -
初始化结果数组
将排序后的第一个区间放入result中,作为初始合并区间。 -
遍历剩余区间
从第二个区间开始,每个区间依次与result中最后一个区间(即当前最新的合并区间)比较:- 若
current[0] <= last[1],说明两个区间存在重叠(包括边界相接),此时合并:last[1] = Math.max(last[1], current[1])。 - 若
current[0] > last[1],说明当前区间与之前所有区间均不重叠(因为已排序且前面的区间已合并完毕),直接将其加入result作为新合并区间的起点。
- 若
-
返回结果
遍历结束后,result中存储的即为所有合并后的不重叠区间。
正确性证明(简要)
排序后,若 intervals[i] 与 intervals[j] 重叠(i < j),则 intervals[i+1] 必然与它们在同一连通块中,因此通过顺序遍历即可捕获所有重叠关系。贪心选择当前区间的最大右端点保证了合并结果的正确性。
三、 API 与语法深度解析
| API / 语法 | 作用 | 参数说明 | 本题适用场景 |
|---|---|---|---|
Array.prototype.sort(compareFn) | 原地排序数组 | compareFn(a, b) 返回值 < 0 则 a 在前,> 0 则 b 在前 | 按区间起点 a[0] - b[0] 升序排序,为合并提供有序序列 |
Math.max(a, b) | 返回两个数中的较大值 | 接受数值参数 | 合并重叠区间时,选取两个区间右端点中的较大值作为新区间的右端点 |
箭头函数 (a, b) => a[0] - b[0] | 简写函数表达式 | 隐式返回计算结果 | 作为 sort() 的比较器,简洁高效 |
const result = [intervals[0]] | 数组字面量初始化 | 直接包含首个元素 | 初始化结果数组,避免空数组判断 |
result[result.length - 1] | 获取数组最后一个元素 | 利用 length 属性索引 | 因为 result 中保留的是已合并的不重叠区间,最后一个区间即为当前待比较的合并区间 |
四、 得分重点与理解难点划分
得分重点(考点)
| 考点 | 说明 |
|---|---|
| 排序算法 | 必须理解为什么先排序,以及排序对后续合并逻辑的必要性 |
| 双指针/贪心思想 | 利用有序性,通过一次遍历完成合并,无需回溯 |
| 区间重叠判断条件 | current[0] <= last[1] 是核心判定条件,包含端点相接情况 |
| 原地修改或新数组构建 | 代码需正确处理原数组的引用或创建新数组 |
理解难点(思维盲区)
| 难点 | 深度剖析 |
|---|---|
| 为什么排序后只需比较相邻区间? | 排序保证了若存在重叠,它们必然在排序序列中连续。若 A 与 B 重叠,B 与 C 重叠,则 A、B、C 形成一个连续的重叠链,只需依次合并相邻元素即可覆盖整个连通块 |
合并时更新 last[1] 而非 current[1] | 直接修改 result 中最后一个区间,避免了频繁 push/pop 操作,同时也是对原数组元素的直接引用(注意 sort 是原地排序) |
| 边界重叠的处理 | current[0] <= last[1] 中包含等号,将 [1,4] 与 [4,5] 视为重叠并合并为 [1,5],符合题意中的“可被视为重叠区间” |
五、 极易踩错的细节与边界错误
边界条件陷阱
| 场景 | 错误写法 | 正确写法/处理 |
|---|---|---|
| 空数组输入 | 未判断 intervals.length === 0,直接 intervals[0] 报错 | 添加 if (!intervals.length) return [] |
| 只有一个区间 | 排序后仍进入循环,虽无大错但多余 | if (intervals.length <= 1) return intervals |
| 所有区间完全重合 | result 中只保留一个区间,但错误地重复推入新区间 | 正确使用 Math.max 更新终点即可 |
| 区间无序且未排序 | 直接遍历,仅能合并相邻原始顺序的区间 | 必须排序后再遍历 |
常见逻辑陷阱
- 引用污染问题:
const last = result[result.length - 1]获取的是对象引用,修改last[1]会直接影响result中的数组。这是本题设计意图,但如果误将current赋值给last再修改,可能导致错误。 - 原地排序副作用:
intervals.sort()会修改原数组,如果后续需要使用原数据则需注意。本题无需保留原始顺序。 - 遍历范围错误:循环从
i = 1开始,但若intervals长度在循环中被修改(如splice),会导致索引错乱。本题采用不修改原数组长度,只修改元素值的方式,安全可靠。
性能相关错误
- 在循环内使用
result.push([...current])创建新数组副本,虽然功能正确但增加内存开销(本题性能尚可,非致命)。 - 使用
splice动态删除已合并区间会导致 O(n²) 时间复杂度,应避免。