题目描述
给你一个若干个区间的集合 intervals,其中每个区间为 intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例
复制代码
示例 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]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
示例 3:
输入:intervals = [[4,7],[1,4]]
输出:[[1,7]]
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。
解题思路总览
| 方法 |
核心思想 |
时间复杂度 |
空间复杂度 |
备注 |
| 排序 + 贪心 |
先按起点排序,后遍历合并 |
O(n log n) |
O(n) |
推荐解法 |
| 模拟法 |
两两比较所有区间 |
O(n^2) |
O(n) |
会超时 |
一、排序 + 贪心(最常用)
核心思想
先按区间的起点进行升序排序,排序后相邻的区间才有可能是需要合并的区间。然后遍历区间,维护一个结果集,如果当前区间与结果集中最后一个区间有重叠,则合并,否则加入结果集。
状态转移逻辑
复制代码
如果当前区间起点 <= 结果集最后一个区间终点:
-> 有重叠,合并(取两区间终点的较大值)
否则:
-> 无重叠,直接加入结果集
图解
复制代码
intervals = [[1,3],[2,6],[8,10],[15,18]]
第一步:排序(按起点升序)
原顺序: [[1,3],[2,6],[8,10],[15,18]]
排序后: [[1,3],[2,6],[8,10],[15,18]] (已按起点排好,无需修改)
第二步:遍历合并
ans = []
处理 [1,3]:
ans 为空,直接加入
ans = [[1,3]]
处理 [2,6]:
2 <= 3(上一个终点),有重叠
合并:ans.back()[1] = max(6, 3) = 6
ans = [[1,6]]
处理 [8,10]:
8 > 6(上一个终点),无重叠
ans = [[1,6],[8,10]]
处理 [15,18]:
15 > 10(上一个终点),无重叠
ans = [[1,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
二、算法流程图
详细合并过程
复制代码
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
初始化:
ans = []
排序后: [[1,3],[2,6],[8,10],[15,18]]
遍历 i=0, [1,3]:
ans = []
ans.empty()? 是
ans.push_back([1,3])
ans = [[1,3]]
遍历 i=1, [2,6]:
ans.back() = [1,3]
interval[0]=2, ans.back()[1]=3
2 <= 3? 是,重叠
ans.back()[1] = max(6, 3) = 6
ans = [[1,6]]
遍历 i=2, [8,10]:
ans.back() = [1,6]
interval[0]=8, ans.back()[1]=6
8 <= 6? 否,不重叠
ans.push_back([8,10])
ans = [[1,6],[8,10]]
遍历 i=3, [15,18]:
ans.back() = [8,10]
interval[0]=15, ans.back()[1]=10
15 <= 10? 否,不重叠
ans.push_back([15,18])
ans = [[1,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
三、完整代码实现
cpp
复制代码
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 按区间起点排序
sort(intervals.begin(), intervals.end());
vector<vector<int>> ans;
for (auto& interval : intervals) {
// 如果当前区间起点 <= 上一个区间终点,说明有重叠
if (!ans.empty() && interval[0] <= ans.back()[1]) {
// 合并:终点取两区间中较大的值
ans.back()[1] = max(interval[1], ans.back()[1]);
} else {
// 无重叠,直接加入
ans.emplace_back(interval);
}
}
return ans;
}
};
四、逐行解析
cpp
复制代码
sort(intervals.begin(), intervals.end());
- 按区间起点升序排列
- 排序后,相邻区间才有可能是需要合并的区间
- 如果不排序,需要 O(n^2) 的两两比较
cpp
复制代码
vector<vector<int>> ans;
cpp
复制代码
for (auto& interval : intervals) {
cpp
复制代码
if (!ans.empty() && interval[0] <= ans.back()[1]) {
!ans.empty() 检查结果集是否为空,避免访问空数组
interval[0] <= ans.back()[1] 检查当前区间起点是否小于等于上一个区间的终点
- 如果为真,说明两个区间重叠或相邻([1,3] 和 [3,5] 也算重叠)
cpp
复制代码
ans.back()[1] = max(interval[1], ans.back()[1]);
- 合并区间:终点取两个区间中较大的那个
- 例如 [1,3] 和 [2,6] 合并后,终点为 max(3, 6) = 6
cpp
复制代码
} else {
ans.emplace_back(interval);
}
五、关键点解释
| 条件 |
含义 |
示例 |
interval[0] <= ans.back()[1] |
当前区间起点 <= 上一个区间终点,有重叠 |
[1,3] 和 [2,6] |
interval[0] > ans.back()[1] |
当前区间起点 > 上一个区间终点,无重叠 |
[1,3] 和 [5,7] |
区间相邻 [1,3], [3,5] |
3 <= 3,也算重叠,需合并为 [1,5] |
- |
边界情况分析
| 情况 |
处理方式 |
示例 |
| 空数组 |
返回空 vector |
intervals = [] |
| 单个区间 |
直接返回 |
intervals = [[1,3]] |
| 区间完全不相交 |
全部保留 |
[[1,2],[5,6]] -> [[1,2],[5,6]] |
| 区间完全包含 |
保留最大的那个 |
[[1,5],[2,3]] -> [[1,5]] |
| 区间首尾相接 |
合并为一个区间 |
[[1,3],[3,5]] -> [[1,5]] |
六、与第 435 题(无重叠区间)对比
| 维度 |
第 56 题 合并区间 |
第 435 题 无重叠区间 |
| 问题类型 |
合并重叠区间 |
移除最少的区间使无重叠 |
| 操作 |
直接合并 |
需要计算移除数量 |
| dp 含义 |
无需 dp,贪心即可 |
dp[i] = 最多保留多少区间 |
| 贪心策略 |
按起点排序,合并重叠 |
按终点排序,选择不重叠的最多区间 |
| 时间复杂度 |
O(n log n) |
O(n log n) |
七、三种操作的贪心选择
复制代码
合并区间时,对于每个区间只有两种选择:
1. 与上一个区间合并
条件:interval[0] <= ans.back()[1]
操作:ans.back()[1] = max(ans.back()[1], interval[1])
2. 作为新区间加入
条件:interval[0] > ans.back()[1]
操作:ans.push_back(interval)
没有"删除"操作,因为合并区间是保留所有区间,只是合并重叠部分。
复杂度分析
| 方法 |
时间复杂度 |
空间复杂度 |
备注 |
| 排序 + 贪心 |
O(n log n) |
O(n) |
排序是瓶颈 |
| 模拟法 |
O(n^2) |
O(n) |
会超时 |
详细分析:
- 时间复杂度:排序 O(n log n) + 遍历 O(n) = O(n log n)
- 空间复杂度:结果集最坏情况 O(n)(所有区间都不重叠)
面试追问 FAQ
| 问题 |
回答要点 |
| Q: 为什么需要排序? |
排序后,相邻的区间才是可能需要合并的区间,否则需要 O(n^2) 的两两比较来判断重叠 |
| Q: 能否不排序? |
可以,但时间复杂度会变成 O(n^2),面试中不推荐 |
| Q: 如果要求原地修改怎么办? |
在原数组上直接修改,排序后遍历合并,结果写回原数组 |
| Q: 如何处理多维区间(如矩形合并)? |
按 x 坐标排序后遍历,再按 y 坐标检查是否能合并,较复杂 |
| Q: 如何处理区间有负数的情况? |
排序只关心相对大小,负数不影响,直接排序即可 |
| Q: 区间数量很大时如何优化? |
可以用扫描线算法,但本题贪心解法已经是 O(n log n) 最优 |
相关题目
| 题目编号 |
题目名称 |
难度 |
核心差异 |
| 56 |
合并区间 |
中等 |
基础题,合并重叠区间 |
| 435 |
无重叠区间 |
中等 |
删除最少的区间使无重叠 |
| 452 |
用最少数量的箭引爆气球 |
中等 |
排序后贪心,区间包含判断 |
| 986 |
区间列表的交集 |
中等 |
双指针求交集 |
| 1288 |
删除被覆盖的区间 |
中等 |
删除被覆盖的区间 |
总结
| 要点 |
内容 |
| 核心思想 |
先排序,后贪心合并 |
| 排序依据 |
按区间起点升序 |
| 关键判断 |
interval[0] <= ans.back()[1] 判断是否有重叠 |
| 合并方式 |
起点取较小值,终点取两区间中较大值 |
| 时间复杂度 |
O(n log n)(排序是瓶颈) |
| 空间复杂度 |
O(n)(结果集最坏情况) |
| 边界情况 |
空数组、单区间、全部重叠、全部不重叠 |
合并区间是经典的区间贪心问题,核心在于排序后只需要一次遍历即可完成合并,思路清晰,代码简洁。