代码随想录|贪心|

分发饼干

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        //g代表胃口
        //s代表饼干
        //原则:大饼干喂大孩子,小饼干喂小孩子
        Arrays.sort(g);
        Arrays.sort(s);
        int count = 0;
        int index = s.length - 1;
        for(int i = g.length - 1; i >= 0; i--){
            if(index >= 0 && s[index] >= g[i]){
                count++;
                index--;

            }
        }
        return count;

    }
}

思路:

两层循环,外层放胃口,里层放饼干,这是因为每次循环胃口,我们都可以用当前的饼干判断一下是否可以满足当前的胃口,如果满足,饼干就可以index--,换成下一个饼干。如果当前的饼干没有办法满足当前的胃口,则胃口--,依次去看下一个的胃口有没有办法被满足。注意循环里面放的是if的逻辑,而不是while,如果是while的话,就会循环将所有的能满足当前胃口的饼干都分配给他。

 

摆动序列 

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (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;
    }
}

思路:

1. 正常摆动,prediff=nums[i] - nums[i-1],curdiff=nums[i+1] - nums[i]

prediff > 0 && curdiff < 0  ||  prediff < 0 && curdiff >0

2. 上下有平坡,比如 1-2-2-2-1

prediff >= 0 && curdiff < 0  ||  prediff < 0 && curdiff >= 0

3. 首尾元素,比如1-2

在这种首情况下,我们算为两个摆动,为了更好的写出代码逻辑,我们可以在1-2前面增加一个元素,比如1-1-2,在这种情况下,就符合prediff=0,curdiff>0

在末尾情况下,我们直接默认尾部有一个摆动 

4. 单调平坡中有平坡 比如1-2-2-2-3-4

解决这种情况,这就要求我们的prediff不能跟着我们的curdiff去变化,只有当出现摆动的时候,prediff才记录,所以preDiff = curDiff;应该放到if里面

最大子序列和

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 1){
            return nums[0]; //剪枝操作
        }
        int sum = Integer.MIN_VALUE;//sum用于记录最大值,所以先初始化为一个最小值
        int count = 0;
        for (int i = 0; i < nums.length; i++){
            count += nums[i];
            sum = Math.max(sum, count); // 取区间累计的最大值(相当于不断确定最大子序终止位置)
            if (count <= 0){
                count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
            }
        }
       return sum;
    }
}

如何通过局部最优推出全局最优?

如果碰到连续和为负数,则接着从下一个正数开始,因为如果从负数开始的话会拖累下一个正数,使两个数的和变小

只要连续和不是负数就继续往后加

count=0设置的很巧妙,这样相当于重置了最大子序列的起始位置,抛弃了之前的元素。

 

 

买卖股票的最佳时机 II

class Solution {
    public int maxProfit(int[] prices) {
        int result = 0;
        for (int i = 1; i < prices.length; i++) {
            result += Math.max(prices[i] - prices[i - 1], 0);//如果是负数则取0,否则相加
        }
        return result;
    }
}

思路: 

如何通过局部最优达到总体最优?

将每一天的利润都计算出来,只收集每天的正利润,然后加起来,就是总和能收集到的最大利润

跳跃游戏 

class Solution {
    public boolean canJump(int[] nums) {
        if (nums.length == 1) {
            return true;
        }
        //覆盖范围, 初始覆盖范围应该是0,因为下面的迭代是从下标0开始的
        int coverRange = 0;
        //在覆盖范围内更新最大的覆盖范围
        for (int i = 0; i <= coverRange; i++) {
            coverRange = Math.max(coverRange, i + nums[i]);
            if (coverRange >= nums.length - 1) {
                return true;
            }
        }
        return false;
    }
}

思路:

判断这个跳跃覆盖范围能否覆盖到最后一个元素

每次更新cover范围内的最大的更新范围coverRange = Math.max(coverRange, i + nums[i]);

假设我们有以下数组:

nums = [3,2,1,0,4]

我们希望确定是否能从数组的第一个元素跳到最后一个元素。

模拟过程

初始状态:

  • coverRange = 0(我们开始时只在数组的第一个位置)

第一步:

  • 当前位置 i = 0
  • nums[i] = 3(这意味着从这个位置最多可以跳3步)
  • 新的coverRange = Math.max(0, 0+3) = 3
  • 我们现在可以覆盖到数组的第四个元素

第二步:

  • 当前位置 i = 1
  • nums[i] = 2(但从这里跳2步并不能扩展我们的coverRange,因为我们已经可以跳到索引3了)
  • coverRange保持不变 = 3

第三步:

  • 当前位置 i = 2
  • nums[i] = 1(同样地,从这里跳1步并不能扩展我们的coverRange)
  • coverRange保持不变 = 3

第四步:

  • 当前位置 i = 3
  • nums[i] = 0(我们无法从这里跳跃)
  • coverRange保持不变 = 3

这时,我们的循环结束,因为我们已经遍历了所有在coverRange范围内的元素。注意,虽然coverRange等于3,但数组的最后一个元素的索引是4,因此我们并未覆盖整个数组。

跳跃游戏 II

class Solution {
    public int jump(int[] nums) {
        if (nums == null || nums.length == 0 || nums.length == 1) {
            return 0;
        }
        //记录跳跃的次数
        int count=0;
        //当前的覆盖最大区域
        int curDistance = 0;
        //最大的覆盖区域
        int maxDistance = 0;
        for (int i = 0; i < nums.length; i++) {
            //在可覆盖区域内更新最大的覆盖区域
            maxDistance = Math.max(maxDistance,i+nums[i]);
            //说明当前一步,再跳一步就到达了末尾
            if (maxDistance>=nums.length-1){
                count++;
                break;
            }
            //走到当前覆盖的最大区域时,更新下一步可达的最大区域
            if (i==curDistance){
                curDistance = maxDistance;
                count++;
            }
        }
        return count;
    }
}

用最少的步数尽可能覆盖更大的范围

  1. maxDistance(最大覆盖距离):

    • 这是在当前位置,你可以达到的最远距离。
    • 在遍历数组的过程中,maxDistance 是我们在当前位置及之前的位置中,可以达到的最远位置。
    • 它会随着遍历而更新:maxDistance = Math.max(maxDistance, i + nums[i])。这里的意思是,我们可以从当前位置 i 跳跃到 i + nums[i] 的距离,如果这个距离比之前计算的 maxDistance 还要远,我们就更新 maxDistance
  2. curDistance(当前覆盖的最大距离):

    • 这实际上代表的是在上一次跳跃后,可以达到的最远距离。
    • 当我们的遍历到的位置 i 等于 curDistance 时,意味着我们需要进行另一次跳跃,因为在当前的跳跃次数下,我们无法再前进更远了。
    • 每次当 i 达到 curDistance 时,我们知道下一步的最远距离就是 maxDistance,所以此时 curDistance 会更新为 maxDistance

简单来说,curDistance 是告诉我们,在当前这一跳中,我们最远能到哪里,而 maxDistance 则告诉我们,在考虑下一跳时,我们最远能到达的位置是哪里。当我们到达 curDistance 指示的位置时,我们知道必须进行另一次跳跃,所以跳跃计数器会增加,并且 curDistance 会更新为 maxDistance 来准备下一跳。

n次取反后最大数组合

class Solution {
    public int largestSumAfterKNegations(int[] nums, int K) {
    	// 将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
	nums = IntStream.of(nums)
		     .boxed()
		     .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
		     .mapToInt(Integer::intValue).toArray();
	int len = nums.length;	    
	for (int i = 0; i < len; i++) {
	    //从前向后遍历,遇到负数将其变为正数,同时K--
	    if (nums[i] < 0 && K > 0) {
	    	nums[i] = -nums[i];
	    	K--;
	    }
	}
	// 如果K还大于0,那么反复转变数值最小的元素,将K用完

	if (K % 2 == 1) nums[len - 1] = -nums[len - 1];
	return Arrays.stream(nums).sum();

    }
}

思路:两次贪心

1. 把数组按照绝对值的从大到小进行排序

2. 找到最小的非负整数,连续取反,把k消耗掉

3. 遍历数组,求和

  1. IntStream.of(nums): 这将整数数组nums转换为一个整数流。

  2. boxed(): 将基本数据类型的流(例如IntStream)转换为对象的流(在这种情况下是Stream<Integer>)。

  3. sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1)): 这是一个自定义排序,它基于整数的绝对值来排序数组。该排序将按绝对值的降序排列数组。例如,对于数组[-3, 1, -2],排序后的结果是[-3, -2, 1]

  4. mapToInt(Integer::intValue): 将Stream<Integer>转换回基本数据类型的流,即IntStream

  5. toArray(): 将流转换回数组。

加油站 

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int curSum = 0;
        int totalSum = 0;
        int index = 0;
        for (int i = 0; i < gas.length; i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {
                index = i + 1; 
                curSum = 0;
            }
        }
        if (totalSum < 0) return -1;
        return index;
    }
}

 思路:

如果车辆从A站出发并在B站中止(因为汽油不足),那么从A和B之间的任何加油站出发都不能到达B站。这意味着下一次尝试应该从B+1站开始

分发糖果

class Solution {
    /**
         分两个阶段
         1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1
         2、起点下标 ratings.length - 2 从右往左, 只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,这样才符合 它比它左边的大,也比它右边大
    */
    public int candy(int[] ratings) {
        int len = ratings.length;
        int[] candyVec = new int[len];
        candyVec[0] = 1;
        //从前往后
        for (int i = 1; i < len; i++) {
            candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1;
        }
        //从后往前
        for (int i = len - 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 num : candyVec) {
            ans += num;
        }
        return ans;
    }
}

思路:

1. 右孩子比左孩子得分高

2. 左孩子比右孩子得分高 

柠檬水找零

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;

        for (int i = 0; i < bills.length; i++) {
            if (bills[i] == 5) {
                five++;
            } else if (bills[i] == 10) {
                if(five == 0) return false;
                five--;
                ten++;
            } else if (bills[i] == 20) {
                if (ten > 0 && five > 0) {
                    ten--;
                    five--;
                    //20++不用统计
                } else if(five >= 3) {
                    five -= 3;
                    //20++不用统计
                }else{
                    return false;
                }
            }
        }
        
        return true;
    }
}

思路:

情况一:支付5,直接收下

情况二:支付10,如果有5,拿5进行找零

情况三:支付20,找零的方式有两种:10+5 或者 5+5+5

在这种情况下 优先选择10+5,因为5更加万能一些

根据身高重建队列

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        // 身高从大到小排(身高相同k小的站前面)
        Arrays.sort(people, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];   // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
            return b[0] - a[0];   //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
        });

        LinkedList<int[]> que = new LinkedList<>();//定义一个二维队列

        for (int[] p : people) {
            que.add(p[1],p);   //Linkedlist.add(index, value),根据k重新插入每个人应该在的位置。
        }

        return que.toArray(new int[people.length][]);
    }
}

思路:按照身高从大到小排序,确定身高维度,然后再确定k这个维度

用最少数量的箭引爆气球

/**
 * 时间复杂度 : O(NlogN)  排序需要 O(NlogN) 的复杂度
 * 空间复杂度 : O(logN) java所使用的内置函数用的是快速排序需要 logN 的空间
 */
class Solution {
    public int findMinArrowShots(int[][] points) {
        // 根据气球直径的开始坐标从小到大排序
        // 使用Integer内置比较方法,不会溢出
        Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));

        int count = 1;  // points 不为空至少需要一支箭
        for (int i = 1; i < points.length; i++) {
            if (points[i][0] > points[i - 1][1]) {  // 气球i和气球i-1不挨着,注意这里不是>=
                count++; // 需要一支箭
            } else {  // 气球i和气球i-1挨着
                points[i][1] = Math.min(points[i][1], points[i - 1][1]); // 更新重叠气球最小右边界
            }
        }
        return count;
    }
}

思路:

1.对气球的左边界/右边界进行排序

2. 不重叠的情况:如果气球i的左边界大于气球i-1的右边界,那么弓箭数量++

3. 重叠的情况:否则,更新气球i的右边界,与气球i-1的右边界进行比较,为了取得相邻两个气球的右边界的最小值,如此为了判断i+1气球是否也可以被一起射中

4. 最后返回弓箭的数量

无重叠区间

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals, (a,b)-> {
            return Integer.compare(a[0],b[0]);
        });
        int count = 0;
        for(int i = 1;i < intervals.length;i++){
            if(intervals[i][0] < intervals[i-1][1]){ //区间i的左边界小于等于区间i-1的右边界 证明 重叠
                count++;
                intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);  //更新右边界
            }   
        }
        return count;
    }
}

思路:

1.先按照左边界进行排序

2. 不重叠的情况:区间i的左边界大于等于区间i-1的右边界

3. 重叠的情况:区间i的右边界和区间i-1右边界取最小值然后和下一个区间的左边界进行比较看是否重叠

划分字母区间

class Solution {
    public List<Integer> partitionLabels(String S) {
        List<Integer> list = new LinkedList<>();
        int[] edge = new int[26];
        char[] chars = S.toCharArray();
        for (int i = 0; i < chars.length; i++) { //记录每一个元素最远出现的坐标
            edge[chars[i] - 'a'] = i;
        }
        int idx = 0;
        int left = 0;
        for (int i = 0; i < chars.length; i++) {
            idx = Math.max(idx,edge[chars[i] - 'a']);
            if (i == idx) {
                list.add(i - left + 1);
                left = i + 1;
            }
        }
        return list;
    }
}

思路:

1. 记录每个元素最后一次出现的位置

2. 遍历数组,不断更新最远的位置,当i == 最远距离的时候,证明这个区间可以结束了,然后把区间距离加入结果集

合并区间

class Solution {
    public int[][] merge(int[][] intervals) {
        LinkedList<int[]> res = new LinkedList<>(); //结果集
        Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0], o2[0]));
        res.add(intervals[0]); //先把第一个区间加入到结果集中
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] <= res.getLast()[1]) { //如果i区间与i-1区间重复
                int start = res.getLast()[0]; //区间的左边界 仍然是res里最后一个区间的左边界
                int end = Math.max(intervals[i][1], res.getLast()[1]); //右边界是i区间右边界与i-1区间右边界的最大值
                res.removeLast(); //去除res里最后一个区间
                res.add(new int[]{start, end});//换成新的区间
            }
            else {
                res.add(intervals[i]); //如果两个区间本身不重叠,直接把当前的区间加入到结果集即可
            }         
        }
        return res.toArray(new int[res.size()][]);
    }
}

思路:

1. 先对区间进行排序(左边界)

2. 如果i区间的左边界小于等于i-1区间的右边界,证明重叠

单调递增的数字 

class Solution {
    public int monotoneIncreasingDigits(int n) {
        String s = String.valueOf(n);
        char[] chars = s.toCharArray();
        int start = s.length();//当1234,可以直接return
        for (int i = s.length() - 1; i > 0; i--) {
            if (chars[i-1] > chars[i]) {//如果后面的数字大于前面的数字
                chars[i-1]--; 
                start = i;
            }
        }
        for (int i = start; i < s.length(); i++) {
            chars[i] = '9';
        }
        return Integer.parseInt(String.valueOf(chars));
    }
}

监控二叉树

class Solution {
    int  res=0;
    public int minCameraCover(TreeNode root) {
        // 对根节点的状态做检验,防止根节点是无覆盖状态 .
        if(minCame(root)==0){
            res++;
        }
        return res;
    }
    /**
     节点的状态值:
       0 表示无覆盖
       1 表示 有摄像头
       2 表示有覆盖
    后序遍历,根据左右节点的情况,来判读 自己的状态
     */
    public int minCame(TreeNode root){
        if(root==null){
            // 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
            return 2;
        }
        int left=minCame(root.left);
        int  right=minCame(root.right);

        // 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
        if(left==2&&right==2){
            //(2,2)
            return 0;
        }else if(left==0||right==0){
            // 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头
            // (0,0) (0,1) (0,2) (1,0) (2,0)
            // 状态值为 1 摄像头数 ++;
            res++;
            return 1;
        }else{
            // 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头,
            // 那么本节点就是处于被覆盖状态
            return 2;
        }
    }
}

思路:

优先在叶子节点的父节点放置摄像头,然后每隔两个节点放置一个摄像头,因为需要从下往上遍历二叉树,用后续遍历

一共三种状态:有摄像头,有覆盖,无覆盖

分别有三个数字来表示:

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖

 情况1:左右节点都有覆盖 

那么此时中间节点应该就是无覆盖的状态了,返回0

情况2:左右节点至少有一个无覆盖的情况

中间节点(父节点)应该放摄像头

情况3:左右节点至少有一个有摄像头

那么其父节点就应该是2(覆盖的状态)

情况4:头结点没有覆盖

以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,所以递归结束之后,还要判断根节点,如果没有覆盖,result++

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值