大家好,我是Simon郎,一个每天想要博学一点点的小青年!

今天为大家分享的是贪心算法。

贪心算法:贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解,从而使得得到的最终结果是全局最优或者接近于全局最优。

本文选取LeetCode比较有代表性的题目来学习贪心算法,选取的题目如下:

  • 1、分发饼干(455)

  • 2、分发糖果(135)

  • 3、无重叠区间(435)

  • 4、种花问题(605)

  • 5、用最少数量的箭引爆气球(452)

  • 6、划分字母区间(763)

  • 7、买卖股票的最佳时机(122)

  • 8、根据身高重建队列(406)

  • 9、非递减数列(665)

每道题目都有五部分组成:

  • 题目描述
  • 示例
  • 解题思路
  • 代码实现
  • 执行结果

1、分发饼干(455)

题目描述:

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例:

示例1:

输入: g = [1,2,3], s = [1,1] 

输出: 1 

解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。所以你应该输出1

示例2:

输入: g = [1,2], s = [1,2,3] 

输出: 2 

解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出2.

思路:

采用贪心算法:大尺寸的饼干能够满足胃口小的孩子,也能够满足胃口大的孩子,所以对于大尺寸的饼干要优先满足胃口大的孩子,而胃口小的孩子用小尺寸的饼干来喂,尽量减少浪费。

局部最优:充分利用饼干尺寸满足一个;全局最优:就是满足尽可能多的孩子

首先将饼干数组和孩子数组进行排序,然后从前往后依次进行比较。

胃口比饼干尺寸小,孩子被满足,继续遍历下一个孩子和下一块饼干; 胃口比饼干尺寸大,遍历下一块饼干。

代码:

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int child=0, cookie=0;
        while(child<g.length && cookie<s.length){
            if(g[child]<=s[cookie]) child++;
            cookie++;
        }
     return child;
    }
}

执行结果:

一举拿下贪心_贪心算法分发饼干

2、分发糖果(135)

题目描述:

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

示例:

示例 1:

输入:[1,0,2] 

输出:5 

解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:

输入:[1,2,2] 

输出:4 

解释:你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

解题思路:

相邻孩子,评分高的孩子必须获得更多的糖果,因此拆分成两个规则:

  • 规则定义:设学生A和学生B左右相邻,A在B的左边

左规则:当B>A时,B的糖果比A的数量多

右规则:当A>B时,则A的糖果比B的糖果数量多

相邻的学生中,评分高的学生必须获得更多的糖果 等价于 所有学生满足左规则且满足右规则。

  • 算法流程
  • 把所有孩子的糖果数初始化为 1。
  • 先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的 糖果数加 1。
  • 再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数 不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

代码:

class Solution {
    public int candy(int[] ratings) {
        //循环遍历两次
        int[] candy=new int[ratings.length];
        int count=candy.length;
        for(int i=1;i<ratings.length;i++){
            if(ratings[i-1]<ratings[i]){
                candy[i]=candy[i-1]+1;
            }
        }  
        for(int j=ratings.length-1;j>0;j--){
            if(ratings[j]<ratings[j-1]){
                candy[j-1]=Math.max(candy[j-1],candy[j]+1);
            }
        }
        for(int k=0;k<candy.length;k++){
            count+=candy[k];
        }
        return count;
    }
}

执行结果:

一举拿下贪心_数组_02分发糖果

3、无重叠区间(435)

题目描述:

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:可以认为区间的终点总是大于它的起点;区间[1,2]和[2,3]的边界相互接触,但没有相互重叠

示例:

示例 1:

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: [ [1,2], [1,2], [1,2] ]

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

输入: [ [1,2], [2,3] ]

输出: 0

解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

解题思路:

首先要对区间进行排序,本题采用的是对区间的头元素进行排序,然后在遍历空间

  • 如果后面区间的头小于当前区间的尾,比如当前区间是[3,6],后面区间是[4,5]或者是[5,9],说明这两个区间有重复,必须要移除一个,那么要移除哪个呢,为了防止在下一个区间和现有区间有重叠,我们应该让现有区间越短越好,所以应该移除尾部比较大的,保留尾部比较小的。
  • 如果后面区间的头不小于当前区间的尾,说明他们没有重合,不需要移除

代码:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        //对2*2数组按照左边界排序
        Arrays.sort(intervals,(a,b)->a[0]-b[0]);
        int end=intervals[0][1];
        int count=0;
        for(int i=1;i<intervals.length;i++){
            if(intervals[i][0]<end){
                //区间有重叠,选取右边界最小的数组
                end=Math.min(end,intervals[i][1]);
                count++;
            }else{
                //区间无重叠,直接更新就好
                end=intervals[i][1];
            }
          
        }
        return count;
    }
}

执行结果:

一举拿下贪心_字符串_03无重叠区

4、种花问题(605)

题目描述:

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

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

示例:

示例 1:

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

示例 2:

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

解题思路:

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

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

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

代码:

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
         //采用跳格子的思想解决
         //1,index为1,跳两格
         //2 index为0,证明前一个格子是0,只需要判断下一个格子是否为0
         //若为0,则n-1,
         //若为1,则跳三格
         for(int i=0;i<flowerbed.length && n>0;){
             if(flowerbed[i]==1){
                 i=i+2;
                 //一定要注意边界问题
             } else if(i==flowerbed.length-1 || flowerbed[i+1]==0){
                    n--;
                    i=i+2;
             }else{
                    i=i+3;
                }
         }
         return n<=0;
    }
}

执行结果:

一举拿下贪心_数组_04种花问题

5、用最少数量的箭引爆气球(452)

题目描述:

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

示例:

示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]] 输出:4

示例 3:

输入:points = [[1,2],[2,3],[3,4],[4,5]] 输出:2

示例 4:

输入:points = [[1,2]] 输出:1

示例 5:

输入:points = [[2,3],[2,3]] 输出:1

解题思路:

首先想到的思路是对数组中的区间值按左端点进行排序

  • 如果新气球的左端点大于射击区间的右边界,那么我们就需要重新开辟一个区间;

  • 如果新气球的左端点小于射击区间的右边界,这里又要分为两种情况:如果右端点大于射击区间的右边界,那么我们的射击区间的右边界无需变化;如果右端点小于射击区间的右边界,那么我们射击区间的右边界就要向左移动,以新气球的右端点为准,确立新的边界。

代码:

class Solution {
    public int findMinArrowShots(int[][] points) {
    //贪心算法,找出一支箭引爆最多的气球的
        if(points.length==0) return 0;
    //对二维数组按照左边界从小到大排休排序
    Arrays.sort(points,(a,b)->a[0]<b[0]?-1:1);
    int right=points[0][1];
    int count=1;
    for(int i=1;i<points.length;i++){
        if(points[i][0]>right){
            count++;
            right=points[i][1];
        }
        else{
            if(points[i][1]<right){
                right=points[i][1];
            }
        }
    } 
    return count;
    }
}

执行结果:

一举拿下贪心_字符串_05用最少数量的箭引爆气球

6、划分字母区间(763)

题目描述:

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

输入:S = "ababcbacadefegdehijhklij" 

输出:[9,7,8]

解释:划分结果为 "ababcbaca", "defegde", "hijhklij"。每个字母最多出现在一个片段中。像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

解题思路:

由于同一个字母只能出现在同一个片段,显然同一个字母的第一次出现的下标位置和最后一次出现的下标位置必须出现在同一个片段。因此需要遍历字符串,得到每个字母最后一次出现的下标位置。

在得到每个字母最后一次出现的下标位置之后,可以使用贪心的方法将字符串划分为尽可能多的片段,具体做法如下。

  • 从左到右遍历字符串,遍历的同时维护当前片段的开始下标start和结束下标end,初始化start=end=0。
  • 对于每个访问到的字母,得到当前字母的最后一次出现的下标位置end1,则当前片段的结束下标一定不会小于end1,因此,令end=max(end,end1)
  • 当访问到下标end 时,当前片段访问结束,当前片段的下标范围是 [start,end],长度为end−start+1,将当前片段的长度添加到返回值,然后令 start=end+1,继续寻找下一个片段
  • 重复上述过程,直至遍历完字符串

代码:

class Solution {
    public List<Integer> partitionLabels(String s) {
        //先生成一个长度为26的数组,用于存放每个的不同字符在序列中的最终位置
        int[] alphabet=new int[26];
        List list=new ArrayList<>();
        int start=0, end=0;
        for(int i=0;i<s.length();i++){
            alphabet[s.charAt(i)-'a']=i;
        }

        for(int i=0;i<s.length();i++){
            end=Math.max(end,alphabet[s.charAt(i)-'a']);
            if(end==i){
                list.add(end-start+1);
                start=end+1;
            }
        }
        return list;
    }
}

执行结果:

一举拿下贪心_字符串_06划分字母区间

7、买卖股票的最佳时机(122)

题目描述:

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例:

示例 1:

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

输出: 7 

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 

示例 2:

输入: prices = [1,2,3,4,5] 

输出: 4

解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 

示例 3:

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

输出: 0 

解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

解题思路:

  • 由于该题可以买卖无限次,只要第二天的价格大于前一天的价格我们就买前一天的股票,然后在第二天卖掉,这样我们就能从中获取利润;
  • 反之,如果第二天的价格小于前一天的价格我们就不进行买卖。

代码:

class Solution {
    public int maxProfit(int[] prices) {
        //
        int sum=0;
        int start=0, end=0;
        for(int i=1;i<prices.length;i++){
            if(prices[i]-prices[i-1]>0){
                sum+=prices[i]-prices[i-1];
            }
        }
        return sum;
    }
}

执行结果

一举拿下贪心_数组_07买卖股票的最佳时机

8、根据身高重建队列(406)

题目描述:

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例:

示例 1:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 

输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 

解释:编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] 

输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

解题思路:

一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。

在本题目中,首先对数对进行排序,按照数对的元素 1 降序排序,按照数对的元素 2 升序排序。

原因是:按照元素 1 进行降序排序,对于每个元素,在其之前的元素的个数,就是大于等于他的元素的数量,而按照第二个元素正向排序,我们希望 k 大的尽量在后面,减少插入操作的次数。

代码:

class Solution {
    public int[][] reconstructQueue(int[][] people) {
      // [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
        // 再一个一个插入。
        // [7,0]
        // [7,0], [7,1]
        // [7,0], [6,1], [7,1]
        // [5,0], [7,0], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
        Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0]);

        LinkedList<int[]> list = new LinkedList<>();
        for (int[] i : people) {
            list.add(i[1], i);
        }

        return list.toArray(new int[list.size()][2]);
    }
}

执行结果

一举拿下贪心_字符串_08根据身高重建队列

9、非递减数列(665)

题目描述:

给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。

我们是这样定义一个非递减数列的:对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。

示例:

示例 1:

输入: nums = [4,2,3] 输出: true 解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。

示例 2:

输入: nums = [4,2,1] 输出: false 解释: 你不能在只改变一个元素的情况下将其变为非递减数列。

解题思路:

本题是要维持一个非递减的数列,所以遇到递减的情况时(nums[i] > nums[i + 1]),要么将前面的元素缩小,要么将后面的元素放大。

但是本题唯一的易错点就在这,

如果将nums[i]缩小,可能会导致其无法融入前面已经遍历过的非递减子数列;如果将nums[i + 1]放大,可能会导致其后续的继续出现递减;所以要采取贪心的策略,在遍历时,每次需要看连续的三个元素,也就是瞻前顾后,遵循以下两个原则:

需要尽可能不放大nums[i + 1],这样会让后续非递减更困难;如果缩小nums[i],但不破坏前面的子序列的非递减性;算法步骤:

遍历数组,如果遇到递减:还能修改:

修改方案1:将nums[i]缩小至nums[i + 1]

修改方案2:将nums[i + 1]放大至nums[i];不能修改了:直接返回false

代码:

class Solution {
    public boolean checkPossibility(int[] nums) {
        if(nums.length==1) return true;
        boolean flag=nums[0]<=nums[1]?true:false;
        for(int i=1;i<nums.length-1;i++){
            if(nums[i+1]<nums[i]){
                if(flag){
                    if(nums[i+1]>=nums[i-1])
                        nums[i]=nums[i+1];
                    else
                        nums[i+1]=nums[i];
                    flag=false;
                }
                else 
                   return false;
            }
        }
        return true;
    }
}

执行结果

一举拿下贪心_结束坐标_09非递减数列

一举拿下贪心_i++_10