合并区间 图解题解
这道题到底在问什么
- 输入
- intervals = [[1,3],[2,4],[6,8]]
- 输出
- [[1,4],[6,8]]([1,3] 和 [2,4] 重叠,合成 [1,4])
最优解:为什么这么做
一句话答案:LeetCode 56 合并区间的标准解是先按起点排序、再一趟线性扫描:维护「当前合并段」的右端 curE,新区间起点 s ≤ curE 就把右端拉成 max(curE, e) 继续并,s > curE 就把旧段封口、另起新段。排序保证每个区间只可能与最近一段重叠,整体时间 O(n log n)、空间 O(n)。
合并区间这道题在问什么
给一堆可能互相重叠的区间 [start, end],要求把所有叠在一起的区间合并成一段,输出一组互不重叠、又恰好覆盖全部输入的区间。比如 [[1,3],[2,4],[6,8]],前两个叠着,合成 [1,4],答案是 [[1,4],[6,8]]。难点不在「怎么合两个区间」,而在乱序输入下怎么知道谁和谁该合——这决定了第一步必须是排序。
为什么合并区间必须先按起点排序
如果不排序,直接从左往右扫,你没法只和「上一段」比:后面随时可能冒出一个起点更小的区间,回头和早已处理过的段重叠,一趟扫描就失效了,只能退化成两两比较。
按起点从小到大排序后,起点单调不减,一个新区间要么接在最近合并出的那段上(重叠或相接),要么整体落在它右边。它绝不可能越过最近一段去和更早的段重叠——因为更早的段已经被最近一段「吞并或隔开」了。于是每个区间只需做一次比较,线性一趟就够。
怎么判断两个区间重叠,等号算不算
扫描时手里始终握着一个「当前合并段」,只关心它的右端 curE。新区间 [s, e] 来了就比一次:s ≤ curE 说明新区间的头没越过当前段的尾,两段叠着或端点相接,并进去;s > curE 说明中间断开了,旧段定型收进结果,用 [s, e] 另起一段。
等号必须算重叠:[1,3] 和 [3,5] 在 3 处相接,按题意要合成 [1,5],判断写成 s < curE 就会把它们错误拆开。另一个细节是合并时右端要取 max(curE, e) 而不是直接赋成 e——新区间可能被当前段完全包住,比如当前段是 [1,9]、来了个 [2,5],直接赋值会把右端从 9 改小成 5。
为什么当前段的起点永远不用更新
因为区间已按起点排序,当前段的起点是这一片区间里最小的起点;之后并进来的区间起点只会更大,不可能把段的左边界往左推。所以合并动作只有一个方向:右端往大里拉。这也解释了为什么整个算法状态可以精简到只记 res 里最后一段——参考代码里 res[-1][1] = max(res[-1][1], e) 一行就是全部合并逻辑。
复杂度怎么算,有哪些边界
时间 O(n log n):瓶颈在排序,排好后线性扫一遍只花 O(n)。空间 O(n):结果数组最坏放下全部 n 个区间(两两不重叠时原样输出);反过来最好情形所有区间连成一片,结果只剩 1 个。
最容易翻车的三处:忘记排序直接扫、漏掉端点相等的重叠、合并时不取 max 导致右端被改小。写完拿一组含「完全包含」和「端点相接」的用例各验一遍,基本就稳了。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住一个判断:新区间起点 s 和当前段右端 curE 比大小。s ≤ curE 就并(拉长右端);s > curE 就断(封旧段、开新段)。下面每一帧都在套这条规则。
- 4排好序后,把第 1 个区间 [1,2] 设为「当前合并段」(绿色高亮)。curE = 2,从下一个区间开始往后并。
- 5指针走到第 2 个区间 [1,3],它的起点 s = 1。当前合并段右端 curE = 2。1 没越过 2,两段挨着或叠着。
- 6s ≤ curE,重叠!把它并进当前段:起点仍是 1,右端拉到 max(curE, 3) = 3。当前段变成 [1,3](绿色组多了这一格)。
- 7指针走到第 3 个区间 [2,4],它的起点 s = 2。当前合并段右端 curE = 3。2 没越过 3,两段挨着或叠着。
- 8s ≤ curE,重叠!把它并进当前段:起点仍是 1,右端拉到 max(curE, 4) = 4。当前段变成 [1,4](绿色组多了这一格)。
- 9指针走到第 4 个区间 [3,4],它的起点 s = 3。当前合并段右端 curE = 4。3 没越过 4,两段挨着或叠着。
- 10s ≤ curE,重叠!把它并进当前段:起点仍是 1,右端拉到 max(curE, 4) = 4。当前段变成 [1,4](绿色组多了这一格)。
- 11指针走到第 5 个区间 [5,5],它的起点 s = 5。当前合并段右端 curE = 4。5 越过了 4,中间断开了。
- 12s > curE,断开!把旧段 [1,4] 收进结果(变灰封口),再用 [5,5] 开一个新的当前段(绿色组重置到这一格)。
- 13指针走到第 6 个区间 [6,7],它的起点 s = 6。当前合并段右端 curE = 5。6 越过了 5,中间断开了。
- 14s > curE,断开!把旧段 [5,5] 收进结果(变灰封口),再用 [6,7] 开一个新的当前段(绿色组重置到这一格)。
- 15指针走到第 7 个区间 [6,8],它的起点 s = 6。当前合并段右端 curE = 7。6 没越过 7,两段挨着或叠着。
- 16s ≤ curE,重叠!把它并进当前段:起点仍是 6,右端拉到 max(curE, 8) = 8。当前段变成 [6,8](绿色组多了这一格)。
- 17指针走到第 8 个区间 [7,8],它的起点 s = 7。当前合并段右端 curE = 8。7 没越过 8,两段挨着或叠着。
- 18s ≤ curE,重叠!把它并进当前段:起点仍是 6,右端拉到 max(curE, 8) = 8。当前段变成 [6,8](绿色组多了这一格)。
- 19指针走到第 9 个区间 [8,9],它的起点 s = 8。当前合并段右端 curE = 8。8 没越过 8,两段挨着或叠着。
- 20s ≤ curE,重叠!把它并进当前段:起点仍是 6,右端拉到 max(curE, 9) = 9。当前段变成 [6,9](绿色组多了这一格)。
- 21指针走到第 10 个区间 [9,9],它的起点 s = 9。当前合并段右端 curE = 9。9 没越过 9,两段挨着或叠着。
- 22s ≤ curE,重叠!把它并进当前段:起点仍是 6,右端拉到 max(curE, 9) = 9。当前段变成 [6,9](绿色组多了这一格)。
- 23扫到末尾,把最后的当前段也收进结果。最终合并出 [1,4] [5,5] [6,9] 这 3 段互不重叠的区间,就是答案。
⚠️ 容易写错的地方
✗ 错:忘了先按起点排序就直接合并
✓ 对:先 sort(key=起点) 再扫
不排序时后面可能冒出一个起点更小的区间,一趟扫描没法保证只和「上一段」比就够
✗ 错:判重叠写成 s < curE(漏了相等)
✓ 对:用 s ≤ curE
[1,3] 和 [3,5] 端点相接也算重叠,要合成 [1,5];用 < 会把它们错误拆开
✗ 错:合并时把右端直接设成新区间的 e
✓ 对:curE = max(curE, e)
新区间可能被旧段完全包住(如当前 [1,9] 来了 [2,5]),直接赋值会把右端改小
完整代码(Python / C++ / Java)
Python
def merge(intervals):
intervals.sort(key=lambda x: x[0]) # 按起点排序
res = []
for s, e in intervals:
if res and s <= res[-1][1]: # 与上一段重叠
res[-1][1] = max(res[-1][1], e)
else: # 断开,另起一段
res.append([s, e])
return resC++
vector<vector<int>> merge(vector<vector<int>>& a){
sort(a.begin(), a.end());
vector<vector<int>> res;
for (auto& it : a) {
if (!res.empty() && it[0] <= res.back()[1])
res.back()[1] = max(res.back()[1], it[1]);
else res.push_back(it);
}
return res;
}Java
public int[][] merge(int[][] a) {
Arrays.sort(a, (x, y) -> x[0] - y[0]);
List<int[]> res = new ArrayList<>();
for (int[] it : a) {
int n = res.size();
if (n > 0 && it[0] <= res.get(n-1)[1])
res.get(n-1)[1] = Math.max(res.get(n-1)[1], it[1]);
else res.add(it);
}
return res.toArray(new int[0][]);
}复杂度
时间
O(n log n)
瓶颈在排序;排好后只扫一遍 O(n),所以总体 O(n log n)
空间
O(n)
结果数组最多放下 n 个区间;不算结果的话排序额外空间 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 合并区间 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么必须先排序?+
排序后保证起点单调不减,这样每个新区间只可能和「最近合并出的那一段」相邻或重叠,不会和更前面的段产生关系,于是一趟线性扫描就够了。不排序就得两两比较,复杂度退化。
curE 代表什么?为什么起点不用动?+
curE 是当前合并段的右端。因为已按起点排序,当前段的起点一定是段里最小的起点,合并新区间时起点不会变小,所以只需要把右端往大里拉。
结果区间的总数最少 / 最多是多少?+
最少 1 个(所有区间连成一片),最多 n 个(两两都不重叠,原样输出)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 合并区间 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。