在LeetCode区间类题目中,56. 合并区间是经典的中等题,也是后续解决复杂区间问题(如插入区间、无重叠区间)的基础。它看似简单,却考察了对区间排序、边界判断的核心能力,很多新手会在"如何判断重叠""如何更新区间边界"上踩坑。今天这篇博客,我们就从题目解读入手,一步步推导解题思路,逐行解析给出的代码,再补充易错点和优化方向,帮大家彻底吃透这道题,掌握区间重叠问题的解题模板。
一、题目解读:明确"合并重叠区间"的核心要求
先吃透题干,避免因理解偏差走弯路,题干核心信息拆解如下:
-
输入:一个二维数组intervals,其中每个元素intervals[i] = [starti, endi] 表示一个区间(starti是区间起始值,endi是区间结束值),输入区间无固定排序规则;
-
输出:一个不重叠的区间数组,要求满足两个核心条件------① 覆盖输入中的所有区间(无遗漏);② 区间之间互不重叠(无冗余);
-
关键提示:重叠的定义的是"区间之间有交集或相邻",比如[1,3]和[2,6]重叠(交集为[2,3]),[1,4]和[4,5]也需要合并(相邻,合并为[1,5])。
举两个典型示例,帮大家直观理解需求:
示例1:输入intervals = [[1,3],[2,6],[8,10],[15,18]],输出[[1,6],[8,10],[15,18]]。原因:[1,3]和[2,6]重叠,合并为[1,6],其余区间无重叠,直接保留。
示例2:输入intervals = [[1,4],[4,5]],输出[[1,5]]。原因:两个区间相邻,属于可合并范围,合并后覆盖所有输入区间且无重叠。
这里有个容易忽略的点:输入区间是无序的------这是解题的第一个关键点。如果区间无序,我们无法直接判断相邻区间是否重叠,因此"排序"是解题的前置步骤,也是核心突破口之一。
二、解题思路:排序 + 遍历合并,构建无重叠区间
结合题干特点,我们可以提炼出"排序先行、遍历合并"的核心思路,步骤清晰且可复用,具体拆解如下:
-
排序预处理:将二维数组intervals按照"每个区间的起始值starti"升序排序。排序后,所有可能重叠的区间会相邻,这是我们能够线性遍历合并的前提;
-
初始化结果集:用一个空数组result存储最终的无重叠区间,后续我们会逐步将合并后的区间加入其中;
-
线性遍历合并:遍历排序后的每个区间interval,分两种情况处理:
-
如果result为空(即还没有处理任何区间),直接将当前区间interval加入result,作为第一个待合并的区间;
-
如果result不为空,取出result中最后一个区间(记为lastInterval,这是当前最右侧的待合并区间),对比当前区间interval的起始值与lastInterval的结束值:
-
若interval[0] ≤ lastInterval[1]:说明两个区间重叠(或相邻),需要合并,合并后的区间起始值不变(仍为lastInterval[0]),结束值取两个区间结束值的最大值(避免出现"一个区间包含另一个区间"的情况,如[1,5]和[2,4],合并后应为[1,5]);
-
若interval[0] > lastInterval[1]:说明两个区间无重叠,直接将当前区间interval加入result,作为新的待合并区间。
-
-
-
返回结果:遍历结束后,result中存储的就是所有合并后的无重叠区间,直接返回即可。
补充对比:这道题和之前提到的"汇总区间"(LeetCode 228)有本质区别------汇总区间是"从有序整数数组提炼区间",输入是单个整数的有序集合;而合并区间是"从区间数组合并重叠区间",输入是多个区间的集合,核心难点在于"排序"和"重叠判断",而非"区间提炼"。
三、代码逐行解析:吃透每一步的逻辑的意义
给出的代码是TypeScript版本,逻辑简洁、高效,完全贴合上述解题思路,我们逐行拆解,搞懂每一步的作用,避免"背代码"却不懂原理:
typescript
function merge(intervals: number[][]): number[][] {
// 1. 初始化结果集,用于存储合并后的无重叠区间
const result: number[][] = [];
// 2. 按照区间起始值升序排序,关键预处理步骤
intervals.sort((a, b) => a[0] - b[0]);
// 3. 遍历排序后的所有区间,逐个合并
for (const interval of intervals) {
// 3.1 若结果集为空,直接加入当前区间(第一个区间无需合并)
if(result.length === 0){
result.push(interval);
} else {
// 3.2 取出结果集最后一个区间(当前最右侧待合并区间)
const lastInterval = result[result.length - 1];
// 3.3 判断当前区间与最后一个区间是否重叠(或相邻)
if (interval[0] <= lastInterval[1]) {
// 重叠:更新最后一个区间的结束值为两者最大值
lastInterval[1] = Math.max(lastInterval[1], interval[1]);
} else {
// 不重叠:直接加入结果集
result.push(interval);
}
}
}
// 4. 返回合并后的无重叠区间数组
return result;
};
关键代码细节解读(易错点重点标注)
-
排序逻辑:intervals.sort((a, b) => a[0] - b[0]) ------ 这里必须按照"区间起始值a[0]"排序,不能按结束值排序。如果按结束值排序,会导致相邻区间可能并非潜在重叠区间,无法正确合并(如[[1,5],[2,3]],按结束值排序后为[[2,3],[1,5]],遍历会误判为不重叠);
-
重叠判断条件:interval[0] ≤ lastInterval[1] ------ 注意是"≤",不是"<"。因为当两个区间相邻时(如[1,4]和[4,5]),interval[0] = lastInterval[1],此时也需要合并,否则会出现两个相邻的区间,不符合"无重叠且全覆盖"的要求;
-
区间更新逻辑:lastInterval[1] = Math.max(lastInterval[1], interval[1]) ------ 这一步是为了处理"一个区间包含另一个区间"的情况。比如输入[[1,5],[2,3]],排序后遍历到[2,3]时,interval[0](2)≤ lastInterval[1](5),此时合并后的结束值应为5(两者最大值),而非3,避免丢失原区间的范围。
四、复杂度分析与优化方向
1. 时间复杂度
核心耗时在于"排序"和"遍历":排序的时间复杂度为O(n log n)(n为区间的个数),遍历的时间复杂度为O(n),因此整体时间复杂度为O(n log n),这是该题的最优时间复杂度(因为必须通过排序才能让潜在重叠区间相邻,排序的最低时间复杂度就是O(n log n))。
2. 空间复杂度
空间复杂度分两种情况:
-
若不考虑排序所需的额外空间(不同语言的sort实现占用空间不同,如JavaScript的sort是原地排序,占用O(log n)空间),仅考虑结果集result,空间复杂度为O(n)(最坏情况:所有区间都不重叠,result需要存储所有区间);
-
若考虑排序的额外空间,整体空间复杂度为O(n log n)。
3. 优化方向
给出的代码已经是最优解法,无需大幅修改,但有两个小细节可以优化(不影响复杂度,仅提升代码简洁度):
-
可以将"result为空"的判断与后续逻辑合并,简化代码(如下所示);
-
对于空输入(intervals = []),代码会直接返回空数组,无需额外处理(因为sort方法对空数组无影响,遍历也不会执行)。
typescript
// 简化版代码(逻辑不变,更简洁)
function merge(intervals: number[][]): number[][] {
const result: number[][] = [];
intervals.sort((a, b) => a[0] - b[0]);
for (const interval of intervals) {
// 合并判断逻辑,无需单独写if判断result是否为空
if (!result.length || interval[0] > result[result.length - 1][1]) {
result.push(interval);
} else {
result[result.length - 1][1] = Math.max(result[result.length - 1][1], interval[1]);
}
}
return result;
};
五、常见易错点总结(避坑必看)
新手做这道题,大概率会在以下3个地方出错,提前规避:
-
忘记排序,直接遍历合并:会导致相邻区间并非潜在重叠区间,无法正确合并(如输入[[1,3],[8,10],[2,6]],不排序会误判[1,3]和[8,10]不重叠,再遍历[2,6]时,也无法和[1,3]合并,最终结果错误);
-
重叠判断条件写错,用"<"代替"≤":会导致相邻区间无法合并(如[[1,4],[4,5]],会被误判为不重叠,输出[[1,4],[4,5]],不符合题目要求);
-
合并时直接覆盖结束值,不用Math.max:会导致"大区间包含小区间"时,丢失大区间的范围(如[[1,5],[2,3]],会误合并为[1,3],丢失原区间[1,5]的范围)。
六、题目延伸:举一反三,掌握区间类题目套路
合并区间是区间类题目的"基础模板",掌握它之后,再做类似题目会事半功倍,推荐3道延伸题目,帮大家巩固思路:
-
LeetCode 57. 插入区间:在合并区间的基础上,新增一个区间,插入后再合并,核心思路仍是"排序+合并",只是多了"插入"的步骤;
-
LeetCode 435. 无重叠区间:求需要移除的最少区间数,使剩余区间无重叠,核心仍是"排序+重叠判断",只是逻辑改为"统计重叠区间个数";
-
LeetCode 228. 汇总区间:与合并区间对比,输入是有序整数数组,核心是"提炼区间",而非"合并区间",可以对比练习,避免混淆。
七、总结
LeetCode 56. 合并区间的核心逻辑其实很简单:排序让重叠区间相邻,遍历过程中逐个判断、合并。这道题的重点不在于代码有多复杂,而在于对"排序的必要性""重叠条件的判断""区间边界的更新"这三个细节的把控。
记住一句话:区间类题目,只要涉及"重叠""排序",优先考虑"按起始值排序 + 线性遍历合并"的模板,再根据具体题目调整细节(如插入、统计重叠个数等)。