代码随想录自刷09:贪心算法

71 阅读14分钟

455. 分发饼干

思路:题目要求尽量满足更多小朋友的胃口,首先把小朋友的胃口和饼干,按照从小到大排序。

尽量让:和胃口相等(可以大于胃口)的饼干去满足小朋友。所以要从后面去遍历小朋友的胃口,饼干也是。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
      //饼干和胃口都进行排序(小到大)
      Arrays.sort(g);
      Arrays.sort(s);
      //饼干末尾下标
      int j=s.length-1;
      int sum=0;
      //从后遍历小朋友(先从胃口大的小朋友开始满足)
      for(int i=g.length-1;i>=0;i--){
        //尽量用和胃口相等的饼干满足小朋友
        if(j>=0 && s[j]>=g[i]){
          sum++;
          j--;
        }
      }
      return sum;
    }
}

376. 摆动序列

思路:第一个点,一定是波动点所以sum初始值为1。接下来需要一个变量记录之前的坡值,一个变量记录当前破值。

注意以下情况:

  • 前坡值>=0 当前坡值<0 符合波动,波动点数量+1
  • 前坡值<=0 当前坡值>0 符合波动,波动点数量+1
  • 前坡值=当前坡值,不符合波动,因此不会更新前坡值=当前坡值,此举动可以过滤平坡
class Solution {
    public int wiggleMaxLength(int[] nums) {
        if(nums.length<2)
            return nums.length;
        int preDiff=0;
        int curDiff=0;
        int sum=1;
        for(int i=1;i<nums.length;i++){
            curDiff=nums[i]-nums[i-1];
            if((preDiff>=0 && curDiff<0) || (preDiff<=0 && curDiff>0)){
                sum++;
                preDiff=curDiff;  //过滤平坡,只记录有波动的
            }
        }
        return sum;
    }
}

53. 最大子数组和

思路:如果相加遇到和小于0的情况,则重新开始计算,当前节点为新开始!还需要一个值随时记录当前连续最大和。因为到了结尾的时候dp[i]不一定是数组中最大的连续子序列和。

class Solution {
    public int maxSubArray(int[] nums) {
        // dp[i]表示包括i之前的最大连续子序列和
        int[] dp=new int[nums.length];
        dp[0]=nums[0];
        int sum=nums[0];
        for(int i=1;i<nums.length;i++){
            dp[i]=Math.max(nums[i],nums[i]+dp[i-1]);
            if(dp[i]>sum)
                sum=dp[i];
        }
        return sum;
    }
}

122. 买卖股票的最佳时机 II

思路:直接用动态规划的思维解题。用二维数组dp来记录:

  • dp[i][0]:第i天买入/持有股票的最多现金,可以从两个方面获得
  • dp[i][1]:第i天卖出/不持有股票的最多现金,可以从两个方面获得

dp[i][0]可以从:“昨天就已经买入或持有股票的现金”和“昨天卖出或不持有股票的现金-股票价”中选出最多现金 dp[i][1]可以从:“昨天就已经卖出或不持有股票的现金”和“昨天买入或持有股票的现金+股票价”中选出最多现金

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];  //第i天买入/持有股票的最多现金
        dp[0][1]=0;           //第i天 卖出/不持有股票的最多现金
        for(int i=1;i<prices.length;i++){
            //只能交易一次,所以是dp[i-1][0]和dp[i-1][1]
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return dp[prices.length-1][1];
    }
}

55. 跳跃游戏

思路:

一个变量curRange记录当前可以跳跃的范围,一个变量nextRange记录下次最大可跳跃范围。

循环遍历中,注意i不能超过curRange只能在这个范围内跳,每次i跳到新位置,都要记录新位置的nextRange(当前位置所能跳跃的最大范围)。

注意当i来到当前可跳跃范围末尾时,就要更新curRange了,把nextRange赋值给它。如果nextRange已经涵盖到数组末尾就可以直接return true。

如果i已经走到范围末尾,却还没到达数组末尾时,返回false。

class Solution {
    public boolean canJump(int[] nums) {
      if(nums.length<2){
        return true;
      }
      int curRange=nums[0];
      int nextRange=0;
      for(int i=1;i<=curRange;i++){
        nextRange=Math.max(nums[i]+i,nextRange);
        if(nextRange>=nums.length-1)
            return true;
        if(i==curRange){
                curRange=nextRange;//更新范围
        }
      }
      //失败,不能跳到结尾
      return false;
    }
}

45. 跳跃游戏 II

思路:

每次尽可能多跳远一点,跳到范围末尾了还没到终点,则增加一步,更新范围,然后继续重复。

如果下一步的最大覆盖范围涵盖了数组结尾,则表示下一步就能到达结尾,步数加一。

class Solution {
    public int jump(int[] nums) {
        if(nums == null || nums.length == 0 || nums.length == 1)
            return 0;
        int count=0;
        //当前最大覆盖范围
        int curRange=0;
        //下一步最大覆盖范围
        int nextRange=0;
        //从下标0开始
        for(int i=0;i<nums.length;i++){
            //当前位置的下一步最大覆盖范围
            nextRange=Math.max(nextRange,i+nums[i]);
            //下一步覆盖范围能涵盖结尾,所以只要+1步就可以结束了
            if(nextRange>=nums.length-1){
                count++;
                break;
            }
            //到达当前最大覆盖范围还没到结尾,更新下一步最大覆盖范围
            if(i==curRange){
                curRange=nextRange;
                count++;
            }
        }
        return count;
    }
}

1005. K 次取反后最大化的数组和

思路:

数组中有正有负,为了达到最大和,需要尽量把负数变成正数,注意要优先改变绝对值最大的负数!如果把数组中的负数都改完了k还有剩余,那就只对绝对值最小的元素做改变即可!

首先数组要按照绝对值大到小排列,接着改变数组中的负数,检查k是否有剩余,有就做处理,没有就直接计算和。

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        //按照绝对值大到小    
        for(int i=0;i<nums.length-1;i++){
            for(int j=0;j<nums.length-1-i;j++){
                if(Math.abs(nums[j+1])>Math.abs(nums[j])){
                    int tmp=nums[j];
                    nums[j]=nums[j+1];
                    nums[j+1]=tmp;
                }
            }
        }
        //System.out.println(Arrays.toString(nums));
        //把负数变成正数
        for(int i=0;i<nums.length;i++){
            if(nums[i]<=0 && k>0){
                nums[i]=-nums[i];
                k--;
            }
        }
        //如果k还有剩下,就都给数组的最后一个元素,他就算变成负数也是最小的值
        if(k%2!=0)
            nums[nums.length-1]=-nums[nums.length-1];
        return Arrays.stream(nums).sum();
    }
}

134. 加油站

思路:根据题目,知道总获得油量要大于总消耗量,这样才能确保一定有解,小于肯定无解。

  1. 所以需要记录跑完一圈的油量剩余多少,用变量curSum记录一路剩余的油量。 还需要一个变量min记录从当前起点出发,路程中油箱里的油量最小值(它是用来确定起点站的)。

  2. 我们必须先得知跑一圈下来的剩余油量,这个量不管从哪里开始跑结果都不会变,因此就先假设起点站是0开始,如果得知油量小于消耗则直接返回-1,否则表示一定有解。

  3. 接下来找出起点站,如果此时min大于等于0,那就表示现在假设的起点0就是真起点!返回0

  4. 如果小于0,则我们需要从非0站开始出发!从后往前找,看哪一站的剩余油量(gas[i]-cost[i])可以把min填平大于等于0时,当前站就是起点

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        //当前剩余油量
        int curSum=0;
        // 从当前起点出发,记录路程中油箱里的油量最小值
        int min=0;
        //先计算从0站出发,油箱里剩余的油量,和实时更新min
        for (int i = 0; i < gas.length; i++){
            curSum+=gas[i]-cost[i];
            min=Math.min(min,curSum);
        }
        //一圈计算下来,总油量小于消耗油量,不可能跑完一圈!
        if(curSum<0)
            return -1;
        
        //总油量大于消耗量,表示一定有解,接下来:找出出发的起点
        
        //正好从0站出发,路程中油箱都没出现负数,表示从0出发可以跑完一圈,是起点
        if(min>=0)
            return 0;
        
        //从后往前找起点,只要min不为负数就表示当前点是起点
        for(int i=gas.length-1;i>0;i--){
            //i站开往前一站的剩余油量
            min+=gas[i]-cost[i];
            if(min>=0)
                return i;
        }
        return -1;
    }
}

135. 分发糖果

思路:相邻孩子进行比较,可以想到用遍历来俩俩进行比较,但注意,一次遍历同时只能比较一边的评分,例如当前孩子和左边孩子/当前孩子和右边孩子,这样才不会混乱!

每个孩子获得的糖果可以先用数组保存起来。

1.假设先比较当前孩子和左边孩子的评分,第一次遍历前,大家都还没有糖果,第一位孩子先给一颗糖,接下来开始遍历,注意孩子从第二位开始:

  • 如果当前孩子>左边孩子,则当前孩子糖果数要比左边孩子多一颗
  • 否则当前孩子获得一颗糖(确保每个孩子都有一颗)

2.现在所有人都至少有一颗糖果,但是评分比较只比较了一半。接下来是当前孩子和右边孩子:

  • 要求最少糖果数,所以当前评分高的孩子糖果数如果已经比左边孩子多就不需要多一颗糖,

  • 否则取左孩子的糖果数+1即可

class Solution {
    public int candy(int[] ratings) {
        int[] candy=new int[ratings.length];
        candy[0]=1;
        //比较当前i孩子和左边朋友评分
        for(int i=1;i<=ratings.length-1;i++){
            //当前评分高的孩子糖果数要比评分低的多一颗
            if(ratings[i]>ratings[i-1])
                candy[i]=candy[i-1]+1;
            //每个孩子至少一颗糖果    
            else
                candy[i]=1;
        }
        //此时大家都至少有一颗糖,但是评分比较还不全,只比较了一半,接下来比较另一半
        //比较当前i孩子和右边朋友评分
        for(int i=ratings.length-2;i>=0;i--){
            if(ratings[i]>ratings[i+1]){
                candy[i]=Math.max(candy[i],candy[i+1]+1);
            }
        }
    
        return Arrays.stream(candy).sum();
    }
}

860. 柠檬水找零

思路:这题简单,一开始的零钱都为0,开始遍历数组,分析给的钱是5/10/20时分别要找多少钱,如果手中的零钱不够则找不出来返回false,否则对零钱数量进行加减。

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five=0;
        int ten=0;
        int twenty=0;
        for(int i : bills){
            if(i==5){
                five++;
            }
            else if(i==10){
                if(five<=0)
                    return false;
                five--;
                ten++;
            }
            else if(i==20){
            //两种零钱找法:10+5,5+5+5
                if(ten>0 && five>0){
                    five--;
                    ten--;
                    twenty++;
                }else if(five>2){
                    five-=3;
                    twenty++;
                }else  
                    return false;
            }
        }
        return true;
    }
}

406. 根据身高重建队列

思路:这题不是单纯的高到矮排序!而是每个人都要按照:我身高为h,前面有k个比我高或相同高的人,这样进行排序。所以要从两个维度来排序:身高高到矮,前面有k个人。

  1. 首先先对所有人的身高进行高到矮排序,如果遇到相同高的,则按照k的小到大排序
  2. 接着把人放进新的列表中,方便进行新排序,按照数组遍历元素,那么人要放在列表哪个位置呢?通过k得知每个人要插入的位置!
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        List<int[]> list=new LinkedList<>();
        Arrays.sort(people,(a,b)->{
            if(a[0]==b[0])
                return a[1]-b[1];   //身高相同,按照k小到大
            return b[0]-a[0];   //身高高到矮
        });
        for(int[] p : people){
            //放入的位置,元素
            list.add(p[1],p);
        }
        return list.toArray(new int[list.size()][]);
    }
}

452. 用最少数量的箭引爆气球

思路:建议画图更好理解,尽量找重叠的区段,并且箭放在重叠区段的末端,这样才保证能把重叠的气球都射掉。如果没有重叠则表示箭要加一!这样才能通过最少的箭射掉所有气球

class Solution {
    public int findMinArrowShots(int[][] points) {
        Arrays.sort(points,(a,b)->Integer.compare(a[0],b[0]));
        int count=1;
        for(int i=1;i<points.length;i++){
            if(points[i][0]<=points[i-1][1]){
                //更新重叠区段的末位置
                points[i][1]=Math.min(points[i][1],points[i-1][1]);
            }else{
                count++;
            }
        }
        return count;
    }
}

435. 无重叠区间

思路:跟上题思路其实差不多,这里只是要删除重叠的区间!用变量count记录删除掉的区间数量,变量一开始为0。接下来找有无重叠的区间,如果有则+1,然后修改区间。

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int count=0;
        Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
        for(int i=1;i<intervals.length;i++){
            if(intervals[i][0]<intervals[i-1][1]){
                count++;
                intervals[i][1]=Math.min(intervals[i-1][1],intervals[i][1]);
            }
        }
        return count;
    }
}

763. 划分字母区间

思路:找到当前片段中出现的最远下标,就是要切割的地方,但如何确定片段的范围呢?答:只要当前还没有走到当前片段中出现的最远下标,就表示都还在同一个片段中!

切割片段后,记得要更新下一个片段的起始位置

class Solution {
    public List<Integer> partitionLabels(String s) {
        List<Integer> list=new ArrayList<>();
        //记录每一个元素最后出现的下标位置
        int[] record=new int[26];
        for(int i=0;i<s.length();i++){
            record[s.charAt(i)-'a']=i;
        }
        //片段起始位置
        int left=0;
        //片段结束位置
        int right=0;
        //开始切割片段
        for(int i=0;i<s.length();i++){
            //记录当前片段中出现的最远下标
            right=Math.max(right,record[s.charAt(i)-'a']);
            //来到片段中出现的最远下标,可以切割片段了
            if(i==right){
                list.add(right-left+1);
                //更新下一个片段起始位置
                left=i+1;
            }
        }
        return list;
    }
}

56. 合并区间

思路:跟前面区间题目思路差不多,只是这里求的是合并后的新区间,所以需要集合暂存合并后的新区间和不重叠的区间,最后再转换成数组

class Solution {
    public int[][] merge(int[][] intervals) {
        if(intervals.length<2){
            return intervals;
        }
        Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
        //暂存合并后的区间
        List<int[]> list=new ArrayList<>();
        for(int i=1;i<intervals.length;i++){
            if(intervals[i-1][1]>=intervals[i][0]){
                //合并区间,以当前i坐标为主
                intervals[i][0]=Math.min(intervals[i-1][0],intervals[i][0]);
                intervals[i][1]=Math.max(intervals[i-1][1],intervals[i][1]);
            }else{
                //当前区间和上一个区间没有重叠,所以把上一个区间加入集合中,继续检查下一个区间和当前区间
                list.add(intervals[i-1]);
            }
        }
        //记得把最后一个区间加入集合中
        list.add(intervals[intervals.length-1]);
        //转换数组
        return list.toArray(new int[list.size()][]);
    }
}

738. 单调递增的数字

思路:俩俩比较,如果nums[i-1]>nums[i],则:

  • nums[i-1]要减一
  • nums[i]变为9

记住每个数字都要呈现单调递增,所以从后面向前遍历才能确保每个都是递增状态!

class Solution {
    public int monotoneIncreasingDigits(int n) {
        //记录标记为9的下标
        int flag=Integer.MIN_VALUE;;
        String str=String.valueOf(n);
        char[] nums=str.toCharArray();
        for(int i=nums.length-1;i>0;i--){
            if(nums[i-1]>nums[i]){
                nums[i-1]--;
                flag=i;
            }
        }
        //表示原来就是单调递增不用变
        if(flag==Integer.MIN_VALUE){
            return n;
        }
        //flag即之后的数字都改为9,这样答案就是小于n的最大数字并且是单调递增的
        for(int i=flag;i<nums.length;i++){
            nums[i]='9';
        }
        return Integer.valueOf(String.valueOf(nums));
    }
}

968. 监控二叉树

思路:要求最少摄像头数量就能监视整个树,那就尽量往父节点身上安装摄像头,它可以覆盖上下两层。

那就需要后序遍历树,从下到上,一个全局变量记录摄像头数量,返回值为每个节点的状态:

  • 0:本节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖
  • 注意:如果是空节点,则其状态为2,这样才能减少摄像头安装在叶子节点上!

此时父节点根据左右节点的返回状态来判断自身状态:

  • 左右节点都有覆盖,则父节点为无覆盖
  • 左右只要有一个为无覆盖,父节点必须加摄像头,因此数量+1
  • 左右只要有一个有加摄像头,则父节点为有覆盖
class Solution {
    int count=0;
    public int minCameraCover(TreeNode root) {
        int sum=backTraversal(root);
        //如果根节点为无覆盖,则必须要加一个摄像头,这样才能监视到根节点
        if(sum==0)
            count++;
        return count;

    }
    // 0:本节点无覆盖
    // 1:本节点有摄像头
    // 2:本节点有覆盖
    public int backTraversal(TreeNode root){
        //空节点当作有覆盖处理,避免在叶子结点加摄像头
        if(root==null)
            return 2;
        //获得左右节点的覆盖状态,以来决定父节点的状态
        int left=backTraversal(root.left);
        int right=backTraversal(root.right);
        //左右都是有覆盖情况,则父节点为无覆盖
        if(left==2 && right==2){
            return 0;
        }
        //左右只要有一个为无覆盖,父节点必须加摄像头,因此数量+1
        if(left==0 || right==0){
            count++;
            return 1;
        }
        //左右有一个有加摄像头,则父节点为有覆盖
        if(left==1 || right==1)
            return 2;
        return -1;   
        
    }
}