☰
合并区间算法详解:从排序贪心到面试通关
2026/10/21 16:46:27 网站建设 项目流程

刚刷完这道题那阵子,我正好在协助一位朋友准备面试。他之前做后端开发,算法基础不算扎实,一看到“合并区间”这种题就慌,觉得要处理的情况太多了。我当时跟他说,这道题真没那么玄乎,本质上就是“先排序,再逛街”,把乱序的区间排成一条时间线,剩下的就是一个简单的判断逻辑。

今天这篇刷题日记,就把“合并区间”这道题完整拆一遍。不光给你能直接通过的写法,更会把背后的思考过程、边界条件、以及我实际踩过的坑都讲清楚。无论你是准备面试、复习数据结构,还是单纯想提升手感,这篇都能给你一些参考。

1. 题目回顾:到底要我们做什么

1.1 题面描述

题目本身非常短:给一个区间的集合,请合并所有重叠的区间。示例也很直白。比如输入[[1,3],[2,6],[8,10],[15,18]],因为[1,3]和[2,6]有重叠部分,合并之后变成[1,6],而[8,10]和[15,18]之间没有重叠,保持原样,所以最终输出[[1,3],[2,6],[8,10],[15,18]]合并后的结果是[[1,6],[8,10],[15,18]]。

1.2 隐藏的考点

看起来简单,但面试官在这道题里真正想看的,其实是两件事:第一,你懂不懂排序在这道题里的必要性和作用;第二,你处理边界情况够不够细心。很多人在“合并”那一步会写得很复杂,又是嵌套循环又是标记数组,实际用不到。这道题只要排序思路对了,代码量能控制在二十行上下。

1.3 适合谁看

如果你是第一次接触这道题,建议从头看一遍。如果你已经刷过,也能在后面的“常见坑”和“边界用例”里找到新的启发。网上很多题解直接给代码,但没说清楚为什么排序之后贪心就能成立,我会在下面重点解释这部分。

2. 核心思路:排序为什么是关键一步

2.1 不排序会怎样

先试想一下,如果不排序,直接从头到尾遍历并尝试合并,会发生什么?对于[[1,3],[8,10],[2,6]]这样的输入,当你看到[8,10]时,并不知道后面会出现一个能把它前面的[1,3]扩展成[1,6]的[2,6]。于是你不得不多次回看之前的区间,甚至要维护一个复杂的“已合并区间集合”,每次新加一个区间都要和集合里每一个已有区间比对。这样一来,最坏情况下时间复杂度会接近 O(n²),而且代码中要处理的判断条件会特别多,极易出错。

2.2 排序之后带来的变化

一旦我们按照每个区间的左端点升序排序,情况就完全不一样了。排序后,每个区间的起始位置都不会小于前一个区间的起始位置。这意味着,在遍历过程中,我们只需要关注“当前区间能不能接在已合并区间的后面”这一个问题。

举一个直观的例子。假设排序后的区间是[1,3]、[2,6]、[5,10]。从[1,3]开始,看[2,6]时,它的左端点 2 小于当前合并区间的右端点 3,说明有重叠,可以把右端点扩展到 6。现在合并区间接住了[1,6]。再看[5,10],它的左端点 5 小于等于 6,又重叠了,继续扩展成[1,10]。整个过程像是把区间一个接一个地“串”起来,不需要回头处理已合并区间,维护的始终只有一个“当前合并区间”。

2.3 排序的稳定性问题

可能有人会问:左端点相同怎么办?比如[[1,4],[1,3]]。这时候按左端点排序后,两者顺序其实无关紧要,因为合并时右端点会取最大值。所以排序函数不必额外处理左端点相同的情况,用默认排序即可。不过如果面试官恰好问到这个点,你能主动说一句“左端点相同时顺序不影响,因为我们会用最大值更新右边界”,会是一个加分项。

2.4 手写排序还是调用库函数

在面试中,直接用语言自带的排序函数就好,大部分场景不需要手写排序。比如 Python 里intervals.sort(key=lambda x: x[0]),Java 里Arrays.sort(intervals, (a, b) -> a[0] - b[0])。这里有一个小提醒:Java 的a[0] - b[0]写法在区间数值很大时存在整型溢出风险。虽然这道题的测试数据通常不会触发,但严谨一点的写法是用Integer.compare(a[0], b[0])。我在团队内部做代码评审时,会建议大家养成这个习惯,毕竟谁也不想在极端数据上翻车。

3. 合并逻辑:从模拟到代码实现

3.1 合并条件的确立

排序之后,我们把第一个区间先放进结果集,然后用一个变量记录“目前合并区间的右端点”。往后遍历时,只有两种情况:

  • 当前区间的左端点小于等于记录中的右端点:说明重叠,合并。合并后的右端点取两者中的较大值。
  • 当前区间的左端点大于记录中的右端点:说明没有重叠,把当前合并区间收尾,然后开启一个新的合并区间。

这里特别要注意“小于等于”的重叠判定。很多初学者会把条件写成“小于”,漏掉了相邻区间[1,2]和[2,3]应该合并的情况。题目描述中的“重叠”通常包含端点重合,所以必须是<=。

3.2 参考代码:Python 版

我习惯用 Python 先把思路快速验证一遍,逻辑清楚之后再用其他语言改写。代码如下:

def merge(intervals): if not intervals: return [] intervals.sort(key=lambda x: x[0]) merged = [] for interval in intervals: # 如果 merged 为空,或者当前区间与上一个合并区间不重叠 if not merged or merged[-1][1] < interval[0]: merged.append(interval) else: # 重叠时更新右端点 merged[-1][1] = max(merged[-1][1], interval[1]) return merged

这段代码里,merged[-1]始终代表当前正在维护的合并区间。因为排序后的区间左端点不会变小,所以只需要调整右端点。max取较大值是为了处理[1,5]遇到[2,4]这种“大区间包小区间”的情况。

3.3 参考代码:Java 版

如果面试要用 Java,写法类似,只是语法上需要多注意一些细节:

public int[][] merge(int[][] intervals) { if (intervals.length == 0) { return new int[0][]; } Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); List<int[]> merged = new ArrayList<>(); for (int[] interval : intervals) { if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < interval[0]) { merged.add(interval); } else { merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], interval[1]); } } return merged.toArray(new int[merged.size()][]); }

最后用merged.toArray(new int[0][])也行,但显式指定大小在某些 JDK 版本下性能略好。这里属于细枝末节,面试中写new int[merged.size()][]已经完全够用。

3.4 手动跑一个完整例子

我们来手动跟踪一遍最经典的输入:[[1,3],[2,6],[8,10],[15,18]]。

排序后结果不变。初始化merged = []。

  • 读入[1,3],merged为空,直接加入,merged = [[1,3]]。
  • 读入[2,6],当前最后一个合并区间右端点是 3,2 <= 3,重叠。更新右端点为max(3, 6) = 6,得到[[1,6]]。
  • 读入[8,10],8 > 6,不重叠。加入新区间,merged = [[1,6],[8,10]]。
  • 读入[15,18],15 > 10,不重叠。加入新区间,得到[[1,6],[8,10],[15,18]]。

整个过程就像在时间轴上推进,非常直观。

3.5 复杂度分析

排序的时间复杂度是 O(n log n),后续一次遍历是 O(n),所以整体是 O(n log n)。空间复杂度方面,如果用 Python 或 Java 的链表结构存储结果,额外空间是 O(n);如果允许修改原数组,有的写法可以做到 O(1) 额外空间(不算输出数组的话)。刷题时能够准确地说出这个复杂度分析,是基本功的体现。

4. 实操中的边界情况与常见陷阱

4.1 边界用例速查表

我整理了一份这个题的高频用例,建议你把这些 case 都手动跑一遍:

输入预期输出关键点
[][]空数组要直接处理,避免排序报错
[[1,4]][[1,4]]单区间直接返回
[[1,4],[2,3]][[1,4]]大区间包小区间
[[1,2],[2,3]][[1,3]]端点重合算重叠
[[1,4],[5,6]][[1,4],[5,6]]恰好不重叠
[[1,4],[0,4]][[0,4]]左端点更新
[[2,3],[1,6]][[1,6]]排序后左端点非递增是关键

4.2 我踩过的三个坑

第一个坑是开头忘了判空。intervals为空数组时,有些语言的排序函数会直接报错或返回奇怪结果。我一开始写 Python 时没加if not intervals,结果本地测试用例没覆盖到,提交之后被系统的隐藏用例打了个措手不及。从那以后,凡是处理数组的题,我写完核心逻辑后第一件事就是检查空输入。

第二个坑是合并时只更新右端点、没考虑到左端点也可能变化。你可能觉得排序之后左端点不会变小,但如果你的原数组没有被真正排序,或者排序函数写错了,就会出现左端点反而变小的情况。比如输入[[1,6],[0,4]],如果没排序,当前合并区间是[1,6],读入[0,4]时,0 <= 6,合并后如果只更新右端点,结果还是[1,6],但正确答案应该是[0,6]。解决办法很粗暴:一定先排序。只要你保证了排序,合并时就不需要再动左端点。

第三个坑是关于整型溢出的。我在某次模拟面试中用过 Java 的a[0] - b[0],当时面试官追问了一句“如果所有数字都是Integer.MAX_VALUE附近的值呢?”我第一反应觉得减法在 Java 里是允许的,但仔细一想确实可能溢出成负数,导致排序完全乱掉。虽然实际上测试数据一般到不了那个极端,但面试当场能意识到这个问题并改成Integer.compare,观感会不一样。

4.3 关于原数组能否修改

很多题解为了“省空间”,直接在原数组上写结果。这样确实可以把额外空间降到 O(1),但代价是破坏了输入数据。如果面试官没有特别说明可以修改入参,我建议还是新建一个结果集。理由很简单:工程化思维里,入参通常不应该被随意修改,否则调用方可能在后续逻辑中出现难以排查的 bug。面试过程中主动提一句“这里我会新建一个结果集合来避免修改原数组”,会让面试官觉得你不仅有算法能力,还有工程意识。

5. 从一道题到一类题:合并区间的变体与延伸

5.1 插入区间

一道非常常见的变体是:给定一组互不相交且已排序的区间,再给一个新的区间,让你插入进去并合并。这题看起来比“合并区间”多了一步插入,实际上思路一脉相承。你只需要找到新区间与所有旧区间重叠的范围,然后把它们合并成一个,再拼接剩余部分。理解了“合并区间”的贪心逻辑后,这种题就是小改。

我一开始做插入区间时,想着把所有区间放在一起重新排序再合并,虽然也能做,但复杂度多了一次 O(n log n)。后来意识到原输入已经有序,完全可以用一次遍历完成,复杂度降到 O(n)。这提醒我一个通用原则:题目给出的每一个条件,都可能是算法优化的线索。

5.2 无重叠区间

另一道经典题“无重叠区间”,问你最少需要移除多少个区间,才能让剩下的区间互不重叠。这题的核心思路是先按右端点排序,然后贪心地选尽可能多的不重叠区间。很多人做完“合并区间”后直接做这题,容易混淆“按左端点排序”和“按右端点排序”的用途。简单记法:合并区间关心的是“能连多远”,按左端点让区间从左往右展开,合并效率高;无重叠区间关心的是“给后面留多少空间”,按右端点排序能保证每次留下的区间结束得最早,给后续留出最大余地。

我强烈建议把“合并区间”“插入区间”“无重叠区间”打包刷掉,这样你对区间类问题的理解才是成体系的。面试时面试官从“合并区间”切入,大概率后续会追问类似变体。

5.3 区间分组问题

还有一类变体是:给一堆会议时间区间,问至少需要多少个会议室。这种题的常见解法是分别记录开始时间和结束时间,排序后用双指针扫描。它和“合并区间”的区别在于,合并要求算出并集,而会议室的题要求的是同时重叠的最大数量。两者都建立在排序思维之上,但一个用栈类似的“结果列表”,一个用两个指针同步推进。想清楚不同点,比多刷十道题还管用。

6. 实操总结与个人体会

6.1 这道题需要的核心能力

在我看来,“合并区间”其实考察了三个层次。第一层是基础的排序能力,包括排序方向和排序稳定性;第二层是线性扫描时的状态维护,也就是知道什么时候该开新区间、什么时候该扩展现有区间;第三层是边界意识,包括空数组、单元素、恰好相邻这些细节。把这三层都想通,代码怎么写都是次要的了。

6.2 我在实际项目中见到的相似场景

有一次在做某个数据处理模块时,遇到了一个真实的需求:一批时间段记录可能存在重叠,需要把它们合并成连续的时间段,用来统计每个用户的有效在线时长。那一刻我意识到,这类题不是只活在题库里的。数据清洗、日历合并、日志聚合、IP 地址段合并,到处都能遇到区间合并的影子。虽然实际工程里我会直接用现成的库函数,但懂了原理,排查异常时能更快定位问题。

6.3 最后一个小技巧

分享一个我刷题时常用的做法:每做完一道题,我会在本地维护一个冲突用例集合,把所有踩过的坑都记录进去。比如这题我会加入[[1,4],[0,2],[3,5]]这种交错重叠的用例,每次想改动写法时,先把这些用例跑一遍。刚开始觉得有点费时间,刷多了才发现,这比盲目刷新题性价比高得多。人在同一个坑里反复摔倒,才是最亏的。

顺带说一句,面试时如果时间充裕,主动写出边界测试用例并解释为什么这些用例有代表性,通常能给你带来不错的加分效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询