1. 贪心算法核心思想与应用场景
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优决策的算法策略。这种局部最优解的累积最终希望能够达到全局最优解。与动态规划不同,贪心算法不会回溯之前的决策,这使得它在时间复杂度上通常具有优势。
贪心算法有效的两个关键条件:
- 贪心选择性质:局部最优解能导致全局最优解
- 最优子结构:问题的最优解包含其子问题的最优解
在实际编码面试中,贪心算法常用于解决以下几类问题:
- 区间调度问题(如本文的三个题目)
- 分配问题(如分糖果、任务分配)
- 图论中的最小生成树、最短路径问题
- 数据压缩(如哈夫曼编码)
提示:贪心算法虽然思路简单,但证明其正确性往往需要一定的数学基础。在实际面试中,如果无法严格证明,至少要通过多个测试用例验证。
2. 用最少数量的箭引爆气球(452题)
2.1 问题分析与解法思路
题目描述:在二维空间中有许多气球,输入是每个气球的直径起点和终点坐标。我们有一支弓箭,可以垂直x轴射出。如果箭的x坐标在气球的直径范围内,气球就会被引爆。求最少需要多少支箭才能引爆所有气球。
关键观察点:
- 气球在x轴上的投影是一个区间
- 箭只要射在区间重叠部分就能同时引爆多个气球
- 问题转化为:找出最少的点,使每个区间至少包含一个点
解法步骤:
- 将所有区间按照起点进行排序
- 初始化箭数为1(至少需要一支箭)
- 遍历排序后的区间,维护当前箭能覆盖的最右边界
- 如果新区间的起点超过当前最右边界,需要增加一支箭
- 否则更新最右边界为当前区间和最右边界中的较小值
2.2 代码实现与细节解析
java复制class Solution {
public int findMinArrowShots(int[][] points) {
// 按照起点升序排序
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
int ans = 1; // 至少需要一支箭
for (int i = 1; i < points.length; i++) {
if (points[i][0] > points[i - 1][1]) {
// 当前气球与前一气球无重叠,需要新箭
ans++;
} else {
// 更新当前箭能覆盖的最右边界
points[i][1] = Math.min(points[i][1], points[i - 1][1]);
}
}
return ans;
}
}
关键细节:
- 排序时使用
Integer.compare()而不是直接相减,避免整数溢出 - 初始箭数为1,因为至少需要一支箭
- 更新最右边界时取较小值,确保新边界能覆盖所有重叠区间
时间复杂度:O(nlogn)(排序占主导)
空间复杂度:O(logn)(排序所需的栈空间)
注意:测试用例中可能存在区间起点和终点非常大的情况,直接使用a[0]-b[0]可能导致整数溢出,这是常见的一个坑点。
3. 无重叠区间(435题)
3.1 问题转化与贪心策略
题目描述:给定一个区间集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
这个问题可以转化为:选择最多数量的不重叠区间。然后用总区间数减去这个最大数量,就是需要移除的最小数量。
贪心策略选择:
- 按照区间终点排序(而不是起点)
- 每次选择终点最小的区间,为后续留下更多空间
为什么按终点排序:
- 选择较早结束的区间,可以为后面留下更多空间
- 这种策略可以得到最大数量的不重叠区间
3.2 代码实现与边界处理
java复制class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) return 0;
// 按照区间终点升序排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
int count = 1; // 至少可以选择一个区间
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= end) {
// 找到下一个不重叠区间
count++;
end = intervals[i][1];
}
}
return intervals.length - count;
}
}
实现细节:
- 空输入的特殊处理
- 排序使用终点而非起点
- 维护当前选中区间的终点end
- 只有当新区间起点≥end时才计数
时间复杂度:O(nlogn)
空间复杂度:O(logn)
实操心得:这个问题的解法与箭射气球问题非常相似,但排序策略不同。理解为什么按终点排序能得到最优解是关键。可以画几个例子来验证这个策略的正确性。
4. 划分字母区间(763题)
4.1 问题分析与解决思路
题目描述:将字符串划分为尽可能多的片段,使得同一字母最多出现在一个片段中,并返回每个片段的长度列表。
关键观察:
- 需要知道每个字母最后出现的位置
- 当前片段的结束位置是片段内所有字母最后出现位置的最大值
- 当遍历到当前片段结束位置时,可以划分一个片段
解决步骤:
- 统计每个字母的最后出现位置
- 维护当前片段的开始和结束位置
- 遍历字符串,更新结束位置为当前字母最后出现位置的最大值
- 当遍历到结束位置时,记录片段长度
4.2 代码实现与优化
java复制class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> result = new ArrayList<>();
int[] lastIndex = new int[26]; // 记录每个字母最后出现的位置
// 第一次遍历:记录每个字母的最后出现位置
for (int i = 0; i < s.length(); i++) {
lastIndex[s.charAt(i) - 'a'] = i;
}
int start = 0, end = 0;
// 第二次遍历:划分区间
for (int i = 0; i < s.length(); i++) {
end = Math.max(end, lastIndex[s.charAt(i) - 'a']);
if (i == end) {
result.add(end - start + 1);
start = end + 1;
}
}
return result;
}
}
优化点:
- 使用固定大小的数组而不是HashMap来存储最后位置,提高效率
- 两次遍历:第一次记录位置,第二次划分区间
- 维护start和end指针来标记当前区间
时间复杂度:O(n)
空间复杂度:O(1)(字母表大小固定)
常见错误:忘记处理空字符串的情况(虽然题目保证非空);在更新end时没有取最大值;没有正确处理区间的开闭(是否需要+1)。
5. 贪心算法解题模板与技巧
5.1 区间类问题通用解法
对于大多数区间调度问题,可以遵循以下步骤:
- 排序:通常按照起点或终点排序
- 初始化关键变量(如箭数、不重叠区间数等)
- 遍历排序后的区间,根据特定条件更新状态
- 返回最终结果
排序选择原则:
- 如果需要覆盖所有区间(如射箭问题),通常按起点排序
- 如果需要选择最多不重叠区间,通常按终点排序
5.2 贪心算法证明技巧
虽然面试中不总是要求严格证明,但理解为什么贪心策略有效很重要。常用证明方法:
- 反证法:假设贪心解不是最优,导出矛盾
- 数学归纳法:证明对于任意n都成立
- 交换论证:将任意解逐步调整为贪心解而不恶化结果
5.3 调试与验证方法
贪心算法容易因边界条件出错,建议:
- 画图:在纸上画出区间分布,直观理解
- 极端测试用例:空输入、完全重叠区间、完全不重叠区间
- 打印中间变量:在关键步骤打印变量值验证逻辑
6. 面试实战建议
6.1 问题识别技巧
当遇到以下特征时,可考虑贪心算法:
- 问题可以分解为一系列决策
- 每个决策只需要考虑当前最优
- 不需要回溯之前的决策
6.2 代码编写规范
- 排序时注意比较器的写法,避免溢出
- 初始化变量要合理(如箭数初始为1还是0)
- 边界条件处理(空输入、单个元素等)
- 变量命名要有意义(如end、lastIndex等)
6.3 复杂度分析要点
贪心算法通常有两部分复杂度:
- 排序部分:O(nlogn)
- 遍历部分:O(n)
因此总复杂度通常是O(nlogn)
在面试中要能够清晰分析并表达这一点。
7. 相关题目拓展
为了巩固贪心算法的应用,建议练习以下LeetCode题目:
-
- 跳跃游戏
-
- 买卖股票的最佳时机 II
-
- 加油站
-
- 根据身高重建队列
-
- 种花问题
每道题目都有其独特的贪心策略,通过对比练习可以更好地掌握贪心算法的灵活应用。