合并区间(LeetCode 56)
🔗 问题简介
题目链接:https://leetcode.cn/problems/merge-intervals?envType=problem-list-v2&envId=2cktkvj
📝 题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start_i, end_i]。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
🧪 示例说明
示例 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] 可被视为重叠区间。
💡 解题思路
✅ 核心思想:排序 + 贪心合并
- 排序:首先将所有区间按照起始位置升序排列。这样可以保证在遍历时,当前区间的起点不会小于前面任何区间的起点。
- 合并逻辑 :
- 初始化结果列表,将第一个区间加入。
- 遍历后续每个区间:
- 如果当前区间的起始位置 ≤ 结果中最后一个区间的结束位置 → 说明有重叠,合并(更新最后一个区间的结束位置为两者结束位置的最大值)。
- 否则 → 没有重叠,直接将当前区间加入结果列表。
💡 为什么排序后只需比较相邻?
因为排序后,若区间 A 与 C 重叠但 A 与 B 不重叠、B 与 C 不重叠,则 A 的 end < B 的 start ≤ C 的 start,所以 A.end < C.start,不可能重叠。因此只需顺序合并即可。
🔁 其他解法(对比)
| 方法 | 思路 | 时间复杂度 | 空间复杂度 | 是否推荐 |
|---|---|---|---|---|
| 排序 + 贪心(主流) | 如上所述 | O(n log n) | O(log n)(排序栈空间)或 O(n)(结果) | ✅ 强烈推荐 |
| 插入排序式合并 | 每次插入新区间时尝试合并已有区间 | O(n²) | O(n) | ❌ 效率低 |
| 扫描线算法 | 将所有端点标记为 start/end,扫描合并 | O(n log n) | O(n) | ⚠️ 可行但复杂 |
📌 结论:排序 + 贪心是最简洁高效的方法。
💻 代码实现
java
// Java 实现
import java.util.*;
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) return new int[0][2];
// 按照起始位置排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> merged = new ArrayList<>();
merged.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] current = intervals[i];
int[] last = merged.get(merged.size() - 1);
if (current[0] <= last[1]) {
// 重叠,合并
last[1] = Math.max(last[1], current[1]);
} else {
// 不重叠,添加新区间
merged.add(current);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
go
// Go 实现
import (
"sort"
)
func merge(intervals [][]int) [][]int {
if len(intervals) == 0 {
return [][]int{}
}
// 按照起始位置排序
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
merged := [][]int{intervals[0]}
for i := 1; i < len(intervals); i++ {
current := intervals[i]
last := merged[len(merged)-1]
if current[0] <= last[1] {
// 重叠,合并
last[1] = max(last[1], current[1])
} else {
// 不重叠,添加新区间
merged = append(merged, current)
}
}
return merged
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
🎯 示例演示
以 intervals = [[1,3],[2,6],[8,10],[15,18]] 为例:
- 排序后 :
[[1,3],[2,6],[8,10],[15,18]](已有序) - 初始化 :
merged = [[1,3]] - 处理 [2,6] :
- 2 ≤ 3 → 重叠 → 合并为
[1, max(3,6)] = [1,6] merged = [[1,6]]
- 2 ≤ 3 → 重叠 → 合并为
- 处理 [8,10] :
- 8 > 6 → 不重叠 → 添加
merged = [[1,6], [8,10]]
- 处理 [15,18] :
- 15 > 10 → 不重叠 → 添加
merged = [[1,6], [8,10], [15,18]]
✅ 最终结果正确。
✅ 答案有效性证明
我们需证明算法满足:
- 覆盖性 :输出区间覆盖所有输入区间。
- 每个输入区间要么被合并进某个输出区间,要么自身成为输出区间。
- 无重叠性 :输出区间两两不重叠。
- 因为只有当新区间起点 > 上一个区间终点时才添加,故任意两个相邻输出区间满足
prev.end < curr.start,自然无重叠。
- 因为只有当新区间起点 > 上一个区间终点时才添加,故任意两个相邻输出区间满足
- 最小性 :无法再合并。
- 若存在可合并的两个输出区间,则它们在排序后应相邻,但算法已确保相邻区间不可合并。
因此,算法正确。
📊 复杂度分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n log n) ------ 主要消耗在排序;合并过程为 O(n) |
| 空间复杂度 | O(log n)(Java/Go 排序使用的栈空间) + O(n)(存储结果) 通常说 O(n)(因结果必须返回) |
📌 注意:若原地修改允许,可优化空间,但本题要求返回新数组,故 O(n) 空间不可避免。
🧠 问题总结
- 关键技巧 :排序 + 贪心合并 是处理区间类问题的经典范式。
- 适用场景:所有"合并重叠区间"、"安排会议"、"最少箭引爆气球"等变种题。
- 易错点 :
- 忘记排序;
- 合并时未取
max(end)(如[1,4]与[2,3]合并应为[1,4],而非[1,3]); - 边界条件(空输入)。
💡 延伸思考:若区间是动态插入的(如在线算法),可考虑使用平衡二叉搜索树维护区间,但本题为离线处理,排序最优。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions