贪心算法其实就是没有什么规律可言 ,所以大家了解贪心算法 就了解它没有规律的本质就够了。不用花心思去研究其规律, 没有思路就立刻看题解。
基本贪心的题目 有两个极端,要不就是特简单,要不就是死活想不出来。
学完贪心之后再去看动态规划,就会了解贪心和动规的区别。
理论基础
贪心的本质是选择每一阶段的局部最优,从而达到全局最优
如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。
贪心一般解题步骤
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
贪心没有套路,说白了就是常识性推导加上举反例。
最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。
455.分发饼干
想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心。
为了满足更多的小孩,就不要造成饼干尺寸的浪费。
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
先遍历的胃口,再遍历的饼干
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。
class Solution {
//优先考虑胃口,先喂饱大胃口
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int size = s.length - 1;// 饼干数组的下标
for (int i = g.length - 1; i >= 0; i--) { // 遍历胃口
if (size >= 0 && s[size] >= g[i]) { // 遍历饼干
count++;
size--;
}
}
return count;
}
}
376. 摆动序列
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)
这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点
在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0
或者 prediff > 0 && curdiff < 0
此时就有波动就需要统计。
这是我们思考本题的一个大题思路,但本题要考虑三种情况:
- 情况一:上下坡中有平坡
- 情况二:数组首尾两端
- 情况三:单调坡中有平坡
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length == 1) return 1;
int prediff = 0;// 当前一对差值
int curdiff = 0;// 前一对差值
int result = 1;// 记录峰值个数,序列默认序列最右边有一个峰值
for (int i = 0; i < nums.length - 1; i++) {
curdiff = nums[i+1] - nums[i];//得到当前差值
// 出现峰值,摆动
//如果当前差值和上一个差值为一正一负
//等于0的情况表示初始时的preDiff
if ((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0)){
result++;
prediff = curdiff;// 注意这里,只在摆动变化的时候更新prediff
}
}
return result;
}
}
3. 最大子序和
暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值
贪心解法
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
区间终止位置不用调整么? 如何才能得到最大“连续和”呢?
区间的终止位置,其实就是如果 count 取到最大值了,及时记录下来了。相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)。
class Solution {
public int maxSubArray(int[] nums) {
int result = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < nums.length; i++) {
count += nums[i];
if (count > result) result = count;// 取区间累计的最大值(相当于不断确定最大子序终止位置)
if (count < 0) count = 0;// 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
return result;
}
}