56. 合并区间

3 阅读6分钟

一、 题目与代码重现

题目描述

以数组 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^4
  • intervals[i].length == 2
  • 0 <= 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;
};

二、 标准解题逻辑步骤

核心思想:排序 + 贪心合并

区间合并问题的本质是将相交的区间归并为一个连续范围。标准解法采用贪心策略,通过排序确保每个区间只与相邻区间比较即可完成全部合并。

详细步骤流程

  1. 特殊情况处理
    若区间数量为 0 或 1,无需合并,直接返回原数组(题目约束至少为 1,但保留此判断更健壮)。

  2. 按起点升序排序
    使用 intervals.sort((a, b) => a[0] - b[0]) 对所有区间按左端点排序。排序后,所有可能重叠的区间必然在排序后的相邻位置,这是后续贪心合并的正确性基础。

  3. 初始化结果数组
    将排序后的第一个区间放入 result 中,作为初始合并区间。

  4. 遍历剩余区间
    从第二个区间开始,每个区间依次与 result 中最后一个区间(即当前最新的合并区间)比较:

    • 若 current[0] <= last[1],说明两个区间存在重叠(包括边界相接),此时合并:last[1] = Math.max(last[1], current[1])
    • 若 current[0] > last[1],说明当前区间与之前所有区间均不重叠(因为已排序且前面的区间已合并完毕),直接将其加入 result 作为新合并区间的起点。
  5. 返回结果
    遍历结束后,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 重叠,则 ABC 形成一个连续的重叠链,只需依次合并相邻元素即可覆盖整个连通块
合并时更新 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²) 时间复杂度,应避免。