算法系列----贪心算法集锦,一套弄懂贪心算法!!

362 阅读9分钟

大家好,这里是白十七,今天给大家带来贪心算法集锦,本集锦参考

CS-Notes/Leetcode 题解 - 目录.md at master · CyC2018/CS-Notes (github.com)

大家一起进步!!有什么好的资源都可以发在评论区嗷!

455. 分发饼干 - 力扣(LeetCode)

先来到简单题开开胃,我刚上来就被开胃菜噎死了 ZAI49WXAXCY3`BIHIQN7E.png

每个孩子都有一个满足度 grid,每个饼干都有一个大小 size,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。

  1. 给一个孩子的饼干应当尽量小并且又能满足该孩子,这样大饼干才能拿来给满足度比较大的孩子。

  2. 因为满足度最小的孩子最容易得到满足,所以先满足满足度最小的孩子。

public int findContentChildren(int[] g, int[] s) {
            if (g == null || s == null) {
                return 0;
            }
            Arrays.sort(g);
            Arrays.sort(s);
            int gi = 0,si=0;
            while(gi<g.length&&si<s.length){
                if(g[gi]<=s[si]){
                    gi++;
                }
                si++;
            }
            return gi;
    }

435. 无重叠区间 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:计算让一组区间不重叠所需要移除的区间个数。

解决思路:先算出不重叠的区间数,最后让总数减去不重叠的,剩下的就是重叠的。

接下来就是怎么计算不重叠的区间数。

先排序,让右区间按照从小到大的序列排好,然后,取nums[i][0]nums[0][1]比较,如果大,就将nums[i][1]和nums[0][1]替换掉注意哦,是nums[i][1]和比较的不是一个值,比较用的是[i][0],也就是用遍历的左区间去和固定的右区间去比较。

如果固定的右区间大,就说明区间重复,继续,如果小,就说明没有重复,将nums[i][1]替换掉nums[0][1],然后让不重复的区间数加1,最后计算出不重叠的区间数,然后让总数组的大小减去区间不重叠的个数就ok。

详细代码如下

   public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length==0) {
            return 0;
        }
        Arrays.sort(intervals,Comparator.comparingInt(o->o[1]));
        //默认从1开始比较,节约时间
        int ans = 1;
        int end = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {
            //注意这里的<  不是<=  
            if (intervals[i][0]<end){
                continue;
            }
            end = intervals[i][1];
            ans++;
        }
        return intervals.length-ans;
    }

452. 用最少数量的箭引爆气球 - 力扣(LeetCode)

Input:
[[10,16], [2,8], [1,6], [7,12]]

Output:
2

题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都被刺破。求解最小的投飞镖次数使所有气球都被刺破。

也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。

这道题和上面的题目思想差不多,不过就是变成了找重复的集合。

还是一样,将右区间排序,然后对nums[0][1]和nums[i][0]做比较,唯一不同的就是比较的时候要看区间的开闭,这个题里面[1, 2] 和 [2, 3] 在本题中算是重叠区间,所以计算的时候要注意,然后赋值,总的来说和上面的差不多。

这里要注意一个细节就是 2>2? 结果为false;

show code

public int findMinArrowShots(int[][] points) {
    if (points.length == 0) {
        return 0;
    }
    Arrays.sort(points, Comparator.comparingInt(o->o[1]));
    int res = 1;
    int end = points[0][1];
    for (int i = 1; i < points.length; i++) {
        //看points[i][0]和end的比较结果,如果小于等于,那就不行。
        if (points[i][0]>end){
            res++;
            end = points[i][1];
        }
    }
    return res;
}

406. 根据身高重建队列 - 力扣(LeetCode)

题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。

这道题刚看的时候,有些懵,想着按照k排序之后,就不知道该怎么办了,还是没有对Arrays.sort()有深入的了解,

为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。

身高 h 降序、个数 k 值升序,然后将某个学生插入队列的第 k 个位置中。

public int[][] reconstructQueue(int[][] people) {
    if (people == null || people.length == 0 || people[0].length == 0) {
        return new int[0][0];
    }
    //(h, k),按照h的降序排列,按照k升序
    Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]));
    //排好之后是这样
     //    0 = {int[2]@483} [7, 0]
     //    1 = {int[2]@485} [7, 1]
     //    2 = {int[2]@487} [6, 1]
     //    3 = {int[2]@486} [5, 0]
     //    4 = {int[2]@488} [5, 2]
     //    5 = {int[2]@484} [4, 4]
    List<int[]> queue = new ArrayList<>();
    //然后精彩的来了,按照p[1]的大小,也就是他们的位置进行插入,太强了!!!神来之笔
    for (int[] p : people) {
        queue.add(p[1], p);
    }
    //最后这个也是个知识盲点,原来直接传个类型,就可以转换为对应的数组
    return queue.toArray(new int[queue.size()][]);
}

121. 买卖股票的最佳时机 - 力扣(LeetCode)

题目描述:一次股票交易包含买入和卖出,只进行一次交易,求最大收益。

一看到这个最先想到是双指针

public int maxProfit(int[] prices) {
       int left = 0;
        int right = prices.length-1;
        int minLeft = prices[left];
        int maxRight = 0;
        int res = 0;
        while (left<right){
            minLeft = Math.min(minLeft,prices[left]);
            maxRight = Math.max(maxRight,prices[right]);
            res = Math.max(res,maxRight-minLeft);
            right--;
            left++;
        }
        return res;
    }

但是报错了

image.png 为什么报错呢,因为他先left++,right就够不到4了,确实是个问题,解决的话,就更麻烦,换种思路:贪心。

public int maxProfit(int[] prices) {
        //贪心思想
        int max=0;
        int cur=prices[0];
        for(int p : prices){
            //如果比cur小则替换
            if(p<cur){
                cur=p;
                continue;
            }
            //如果比 cur 大则计算卖出最大获利
            if(p>cur){
                max=Math.max(max,p-cur);
            }

        }
        return max;
    }

这么一来又简洁,又易读。

下面是这道题的升级

122. 买卖股票的最佳时机 II - 力扣(LeetCode)

题目描述:可以进行多次交易,多次交易之间不能交叉进行,可以进行多次交易。

这个就是拆分。

输入: prices = [7,1,5,3,6,4]
输出: 7

结果就是5-1+6-3。

public int maxProfit(int[] prices) {
    int count = 0;
    for (int i = 1; i < prices.length; i++) {
        if (prices[i]>prices[i-1]){
            count+=(prices[i]-prices[i-1]);
        }
    }
    return count;
}

不行了,必须自己写一道,老看解析....

605. 种花问题 - 力扣(LeetCode)

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组  flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。

输入: flowerbed = [1,0,0,0,1], n = 1
输出: true

思路: 就是判断两边有没有0,如果有0就可以种,计算出最多种几朵花,然后和这个n比较就ok。

一开始代码长这样:

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    int count = 0;
    for (int i = 1; i < flowerbed.length; i++) {

        if ( flowerbed[i]==0 && i+1<flowerbed.length && flowerbed[i+1]==0 && flowerbed[i-1]==0){
            count++;
        }
    }
    return count>=n;
}

测试用例过了,然后提交,发现

{1, 0, 0, 0, 0, 1}, 2

这个过不了,因为没有把走过的设置为1,所以设置两个的时候也是true,因为都是0吗,所以修改一下 加了一行

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    int count = 0;
    for (int i = 1; i < flowerbed.length; i++) {

        if ( flowerbed[i]==0 && i+1<flowerbed.length && flowerbed[i+1]==0 && flowerbed[i-1]==0){
            count++;
            flowerbed[i]=1;
        }
    }
    return count>=n;
}

再次提交,又g了。

[0,0,1,0,1] 1

因为我们直接从1开始走的,所以从0开始走,特判一下第一个在。

然后又双叒g了。没考虑结尾

[1,0,0,0,1,0,0] 2

这次是这个用例g了。

当我修改好,信心慢慢的传上去。

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    int count = 0;
    for (int i = 0; i < flowerbed.length; i++) {
        if (i==0&& flowerbed[i]==0 && flowerbed[i+1]==0){
            count++;
            flowerbed[i]=1;
        }
        if ( flowerbed[i]==0 && i+1<flowerbed.length && flowerbed[i+1]==0 && flowerbed[i-1]==0){
            count++;
            flowerbed[i]=1;
        }
        if (i==flowerbed.length-1 && flowerbed[i-1]==0 && flowerbed[i]==0){
            count++;
            flowerbed[i]=1;
        }
    }
    return count>=n;
}

wc,又tm g了。

[1] 0

因为我们最后一个判断数组越界了。

再改。

加上

flowerbed.length>1

条件又g了。

[0] 1

又是数组越界。 真的顶啊,xdm,这像不像我们写项目的时候,莫名其妙的报错,考虑的不周啊!

再改一下初始条件。

终于啊!终于啊!家人们,终于成功了!太不容易了可是~

public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count = 0;
        for (int i = 0; i < flowerbed.length; i++) {
        if ((i==0&& flowerbed[i]==0 )&& (flowerbed.length>1 && flowerbed[i+1]==0 || flowerbed.length==1)){
            count++;
            flowerbed[i]=1;
        }
        if ( flowerbed[i]==0 && i+1<flowerbed.length && flowerbed[i+1]==0 && flowerbed[i-1]==0){
            count++;
            flowerbed[i]=1;
        }
        if (flowerbed.length>1&& i==flowerbed.length-1 && flowerbed[i-1]==0 && flowerbed[i]==0){
            count++;
            flowerbed[i]=1;
        }
    }
    return count>=n;
}

可是太不优雅了,而且考虑的情况太多,有没有什么简单的方法呢?

贴一份题解里面的,真是简洁优雅,真棒啊!给这个作者点个赞

2937F65E.png

题目要求是否能在不打破规则的情况下插入n朵花,与直接计算不同,采用“跳格子”的解法只需遍历不到一遍数组,处理以下两种不同的情况即可:

【1】当遍历到index遇到1时,说明这个位置有花,那必然从index+2的位置才有可能种花,因此当碰到1时直接跳过下一格。 【2】当遍历到index遇到0时,由于每次碰到1都是跳两格,因此前一格必定是0,此时只需要判断下一格是不是1即可得出index这一格能不能种花,如果能种则令n减一,然后这个位置就按照遇到1时处理,即跳两格;如果index的后一格是1,说明这个位置不能种花且之后两格也不可能种花(参照【1】),直接跳过3格。

当n减为0时,说明可以种入n朵花,则可以直接退出遍历返回true;如果遍历结束n没有减到0,说明最多种入的花的数量小于n,则返回false

public boolean canPlaceFlowers(int[] flowerbed, int n) {
	for (int i = 0, len = flowerbed.length; i < len && n > 0;) {
		if (flowerbed[i] == 1) {
			i += 2;
		} else if (i == flowerbed.length - 1 || flowerbed[i + 1] == 0) {
			n--;
			i += 2;
		} else {
			i += 3;
		}
	}
	return n <= 0;
}

作者:hatsune-miku-k
链接:https://leetcode.cn/problems/can-place-flowers/solution/fei-chang-jian-dan-de-tiao-ge-zi-jie-fa-nhzwc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

额~~~,好像离贪心有点远了,接着来看贪心怎么做的。

这么大体看,霍~也是不得了啊,简洁优雅,是我的菜。

结果一看一脸懵,md,看不懂啊。

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    int len = flowerbed.length;
    int cnt = 0;
    for (int i = 0; i < len && cnt < n; i++) {
        if (flowerbed[i] == 1) {
            continue;
        }
        int pre = i == 0 ? 0 : flowerbed[i - 1];
        int next = i == len - 1 ? 0 : flowerbed[i + 1];
        if (pre == 0 && next == 0) {
            cnt++;
            flowerbed[i] = 1;
        }
    }
    return cnt >= n;
}

细看之下,和我的思想差不多,不过人家这个简单多了。

怎么做的呢,首先定义好总数,cnt,这个变量不是很明白什么意思,直接整个count不好么。

然后就开始比,他这个for里面加的限制cnt大小是为了优化,可以省略。

接着就开始判断了,如果是1,直接下步,如果不是,找它的前一位和后一位。

他这个i的前一位和后一位的找寻,真是精妙,通过判断i的位置来省掉对数组越界的判断,比我们要轻巧简介多了,要素察觉.jpg。

然后在判断,通过了,就把该位置1,最后返回。

优雅,真优雅,我也要试着用这个写写。

392. 判断子序列 - 力扣(LeetCode)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

其实对这种,我都想直接用api。 我上来想到的就是map.containsKey,把数组toCharArray,然后两个for,结束战斗。

然后突然想到,不行,好像用int[26],来整更好。int的下标表示具体的字母,赋的值代表在哪里出现,就是具体的下标,然后通过对他们两个数组做运算就行,因为这样存,子序列的数组下标如果不大于母数组的话,他们的大小就位负数。

以ace,aec,abcde来说。

ace就是int[1,0,2,0,3],aec就是int[1,0,3,0,2],abcde就是int[1,2,3,4,5],一做运算就行。

接下来编写。

测试运行,报错。

public boolean isSubsequence(String s, String t) {
   int[] nums = new int[26];
    for (int i = 0; i < s.length(); i++) {
         nums[t.charAt(i)-'a']=i;
    }
    for (int i = 0; i < t.length(); i++) {
        if (nums[t.charAt(i)-'a']!=0 && nums[t.charAt(i)-'a']-i<0){
            return false;
        }
        nums[t.charAt(i)-'a']=i;
    }
    return true;
}
"axc" "ahbgdc"

因为t没有x,gg。

这样肯定是不行的

接下来看人家这个怎么实现的,用了String的indexof方法,返回字符串中字符的下标,传两个参数的话,第二个参数代表从指定位置开始检索,如果不存在,那么返回-1.

先定义号一个index,然后遍历我们的子串,判断子串中字符在母串中的位置,如果不存在,那么返回-1,如果存在,那么将index赋值到最新出现的母串中的位置。

然后接着下一轮的检索。优雅~

public boolean isSubsequence2(String s, String t) {
    int index = -1;
    for (char c : s.toCharArray()){
        index = t.indexOf(c, index+1);
        if (index == -1) {
            return false;
        }
    }
    return true;
}

665. 非递减数列 题解 - 力扣(LeetCode)

题目描述:判断一个数组是否能只修改一个数就成为非递减数组。

Input: [4,2,3]
Output: True
Explanation: You could modify the first 4 to 1 to get a non-decreasing array.

这道题,需要考虑两个点,一个是数组越界,什么时候取i-1,什么时候取i-2,还有一个点,就是判断比较修改前面的值还是修改后面的值。

比如[1,4,2,5][3,4,2,5]

[1,4,2,5]怎么改,直接将nums[i]=nums[i-1];

[3,4,2,5]呢,还将nums[i]=nums[i-1]?那么[3,2,2,5]肯定不符合题意,就要换个方法需要先判断一下nums[i-2]和nums[i]的比较关系,如果nums[i-2]>nums[i],那么我们就需要将nums[i]=nums[i-1];

知道了怎么回事儿之后,代码就好写了。

public boolean checkPossibility(int[] nums) {
    int count = 0;
    for(int i = 1;i<nums.length;i++){
        //如果符合条件直接下一步循环
        if(nums[i]>=nums[i-1]){
            continue;
        }
        //不符合,进行替换,并将改变的次数加1
        count++;
        if(i-2>=0 && nums[i-2]>nums[i]){
            //判断条件,如果i-2大于当前的数字,那么我们只能将nums[i-1]赋给nums[i],否则的话前面的数将会大于后面的数,判断会出错
            nums[i]= nums[i-1];
        }else{
        //如果不大于的话,直接将当前的nums[i]赋给nums[i-1],就ok
            nums[i-1]=nums[i];
        }
    }
    return count<=1;
}

53. 最大子数组和 - 力扣(LeetCode)

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

For example, given the array [-2,1,-3,4,-1,2,1,-5,4],
the contiguous subarray [4,-1,2,1] has the largest sum = 6.

这道题的话,首先想到是Math.max(res,sum),一个当前结果,一个总的返回结果。

然后就是看怎么加了。

肯定是要遍历所有数组的,整个sum,做的是当前结果求和,然后一个一个的加呗。

如果结果小于0了,就从当前位置重新开始遍历,从不为0的开始。

上代码

public int maxSubArray(int[] nums) {
      int ans = nums[0];
      int sum =0;
      for(int num: nums){
          if(sum>0){
              sum+=num;
          }else{
              sum = num;
          }
          ans = Math.max(ans,sum);
      }
      return ans;
    }

763. 划分字母区间 - 力扣(LeetCode)

Input: S = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation:
The partition is "ababcbaca", "defegde", "hijhklij".
This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.

一开始拿到这道题的时候,很懵,有些朦胧的想法,但是没有深入的具体的,你比如说,直到该去想找到该数组中字母的最后出现的位置,并拿这个数组中的数去和这个最后出现的位置去比较,然后再实现方面却有些困难。

后来看了看题解,嗷~~,差不多,只不过人家有了具体的思考和实现。

具体的如下:

1、遍历数组,找到最后出现的位置。
2、定义最后出现的位置和刚开始的位置
3、如果到字母最后位置的遍历中有字母的位置比当前的最后大的,就进行更新
4、直到i和最后的位置相等,将end-start+1存入list,并将startend进行更新。

具体代码如下:

 public List<Integer> partitionLabels(String s) {
        List<Integer> res = new ArrayList<Integer>();
        //记录最后出现位置
        int[] index = new int[26];
        for(int i = 0;i<s.length();i++){
            index[s.charAt(i)-'a']=i;
        }
        //记录开始和结尾
        int start =0,end = 0;
        for (int i = 0; i < s.length(); i++) {
            end = index[s.charAt(i)-'a']>end?index[s.charAt(i)-'a']:end;
            if (i==end){
                res.add(end-start+1);
                start = end+1;
            }
        }
        return res;
    }

记住,一定要先想后做,不要直接看答案,像我一样

14332CEC.gif

这样就老是忘,所以最好还是自己去思考,然后总结,加油吧,各位~