1. 贪心算法基础与解题思路
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优决策的算法策略。它不像动态规划那样考虑全局最优解,而是通过局部最优的选择,希望最终达到全局最优。这种算法思想特别适合解决一些具有"最优子结构"特性的问题。
1.1 贪心算法的核心特征
贪心算法有效的三个关键条件:
-
贪心选择性质:每一步的局部最优选择能导致全局最优解。这意味着我们不需要回溯或重新考虑之前的选择。
-
最优子结构:问题的最优解包含其子问题的最优解。这与动态规划类似,但贪心算法不需要保存子问题的解。
-
无后效性:某个状态以前的过程不会影响以后的状态,只与当前状态有关。
在实际应用中,贪心算法常用于解决:
- 分配问题(如分发饼干)
- 区间调度问题
- 最短路径问题
- 数据压缩(如霍夫曼编码)
1.2 贪心算法的解题步骤
- 问题转化:将原问题分解为若干子问题
- 贪心策略:确定在每个子问题中做出选择的准则
- 证明有效性:证明该策略确实能得到全局最优解
- 算法实现:根据策略编写代码实现
注意:贪心算法并不总是能得到最优解,因此在应用前必须确认问题的特性是否适合贪心策略。当不确定时,可以通过举反例来验证策略的正确性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分发饼干问题解析(455题)
2.1 问题重述与理解
题目描述:假设你是一位家长,要给孩子们分发饼干。每个孩子i有一个胃口值g[i],每块饼干j的尺寸为s[j]。只有当饼干尺寸≥孩子胃口时,孩子才能得到满足。目标是尽可能满足更多的孩子。
示例:
- 输入:g = [1,2,3], s = [1,1]
- 输出:1
- 解释:只能满足胃口为1的孩子
2.2 贪心策略分析
这个问题可以抽象为一个匹配问题,我们需要找到最多的(g[i], s[j])对,其中s[j] ≥ g[i]。为了使满足的孩子数量最大化,我们需要考虑以下两种策略:
- 小饼干优先:将最小的饼干分配给能满足的最小胃口的孩子
- 大饼干优先:将最大的饼干分配给能满足的最大胃口的孩子
经过分析,两种策略都能得到最优解,但实现方式不同。我们选择"大饼干优先"策略,因为:
- 大饼干能覆盖更多可能性
- 排序后可以从数组尾部开始匹配,实现更直观
2.3 详细实现步骤
- 排序数组:将g和s数组分别排序
- 双指针初始化:index指向最大的饼干,i指向最大的胃口
- 贪心匹配:
- 如果当前饼干能满足当前胃口,计数+1,两个指针都前移
- 否则只移动胃口指针(尝试用当前饼干满足更小的胃口)
- 终止条件:任一数组遍历完成
cpp复制class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int index = s.size() - 1; // 饼干指针
int result = 0;
for (int i = g.size() - 1; i >= 0; i--) { // 从最大胃口开始
if (index >= 0 && s[index] >= g[i]) {
result++;
index--;
}
}
return result;
}
};
2.4 复杂度分析与优化
- 时间复杂度:O(nlogn + mlogm),主要由排序决定
- 空间复杂度:O(1),只使用了常数个额外变量
优化点:
- 可以提前终止循环:当饼干用完时直接返回结果
- 对小规模数据可以使用插入排序等简单排序算法
2.5 常见错误与注意事项
- 未排序直接匹配:这样无法保证最优解
- 指针移动错误:注意只有在成功匹配时才移动饼干指针
- 边界条件:
- 空数组处理
- 所有饼干都太小的情况
- 变量初始化:result必须初始化为0
提示:在实际面试中,可以讨论两种策略的优劣,并解释为什么选择当前实现方式。
3. 摆动序列问题解析(376题)
3.1 问题理解与建模
摆动序列是指连续数字之间的差严格地在正负之间交替。例如[1,7,4,9,2,5]是一个摆动序列,因为差值(6,-3,5,-7,3)交替变化。我们的目标是找到给定数组中最长的摆动子序列的长度。
这个问题可以转化为寻找序列中的"峰"和"谷"点。我们需要跳过单调变化的部分,只保留方向变化的点。
3.2 贪心策略设计
关键观察:
- 摆动序列的长度等于序列中波峰和波谷的数量+1
- 我们需要记录当前趋势(preDiff)和新的趋势(curDiff)
- 当趋势发生变化时,说明找到了一个新的极值点
具体策略:
- 初始化preDiff为0,result为1(单个元素也算摆动序列)
- 遍历数组,计算当前差值curDiff
- 当发现趋势变化时(preDiff和curDiff符号相反),增加计数并更新preDiff
- 忽略平坡情况(curDiff=0)
3.3 代码实现与解析
cpp复制class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int curDiff = 0;
int preDiff = 0;
int result = 1; // 默认最右侧有一个峰值
for (int i = 0; i < nums.size() - 1; i++) {
curDiff = nums[i + 1] - nums[i];
// 出现峰值
if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
result++;
preDiff = curDiff; // 只在摆动变化时更新preDiff
}
}
return result;
}
};
3.4 边界情况处理
- 空数组或单元素数组:直接返回数组长度
- 平坡情况:如[2,2,2,2],应该返回1
- 两个不同元素:如[2,5],应该返回2
- 单调递增/减序列:如[1,2,3,4],应该返回2
3.5 算法优化与变种
- 空间优化:可以只记录前一个趋势的符号,而不需要存储具体差值
- 并行计算:对于超大数组,可以考虑分块处理
- 变种问题:如果要求返回摆动序列本身而非长度,需要额外存储极值点
注意事项:在更新preDiff时,必须只在检测到摆动变化时才更新,否则会错过后续的摆动。这是算法中最容易出错的地方。
4. 最大子序和问题解析(53题)
4.1 问题描述与理解
给定一个整数数组nums,找到一个具有最大和的连续子数组(至少包含一个元素),返回其最大和。
示例:
- 输入:[-2,1,-3,4,-1,2,1,-5,4]
- 输出:6
- 解释:连续子数组[4,-1,2,1]的和最大,为6
4.2 暴力解法分析
最直观的方法是枚举所有可能的子数组,计算它们的和并找出最大值。
cpp复制class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT_MIN;
int count = 0;
for (int i = 0; i < nums.size(); i++) { // 起始位置
count = 0;
for (int j = i; j < nums.size(); j++) { // 结束位置
count += nums[j];
result = max(result, count);
}
}
return result;
}
};
复杂度分析:
- 时间复杂度:O(n²) - 对于每个起始点,遍历所有结束点
- 空间复杂度:O(1)
虽然直观,但对于大规模数据(n>10⁴)效率太低。
4.3 贪心算法解法
贪心策略:当连续和变为负数时,立即放弃当前子数组,从下一个元素重新开始计算。因为负数只会拉低后续的和。
算法步骤:
- 初始化最大和为最小整数,当前和为0
- 遍历数组:
- 累加当前元素到当前和
- 更新最大和
- 如果当前和<0,重置为0(放弃当前子数组)
- 返回最大和
cpp复制class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT_MIN;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
if (count > result) { // 更新最大和
result = count;
}
if (count <= 0) { // 当前和为负,重置
count = 0;
}
}
return result;
}
};
4.4 复杂度分析与比较
贪心解法:
- 时间复杂度:O(n) - 只需一次遍历
- 空间复杂度:O(1)
相比暴力解法有显著提升,适合处理大规模数据。
4.5 边界情况与注意事项
- 全负数数组:如[-2,-1,-3],应该返回最大的单个元素(-1)
- 包含零的数组:如[0,0,0,1,0],最大和为1
- 大数溢出:题目保证结果在int范围内,但实际工程中需要考虑
- 空数组:根据题意,题目保证数组非空
实用技巧:在面试中,可以先提出暴力解法,然后分析其不足,再引出贪心解法,展示思考过程。同时可以提及还有分治法和动态规划等其他解法,体现知识广度。
5. 贪心算法综合应用与比较
5.1 三个问题的共性分析
虽然这三个问题表面不同,但它们都体现了贪心算法的核心思想:
-
局部最优导致全局最优:每个问题都通过某种局部最优选择(最大饼干分配、趋势变化点选择、舍弃负和子数组)来达到全局最优解。
-
问题分解:都将原问题分解为一系列子问题,每个子问题做出一个贪心选择。
-
高效性:相比暴力解法,贪心算法都提供了更高效的解决方案。
5.2 贪心选择的差异性
- 分发饼干:贪心选择体现在"分配策略"上(大饼干优先)
- 摆动序列:贪心选择体现在"趋势变化点检测"上
- 最大子序和:贪心选择体现在"子数组取舍"上
5.3 实际应用场景
- 资源分配:类似分发饼干的问题出现在云计算资源调度、广告投放等场景
- 信号处理:摆动序列的思想可用于股票价格分析、传感器数据处理
- 数据分析:最大子序和问题在金融分析、基因组序列分析中有广泛应用
5.4 算法选择建议
- 当问题具有"贪心选择性质"时,优先考虑贪心算法
- 如果不确定,可以先尝试举反例验证贪心策略的正确性
- 当贪心算法不适用时,考虑动态规划或回溯等其他方法
6. 贪心算法解题模板与技巧
6.1 通用解题框架
- 问题分析:确定问题是否适合贪心算法
- 贪心策略设计:找出局部最优的选择标准
- 正确性证明:证明局部最优能导致全局最优
- 算法实现:编写高效代码
- 边界处理:考虑各种极端情况
6.2 代码实现技巧
- 排序预处理:许多贪心算法需要先对数据进行排序
- 双指针法:常用于处理有序数组的匹配问题
- 状态记录:如摆动序列中的preDiff和curDiff
- 提前终止:当确定无法改进结果时提前退出循环
6.3 调试与验证方法
- 小数据测试:手动计算几个小例子验证正确性
- 边界测试:空数组、单元素、全相同元素等情况
- 随机测试:生成随机数据与暴力解法对比
- 可视化:绘制数据图形帮助理解(如摆动序列的折线图)
6.4 常见错误警示
- 错误假设:未验证问题是否适合贪心策略
- 实现错误:贪心策略实现不完整或条件判断错误
- 边界遗漏:未考虑空输入、全负数等特殊情况
- 过早优化:在确保正确性前追求代码简洁性
在实际刷题和面试中,建议先从暴力解法入手,然后分析其不足,再逐步优化到贪心解法。同时要清楚地解释每一步选择的理由和算法的正确性证明。
