1. 贪心算法核心思想解析
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优决策的算法策略。它不像动态规划那样考虑全局最优解,而是通过局部最优的累积来逼近全局最优。这种算法思想特别适合解决具有"最优子结构"特性的问题。
在实际应用中,贪心算法通常具有以下三个关键特征:
- 贪心选择性质:每一步的最优解都包含子问题的最优解
- 无后效性:当前决策不会影响后续决策
- 高效性:通常时间复杂度为O(n)或O(nlogn)
注意:贪心算法并不总是能得到全局最优解,只有在问题满足贪心选择性质时才适用。因此在使用前必须证明问题的贪心性质。
2. 买卖股票的最佳时机(121题)
2.1 问题分析与思路拆解
这道题要求我们在给定的股票价格序列中找到一次买入和卖出的最佳时机,使得利润最大化。关键在于我们只能在买入之后卖出,且只能进行一次交易。
贪心策略的核心在于:
- 维护一个"历史最低价"变量
- 每天计算如果今天卖出能获得的利润
- 不断更新最大利润
这种方法的正确性在于:要想在某天卖出时获得最大利润,必然是在此之前的最低点买入。因此我们只需要在遍历时不断更新这两个值即可。
2.2 代码实现与细节解析
java复制class Solution {
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE; // 初始化历史最低价为极大值
int maxProfit = 0; // 初始化最大利润为0
for(int p : prices) {
if(p < minPrice) {
minPrice = p; // 更新历史最低价
}
maxProfit = Math.max(maxProfit, p - minPrice); // 计算当前利润并更新最大值
}
return maxProfit;
}
}
关键点说明:
minPrice初始化为Integer.MAX_VALUE确保第一个价格一定会被记录- 遍历时先更新
minPrice再计算利润,保证不会在同一天买卖 - 时间复杂度O(n),空间复杂度O(1)
实操心得:在实际编码面试中,这类问题常会有变种,比如允许多次交易或加入冷却期。理解这个基础版本的核心思想是解决所有变种的关键。
3. 跳跃游戏(55题)
3.1 算法思路与正确性证明
这个问题要求判断是否能够从数组起点跳到终点,其中每个元素表示在该位置可以跳跃的最大长度。
贪心策略:
- 维护一个"最远可达位置"变量
far - 遍历数组时,如果当前位置超过了
far,说明无法到达 - 否则更新
far为当前位置能跳到的最远距离
这种方法的正确性在于:只要能到达的位置覆盖了终点,就一定有解。我们不需要关心具体怎么跳,只需要关心最远能到哪。
3.2 代码实现与边界处理
java复制class Solution {
public boolean canJump(int[] nums) {
int far = 0;
for(int i = 0; i < nums.length; ++i) {
if(i > far) return false; // 当前位置不可达
far = Math.max(far, i + nums[i]); // 更新最远可达位置
if(far >= nums.length - 1) return true; // 提前终止判断
}
return true;
}
}
关键细节:
- 遍历时先检查可达性再更新
far,顺序很重要 - 添加提前终止条件优化性能
- 处理空数组和单元素数组的特殊情况
常见错误:初学者容易忘记处理数组长度为1的情况,或者把
i + nums[i]写成nums[i]。在面试中要特别注意这些边界条件。
4. 跳跃游戏II(45题)
4.1 最小跳跃次数算法设计
这道题是跳跃游戏的进阶版,要求找到到达终点的最小跳跃次数。我们需要更精细地维护跳跃的边界信息。
贪心策略:
- 维护当前跳跃范围的边界
end - 维护下一跳能到达的最远位置
far - 当遍历到当前边界时,必须进行一次跳跃
- 跳跃次数
steps在每次跨越边界时增加
这种方法的正确性基于:在每一跳的可达范围内选择能带我们走最远的那个点作为下一跳的起点。
4.2 代码实现与性能优化
java复制class Solution {
public int jump(int[] nums) {
int n = nums.length;
int end = 0; // 当前跳的边界
int far = 0; // 下一跳的最远边界
int steps = 0; // 记录步数
for(int i = 0; i < n - 1; ++i) { // 注意是小于n-1
far = Math.max(far, i + nums[i]);
if(i == end) {
steps++;
end = far;
}
}
return steps;
}
}
优化点说明:
- 只遍历到n-2,因为到达终点不需要再跳
- 每次只在必须跳的时候才增加步数
- 时间复杂度O(n),空间复杂度O(1)
调试技巧:可以用小数组如[2,3,1,1,4]手动模拟算法执行过程,验证每个变量的变化是否符合预期。
5. 划分字母区间(763题)
5.1 问题转化与预处理
这道题要求将字符串划分为尽可能多的片段,使得每个字母最多出现在一个片段中。这实际上是一个区间划分问题。
解题步骤:
- 预处理:记录每个字符最后出现的位置
- 扫描时动态维护当前片段的边界
- 当扫描位置与当前边界重合时进行划分
5.2 完整实现与字符处理
java复制class Solution {
public List<Integer> partitionLabels(String s) {
int[] last = new int[26]; // 记录每个字母最后出现的位置
int n = s.length();
for(int i = 0; i < n; ++i) {
last[s.charAt(i) - 'a'] = i; // 更新最后出现位置
}
int start = 0, end = 0;
List<Integer> res = new ArrayList<>();
for(int i = 0; i < n; ++i) {
end = Math.max(end, last[s.charAt(i) - 'a']); // 扩展当前片段边界
if(i == end) { // 到达边界可以进行划分
res.add(end - start + 1);
start = i + 1; // 开始新的片段
}
}
return res;
}
}
关键细节:
- 使用长度为26的数组处理小写字母
end维护的是当前片段中所有字符的最远边界- 当
i == end时说明当前片段已经包含了所有相关字符的最后出现位置
性能考量:虽然使用了额外的O(26)空间存储最后出现位置,但这是常数空间。算法的时间复杂度是O(n),非常高效。
6. 贪心算法解题模板与技巧
6.1 通用解题框架
通过以上四道题,我们可以总结出贪心算法解题的一般步骤:
- 问题分析:确定问题是否具有贪心选择性质
- 贪心策略设计:找出局部最优的选择方式
- 正确性验证:证明局部最优能导致全局最优
- 实现优化:考虑边界条件和性能优化
6.2 常见错误与调试方法
在实际应用中,贪心算法容易遇到以下问题:
- 错误判断了问题的贪心性质
- 忽略了边界条件
- 变量更新顺序错误
调试建议:
- 使用小例子手动模拟算法执行
- 打印关键变量的中间值
- 特别注意循环的起始和终止条件
经验分享:在面试中,可以先提出暴力解法,然后分析如何用贪心优化。即使不能完全证明正确性,展示思考过程也很重要。
7. 贪心算法与其他算法的比较
7.1 贪心 vs 动态规划
贪心算法和动态规划都用于优化问题,但有以下区别:
- 贪心算法不回溯,动态规划考虑所有可能性
- 贪心算法通常更高效,但适用场景更有限
- 动态规划需要存储子问题的解,贪心算法不需要
7.2 贪心 vs 回溯
回溯法是暴力搜索的优化,而贪心是启发式算法:
- 回溯法保证找到解,但时间复杂度高
- 贪心算法效率高,但不保证最优解
- 回溯法适合组合问题,贪心适合优化问题
在实际开发中,我经常先用暴力解法理清思路,再尝试用贪心优化。特别是在处理时间序列或区间问题时,贪心算法往往能提供简洁高效的解决方案。