Problem: 56. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
文章目录
- 整体思路
- 完整代码
- 时空复杂度
-
- [时间复杂度:O(N log N)](#时间复杂度:O(N log N))
- 空间复杂度:O(N)
整体思路
这段代码旨在解决一个经典的区间问题:合并重叠区间 (Merge Intervals)。给定一个区间的集合,它需要合并所有重叠的区间,并返回一个不重叠的区间集合,该集合能覆盖输入的所有区间。
该算法的整体思路清晰且高效,主要依赖于 排序 和 单次遍历。其核心逻辑步骤如下:
-
排序是关键前提:
- 算法的第一步,也是最重要的一步,是对所有区间按照它们的 起始位置 进行升序排序。
- 目的 :排序后,我们就可以按顺序处理区间。当我们考虑一个新区间
current
时,任何可能与它重叠的区间,必然是结果集中我们刚刚处理过的最后一个区间last
。我们无需再关心last
之前的区间,因为它们的结束位置必然小于last
的结束位置,而last
的起始位置又小于等于current
的起始位置,所以current
不可能与last
之前的区间重叠。
-
遍历与合并逻辑:
- 算法使用一个动态列表
ans
来存储最终合并好的区间。这样做的好处是,我们不需要预先知道结果集的大小。 - 接着,遍历排序后的每一个区间
p
。对于每个区间,将其与ans
中的最后一个区间进行比较:- Case 1: 可合并(有重叠) 。如果
ans
不为空,并且当前区间p
的起始位置p[0]
小于或等于ans
最后一个区间的结束位置ans.get(m-1)[1]
,则说明它们有重叠。- 合并操作 :我们不添加新区间,而是更新
ans
中最后一个区间的结束位置。新的结束位置应该是原结束位置和当前区间p
结束位置p[1]
中的较大者。即new_end = Math.max(last_end, p[1])
。这确保了合并后的区间能完全覆盖原始的两个区间。
- 合并操作 :我们不添加新区间,而是更新
- Case 2: 不可合并(无重叠) 。如果
ans
为空,或者当前区间p
的起始位置大于ans
最后一个区间的结束位置,则说明它们之间没有重叠,p
是一个新的、独立的区间段的开始。- 添加操作 :直接将当前区间
p
添加到ans
列表的末尾。
- 添加操作 :直接将当前区间
- Case 1: 可合并(有重叠) 。如果
- 算法使用一个动态列表
-
结果转换:
- 遍历结束后,
ans
列表中就包含了所有合并后的、不重叠的区间。 - 最后,将这个
List<int[]>
转换为题目要求的int[][]
格式并返回。
- 遍历结束后,
完整代码
java
class Solution {
/**
* 合并所有重叠的区间。
* @param intervals 一个包含多个区间的二维数组,每个区间为 [start, end]。
* @return 一个不重叠的区间集合,覆盖了输入的所有区间。
*/
public int[][] merge(int[][] intervals) {
// ans: 用于动态存储合并后的结果区间。使用List因为最终区间数量不确定。
List<int[]> ans = new ArrayList<>();
// 核心步骤 1: 按区间的起始位置进行升序排序。
// 这是高效合并的前提。
// (p, q) -> p[0] - q[0] 是一个 lambda 表达式,用于定义比较器。
Arrays.sort(intervals, (p, q) -> p[0] - q[0]);
// 核心步骤 2: 遍历排序后的每一个区间
for (int[] p : intervals) {
// 获取当前结果集的大小
int m = ans.size();
// 判断当前区间 p 是否可以与结果集中的最后一个区间合并
// 条件:1. 结果集不为空 (m > 0)
// 2. 当前区间的起始 <= 最后一个区间的结束
if (m > 0 && p[0] <= ans.get(m - 1)[1]) {
// 合并操作:不添加新区间,而是更新最后一个区间的结束边界。
// 新的结束边界是原结束边界和当前区间结束边界中的较大值。
ans.get(m - 1)[1] = Math.max(ans.get(m - 1)[1], p[1]);
} else {
// 无重叠:将当前区间作为一个新的独立区间添加到结果集中。
ans.add(p);
}
}
// 核心步骤 3: 将 List<int[]> 转换为 int[][] 格式并返回。
return ans.toArray(new int[ans.size()][2]);
}
}
时空复杂度
时间复杂度:O(N log N)
- 排序 :
Arrays.sort(intervals, ...)
是算法中时间开销最大的部分。对一个包含N
个区间的数组进行排序,其平均和最坏情况下的时间复杂度为 O(N log N)。 - 遍历 :
for (int[] p : intervals)
循环遍历所有N
个区间一次。 - 循环内部操作 :
ans.size()
、ans.get(m-1)
、ans.add(p)
这些对ArrayList
的操作,在均摊意义下都是 O(1) 的。- 因此,整个循环体的时间复杂度为
N * O(1)
= O(N)。
综合分析 :
算法的总时间复杂度由各部分相加决定:O(N log N) (排序) + O(N) (遍历)。在 Big O 表示法中,我们只保留最高阶项,所以最终的时间复杂度是 O(N log N)。
空间复杂度:O(N)
- 主要存储开销 :
List<int[]> ans
:用于存储结果。在最坏的情况下,如果所有区间都互不重叠,那么ans
列表将需要存储所有N
个原始区间。因此,这部分空间是 O(N)。ans.toArray(...)
:这个操作会创建一个新的int[][]
数组,其大小也与ans
列表相同,为 O(N)。
- 排序的额外空间 :
- Java 中
Arrays.sort
对于对象数组使用的是 Timsort 算法。Timsort 是一种混合排序算法,其空间复杂度在最好的情况下是 O(1),在平均情况下是 O(log N),在最坏的情况下是 O(N)。
- Java 中
综合分析 :
算法所需的额外空间主要由存储结果的 ans
列表决定。考虑到最坏情况,空间复杂度为 O(N)。即使算上排序所需的空间,O(N) 仍然是主导项。
参考灵神