【leetcode】贪心

总结

贪心算法并没有固定的套路。刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心

  • 很多问题需要转化成另一个问题来解决

区间问题

【参考:贪心算法之区间调度问题 :: labuladong的算法小抄

452、题

【参考:一文秒杀所有区间相关问题_labuladong_微信公众号
56题


简单

455. 分发饼干

【参考:455. 分发饼干 - 力扣(LeetCode)

【参考:代码随想录# 455.分发饼干

局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。

  • 优先考虑饼干,小饼干先喂饱小胃口
  • 先用最小的饼干喂胃口最小的孩子

i 遍 历 饼 干 s , j 遍 历 胃 口 g i 遍历饼干s,j 遍历胃口g isjg

从代码中可以看出我用了一个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. 摆动序列(难)

【参考:376. 摆动序列 - 力扣(LeetCode)

【参考: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. 加油站 - 力扣(LeetCode)

【参考:代码随想录# 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. 合并区间

【参考:56. 合并区间 - 力扣(LeetCode)

【参考:一文秒杀所有区间相关问题_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. 救生艇

【参考:881. 救生艇 - 力扣(LeetCode)

【参考:救生艇 - 救生艇 - 力扣(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. 分发糖果 - 力扣(LeetCode)

【参考:代码随想录# 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);  //继续进行下一趟排序 
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值