总结
贪心算法并没有固定的套路。刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
- 很多问题需要转化成另一个问题来解决
区间问题
【参考:贪心算法之区间调度问题 :: labuladong的算法小抄】
452、题
【参考:一文秒杀所有区间相关问题_labuladong_微信公众号】
56题
简单
455. 分发饼干
【参考:代码随想录# 455.分发饼干】
局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
- 优先考虑饼干,小饼干先喂饱小胃口
- 先用最小的饼干喂胃口最小的孩子
i 遍 历 饼 干 s , j 遍 历 胃 口 g i 遍历饼干s,j 遍历胃口g i遍历饼干s,j遍历胃口g
从代码中可以看出我用了一个j来控制饼干数组的遍历,遍历饼干并没有再起一个for循环,而是采用自减的方式,这也是常用的技巧。
有的同学看到要遍历两个数组,就想到用两个for循环,那样逻辑其实就复杂了。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int result=0;
int j=0;// 饼干数组g下标
// 遍历饼干 先用最小的小饼干喂胃口最小的孩子
for(int i=0;i<s.length ;i++){
// 饼干 >= 胃口
if(j<g.length && s[i]>=g[j]){
j++;
result++;
}
}
return result;
}
}
- 优先考虑胃口,先喂饱大胃口
- 先用最大的饼干喂胃口最大的孩子
class Solution {
public int findContentChildren(int[] g, int[] s) {
//大饼干找孩子
int result = 0;
Arrays.sort(g);
Arrays.sort(s);
int index = s.length - 1;
for(int i = g.length - 1; i >= 0; i--) {
if(index >= 0 && s[index] >= g[i]) {
index--;
result++;
}
}
return result;
}
}
53. 最大子序和
【参考:53. 最大子数组和 - 力扣(LeetCode)】
贪心法
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length==0) return 0;
int count=0,result=Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){
count += nums[i];
if(count > result){ // 取区间累计的最大值(相当于不断确定最大子序终止位置)
result=count;
}
if(count<0) count=0; // 相当于重置最大子序起始位置,因为count变成负数后再与后面的数相加一定会拉低总和
}
return result;
}
}
860. 柠檬水找零 ***
【参考:860. 柠檬水找零 - 力扣(LeetCode)】
【参考:代码随想录# 860.柠檬水找零】
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
局部最优:遇到账单20,优先消耗美元10,完成本次找零。
全局最优:完成全部账单的找零。
局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!
如果一直陷入想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。
class Solution {
public boolean lemonadeChange(int[] bills) {
int five=0, ten=0;// 纸币的数量
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
ten++;
if (five <= 0) {
return false;
}
five--;// 找零钱
}
else if (bill == 20) {
if (ten > 0 && five > 0) { // 找零钱
ten--;
five--;
} else if (ten <= 0 && five >= 3) { // 找零钱
five -= 3;
} else {
return false;
}
}
}
return true;
}
}
中等
122. 买卖股票的最佳时机 II ***
【参考:122. 买卖股票的最佳时机 II - 力扣(LeetCode)】
贪心算法最简单
当天卖出以后,当天还可以买入
只要今天比昨天大,就卖出。利润就可以增加
局部最优:收集每天的正利润,全局最优:求得最大利润。
class Solution {
public int maxProfit(int[] prices) {
int result=0;
for(int i=1;i<prices.length;i++){
if(prices[i]>prices[i-1]){ // 正利润
result+=prices[i]-prices[i-1];
}
}
return result;
}
}
55. 跳跃游戏 ***
【参考:55. 跳跃游戏 - 力扣(LeetCode)】
【参考:代码随想录# 55. 跳跃游戏】
其实跳几步无所谓,关键在于可跳的覆盖范围!
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
这个范围内,别管是怎么跳的,反正一定可以跳过来。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
局部最优推出全局最优,找不出反例,试试贪心!
i每次移动只能在cover的范围内移动,每移动一个元素,cover得到该元素数值(新的覆盖范围)的补充,让i继续移动下去。
而cover每次只取 max(该元素数值补充后的范围, cover本身范围)。
如果cover大于等于了终点下标,直接return true就可以了。
class Solution {
public boolean canJump(int[] nums) {
if (nums.length == 1) // 只有一个元素,就是能达到
return true;
int cover = 0;// 覆盖范围,即能跳到的范围
for (int i = 0; i <= cover; i++) {
cover = Math.max(i + nums[i], cover);// max(当前位置+跳数,cover本身范围)
if (cover >= nums.length - 1) // 说明可以覆盖到终点了
return true;
}
return false;
}
}
45. 跳跃游戏 II (难)
【参考:45. 跳跃游戏 II - 力扣(LeetCode)】
【参考:代码随想录# 45.跳跃游戏II】
不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数!
这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。
class Solution {
public int jump(int[] nums) {
if (nums.length == 1) return 0;
int count=0; //记录跳跃的次数
int curDistance = 0; //当前覆盖的最远距离下标
int nextDistance = 0; //下一步覆盖的最远距离下标
for (int i = 0; i < nums.length; i++) {
nextDistance = Math.max(nextDistance,i+nums[i]); //更新下一步覆盖的最远距离下标
//说明当前一步,再跳一步就到达了末尾
if (nextDistance>=nums.length-1){
count++;
break;
}
// 遇到当前覆盖的最远距离下标,就再走下一步
if (i==curDistance){
curDistance = nextDistance; // 更新当前覆盖的最远距离下标
count++;
}
}
return count;
}
}
376. 摆动序列(难)
【参考:python3-一图胜千言 - 摆动序列 - 力扣(LeetCode)】
只有真正的V型(波峰或者波谷位置)翻转才会增加摆动序列的长度。
用 trend 表示摆动序列最后的趋势:0代表未知(即初始状态),-1代表下降(波谷),1代表上升(波峰)。
因此,当 nums[i] > nums[i-1] & trend =-1 或者 nums[i] < nums[i-1] & trend = 1 时,摆动序列长度增加。
class Solution {
public int wiggleMaxLength(int[] nums) {
int result = 1;// 仅有一个元素或者含两个不等元素的序列也视作摆动序列
int trend = 0;
for (int i = 1; i < nums.length; i++) {
int diff = nums[i] - nums[i - 1];
if (diff > 0 && trend <= 0) { // 这里有等于号是为了满足trend的初始状态,下同
result++;
trend = 1;
} else if (diff < 0 && trend >= 0) {
result++;
trend = -1;
}
}
return result;
}
}
【参考:代码随想录# 376. 摆动序列】
这个思想和上面的一样,preDiff 相当于 trend
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums == null || nums.length <= 1) {
return nums.length;
}
//当前差值
int curDiff = 0;
//上一个差值
int preDiff = 0;
int count = 1;
for (int i = 1; i < nums.length; i++) {
//得到当前差值
curDiff = nums[i] - nums[i - 1];
//如果当前差值和上一个差值为一正一负
//等于0的情况表示初始时的preDiff
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
count++;
preDiff = curDiff;
}
}
return count;
}
}
134. 加油站(难)
【参考:代码随想录# 134. 加油站】
- 暴力法
for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
// i代表从哪个加油站出发
for (int i = 0; i < gas.length; i++) {
int rest = gas[i] - cost[i]; // 记录到达下一个加油站的剩余油量
int index = (i + 1) % gas.length; //记录到达哪个加油站
while (rest > 0 && index != i) {
rest += gas[index] - cost[index];
index = (index + 1) % gas.length;
}
// 回到起始位置
if (rest >= 0 && index == i)
return i;
}
return -1;
}
}
来自【代码随想录# 134. 加油站 贪心算法(方法二)】
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int totalSum = 0;
for (int i = 0; i < gas.length; i++) {
totalSum += gas[i] - cost[i];
}
// 全程获得的油不够消耗
if (totalSum < 0)
return -1;
// 此时说明一定可以走完全程
int curSum = 0;
int start = 0; // 记录从哪个加油站出发可以回到原点
// i遍历从哪个加油站出发
for (int i = 0; i < gas.length; i++) {
curSum += gas[i] - cost[i]; // 记录从start出发累加的剩余油量
// 从[0,i]出发的剩余油量不够消耗,所以都不能作为起始位置
if (curSum < 0) {
start = i + 1; // 起始位置从i+1开始算
curSum = 0;// 重新计算
}
}
return start;
}
}
452. 用最少数量的箭引爆气球 ***
【参考:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)】
【参考:代码随想录# 452. 用最少数量的箭引爆气球】代码没看懂
【参考:贪心算法之区间调度问题 :: labuladong的算法小抄】
class Solution {
public int findMinArrowShots(int[][] points) {
// 按照区间结尾排序
Arrays.sort(points, (int[] a, int[] b) -> {
// 这里不能只写 return a[1]-b[1]; 数大了会越界
if (a[1] > b[1]) {
return 1;
} else if (a[1] < b[1]) {
return -1;
} else {
return 0;
}
});
int count = 1;// 至少有一个区间不相交
// 排序后,第一个区间的右边界就是第一个气球直径的结束坐标
int x_end = points[0][1];
for (int i = 1; i < points.length; i++) {
int start = points[i][0];// 气球直径的起始点
if (start > x_end) {
count++;
x_end = points[i][1];
}
}
return count;// 返回不相交的区间数量
}
}
56. 合并区间
【参考:一文秒杀所有区间相关问题_labuladong_微信公众号】
【参考:代码随想录# 56. 合并区间】 代码没看懂
自己写的代码
class Solution {
public int[][] merge(int[][] intervals) {
int[][] result = new int[intervals.length][2];
// 按照区间起点排序
Arrays.sort(intervals, (int[] a, int[] b) -> {
if (a[0] > b[0]) {
return 1;
} else if (a[0] < b[0]) {
return -1;
} else {
return 0;
}
});
int index = 0;// 数组下标
int start = intervals[0][0];// 区间起点
int end = intervals[0][1];// 区间终点
for (int i = 1; i < intervals.length; i++) {
// 区间终点>= 下一个区间的起点
if (end >= intervals[i][0]) {
// (区间终点,下一个区间的终点)取最大值
end = Math.max(end, intervals[i][1]);
} else {
// 添加区间
result[index][0] = start;
result[index][1] = end;
index++;
// 重置区间的起点和终点
start = intervals[i][0];
end = intervals[i][1];
}
}
// 加入最后一个区间
result[index][0] = start;
result[index][1] = end;
index++;
// result[0,index)
return Arrays.copyOfRange(result,0,index);
}
}
738. 单调递增的数字
【参考:738. 单调递增的数字 - 力扣(LeetCode)】
暴力
n=853567367 超时
class Solution {
public int monotoneIncreasingDigits(int n) {
// 从大到小遍历
for (int i = n; i > 0; i--) {
if (checkNum(i)) {
return i;
}
}
return 0;
}
public boolean checkNum(int n) {
int max = 10;// 当前最大的数
// 数字从右向左遍历应该是单调递减的
while (n > 0) {
int t = n % 10;// 获得最低位数字
if (max < t) {
return false;
}
max = t;
n = n / 10;// 去除最低位
}
return true;
}
}
【参考:代码随想录# 738.单调递增的数字】
举例:45321
输出:44999
flag从数字5开始
class Solution {
public int monotoneIncreasingDigits(int n) {
if (n < 10) {
return n;
}
String s = String.valueOf(n);// int -> String
char[] chars = s.toCharArray();
// flag用来标记赋值9应该从哪里开始
// 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
int flag = chars.length;
// 从右向左遍历
for (int i = chars.length - 1; i > 0; i--) {
if (chars[i - 1] > chars[i]) {
flag = i;
chars[i - 1]--;
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < flag; i++) {
sb.append(chars[i]);
}
for (int i = flag; i < chars.length; i++) {
chars[i] = '9';
sb.append(chars[i]);
}
return Integer.valueOf(sb.toString());
}
}
881. 救生艇
【参考:救生艇 - 救生艇 - 力扣(LeetCode)】
先对 people 排序,然后用两个指针分别指向体重最轻和体重最重的人,按照上述规则来移动指针,并统计答案。
贪心法用双指针实现
class Solution {
public int numRescueBoats(int[] people, int limit) {
Arrays.sort(people);
int i=0,j=people.length-1;
int res=0;
while(i<=j){
// 每艘船最多可同时载两人
if(people[i]+people[j]<=limit){
res++;// 装(i,j)
i++;
j--;
}else{
res++;// 装j
j--;
}
}
return res;
}
}
困难
135. 分发糖果
【参考:代码随想录# 135. 分发糖果】
class Solution {
/**
分两个阶段
1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1
2、起点下标 ratings.length - 2 从右往左,
只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,
这样才符合 它比它左边的大,也比它右边大
*/
public int candy(int[] ratings) {
int[] candyVec = new int[ratings.length];
candyVec[0] = 1;
for (int i = 1; i < ratings.length; i++) {
if (ratings[i] > ratings[i - 1]) {
candyVec[i] = candyVec[i - 1] + 1;
} else {
candyVec[i] = 1;
}
}
for (int i = ratings.length - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
}
}
int ans = 0;
for (int s : candyVec) {
ans += s;
}
return ans;
}
}
【参考:分发糖果 (贪心思想,线性复杂度,清晰图解) - 分发糖果 - 力扣(LeetCode)】
class Solution {
public int candy(int[] ratings) {
int[] left = new int[ratings.length];
int[] right = new int[ratings.length];
Arrays.fill(left, 1);
Arrays.fill(right, 1);
for(int i = 1; i < ratings.length; i++)
if(ratings[i] > ratings[i - 1])
left[i] = left[i - 1] + 1;
int count = left[ratings.length - 1];// 结果
for(int i = ratings.length - 2; i >= 0; i--) {
if(ratings[i] > ratings[i + 1])
right[i] = right[i + 1] + 1;
count += Math.max(left[i], right[i]);
}
return count;
}
}
简单选择排序
public static void sort(int[] a) {
for (int i = 0; i < a.length; i++) {
int min = i; //假设关键值最小元素的数组下标为 min
//选出之后待排序中值最小的位置
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[min]) {
min = j;
}
}
//最小值不等于当前值时进行交换
if (min != i) {
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
// 递归
/*
第一次从a[1]开始,查找比a[0]小的元素,如果存在此元素,则将元素的位置信息记录下来,
运用此信息判断查找到的元素是否为a[0],如果不是,则将a[0]与此最小元素交换值的大小。
第二次从a[2]开始,查找比a[1]小的元素.......
........
进行n-1次后,算法结束。
*/
public void SelectSort(int a[],int n,int i){ // 第 i趟排序,i从0开始
int j,k,temp;
if(i==n-1) //n-1趟排序
return; //递归出口
k=i; // k用于存放关键值最小元素的数组下标
for(j=i+1;j<n;j++)
if(a[j]<a[k])//如果存在比a[k]小的数字
k=j; //将j位置信息记录下来,直到末尾,即
//k为从(i+1)到末尾的所有数字的最小值的元素位置
if(k!=i){ //将找到关键字最小的记录与数组下标为 k的记录交换
temp=a[i];
a[i]=a[k];
a[k]=temp;
}
SelectSort(a,n,i+1); //继续进行下一趟排序
}