1. 问题背景与核心概念
今天想和大家分享一道经典的贪心算法题目——LeetCode 100题中的"划分区间"问题。这个问题看似简单,但蕴含着贪心算法的精髓,也是面试中经常被用来考察候选人算法思维能力的典型题目。
首先明确问题定义:给定一组区间,我们需要找到最少数量的区间,使得这些区间能够覆盖所有原始区间。换句话说,就是选择尽可能少的区间,让它们的并集能够包含所有原始区间的并集。
贪心算法(Greedy Algorithm)在这里的应用非常巧妙。贪心算法的核心思想是:在每一步选择中都采取当前状态下最优的选择,从而希望导致全局最优的结果。对于区间问题,常见的贪心策略包括按起始点排序、按结束点排序等。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 输入输出示例
为了更好地理解问题,我们先看一个具体例子:
输入:[[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
在这个例子中,区间[1,3]和[2,6]有重叠,可以合并为[1,6],而其他区间没有重叠,保持不变。
2.2 解题步骤详解
解决这个问题的标准贪心算法步骤如下:
- 首先将所有区间按照起始点进行排序
- 初始化一个结果列表,将第一个区间加入其中
- 遍历排序后的区间列表:
- 如果当前区间与结果列表中最后一个区间有重叠,则合并它们
- 否则,将当前区间加入结果列表
这个算法的关键在于如何判断两个区间是否重叠以及如何合并它们。两个区间[a,b]和[c,d]重叠的条件是c ≤ b,合并后的区间是[a, max(b,d)]。
3. Python实现与代码解析
3.1 完整代码实现
下面是用Python实现这个算法的完整代码:
python复制def merge(intervals):
if not intervals:
return []
# 按起始点排序
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
last = merged[-1]
# 检查是否有重叠
if current[0] <= last[1]:
# 合并区间
merged[-1] = [last[0], max(last[1], current[1])]
else:
merged.append(current)
return merged
3.2 代码逐行解析
-
intervals.sort(key=lambda x: x[0]):这是算法的第一步,按照区间的起始点进行排序。排序是贪心算法能够正确工作的前提。 -
merged = [intervals[0]]:初始化结果列表,先将第一个区间加入其中。 -
for current in intervals[1:]:从第二个区间开始遍历。 -
if current[0] <= last[1]:这是判断是否有重叠的关键条件。如果当前区间的起始点小于等于上一个区间的结束点,说明有重叠。 -
merged[-1] = [last[0], max(last[1], current[1])]:合并区间的操作,取两个区间起始点的最小值作为新起始点,结束点的最大值作为新结束点。
4. 算法复杂度分析
4.1 时间复杂度
这个算法的时间复杂度主要由排序操作决定。Python的sort()方法使用的是Timsort算法,其时间复杂度为O(n log n)。之后我们只进行了一次线性扫描,时间复杂度为O(n)。因此,总的时间复杂度是O(n log n)。
4.2 空间复杂度
空间复杂度取决于结果列表的大小。在最坏情况下(所有区间都不重叠),结果列表的大小与输入相同,因此空间复杂度是O(n)。如果允许修改输入列表,可以将空间复杂度优化到O(1),但通常不建议这样做。
5. 边界条件与特殊情况处理
5.1 空输入处理
代码中首先检查了输入是否为空:if not intervals: return []。这是一个良好的编程习惯,可以避免后续操作中出现异常。
5.2 单区间情况
当输入只有一个区间时,算法会直接返回该区间,这是正确的行为。
5.3 完全包含的区间
考虑这种情况:[[1,4],[2,3]]。我们的算法会正确处理这种情况,合并结果为[[1,4]],因为[2,3]完全包含在[1,4]中。
6. 贪心算法的正确性证明
为什么这个贪心算法是正确的?我们可以从以下几个方面来理解:
-
排序确保了我们可以按顺序处理区间,不会遗漏任何可能的合并机会。
-
每次合并都采取了最"贪心"的策略:尽可能延长当前区间的结束点,为后续的合并创造更多机会。
-
这种局部最优的选择最终会导致全局最优的解。因为如果我们不合并可以合并的区间,最终得到的区间数量一定会比合并后的多。
7. 实际应用场景
这种区间合并算法在实际中有很多应用场景:
-
日程安排:合并多个人的空闲时间,找出共同的空闲时段。
-
资源分配:合并相邻或重叠的资源块,提高资源利用率。
-
基因序列分析:合并相似的基因片段。
-
磁盘空间管理:合并空闲的磁盘块。
8. 常见错误与调试技巧
8.1 忘记排序
这是最常见的错误。如果不先排序,贪心算法就无法正确工作。例如对于输入[[1,4],[0,4]],如果不排序直接处理,会得到错误的结果。
8.2 错误的合并条件
另一个常见错误是合并条件写错。例如写成if current[1] > last[1],这会漏掉一些应该合并的情况。
8.3 调试技巧
当算法出现问题时,可以:
-
打印出排序后的区间列表,确认排序是否正确。
-
在合并步骤前后打印当前区间和结果列表,观察合并过程。
-
使用小规模的测试用例,手动模拟算法执行过程。
9. 算法优化与变种
9.1 原地合并
如果允许修改输入列表,可以尝试原地合并,减少空间使用:
python复制def merge(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
i = 0
for j in range(1, len(intervals)):
if intervals[j][0] <= intervals[i][1]:
intervals[i][1] = max(intervals[i][1], intervals[j][1])
else:
i += 1
intervals[i] = intervals[j]
return intervals[:i+1]
9.2 相关变种问题
-
区间插入:给定一组不重叠的区间和一个新区间,插入并合并必要的区间。
-
区间交集:找出两组区间的所有交集。
-
最少会议室:给定一组会议时间区间,计算需要的最少会议室数量。
10. 不同语言的实现对比
10.1 Java实现
java复制public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) {
return intervals;
}
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> result = new ArrayList<>();
int[] newInterval = intervals[0];
result.add(newInterval);
for (int[] interval : intervals) {
if (interval[0] <= newInterval[1]) {
newInterval[1] = Math.max(newInterval[1], interval[1]);
} else {
newInterval = interval;
result.add(newInterval);
}
}
return result.toArray(new int[result.size()][]);
}
10.2 C++实现
cpp复制vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
merged.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] <= merged.back()[1]) {
merged.back()[1] = max(merged.back()[1], intervals[i][1]);
} else {
merged.push_back(intervals[i]);
}
}
return merged;
}
11. 测试用例设计
全面的测试用例应该包括:
-
常规情况:
- 输入:[[1,3],[2,6],[8,10],[15,18]]
- 输出:[[1,6],[8,10],[15,18]]
-
完全包含的情况:
- 输入:[[1,4],[2,3]]
- 输出:[[1,4]]
-
完全不重叠的情况:
- 输入:[[1,2],[3,4]]
- 输出:[[1,2],[3,4]]
-
单个区间:
- 输入:[[1,2]]
- 输出:[[1,2]]
-
空输入:
- 输入:[]
- 输出:[]
-
边界情况:
- 输入:[[1,1],[2,2],[1,2]]
- 输出:[[1,2]]
12. 贪心算法的适用场景总结
贪心算法适用于满足贪心选择性质的问题,即局部最优解能导致全局最优解。区间问题通常满足这个性质,因为:
-
通过排序,我们可以确保处理的顺序性。
-
每次合并都是基于当前最佳选择。
-
这种选择不会影响后续的选择。
贪心算法的优点是通常效率较高,代码简洁;缺点是不是所有问题都适用,需要证明其正确性。
