leetcode系列-贪心

这篇博客探讨了贪心算法在LeetCode题目中的应用,包括简单、中等和难题级别的题目,如分发饼干、K次取反数组和、柠檬水找零等。文章详细分析了每道题目的贪心策略,例如优先考虑大饼干喂给胃口大的孩子,以最大化满足孩子数量,以及如何在有限的箭矢中引爆最多气球。通过这些案例,展示了贪心算法如何通过局部最优解得出全局最优解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图片

贪心简单题

以下三道题目就是简单题,大家会发现贪心感觉就是常识。是的,如下三道题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据!

455-分发饼干

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

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

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

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

图片

这个例子可以看出饼干9只有喂给胃口为7的小孩,这样才是整体最优解。

/**
 * 贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
 */
class Solution {
    public int findContentChildren(int[] g, int[] s) {

        Arrays.sort(g);
        Arrays.sort(s);

        int index = s.length - 1; // 饼干数组的下表
        int result = 0;

        for (int i = g.length - 1; i >= 0; i--) {

            if (index >= 0 && s[index] >= g[i]) {
                result++;
                index--;
            }
        }
        return result;
    }

}

复杂度分析

时间复杂度:O(nlogn):快排O(nlogn),遍历O(n),加一起就是还是O(nlogn)。

空间复杂度:O(1)

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

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

示例 3:
输入:A = [2,-3,-1,5,-4], K = 2
输出:13
解释:选择索引 (1, 4) ,然后 A 变为 [2,3,-1,5,4]。

本题思路其实比较好想了,如何可以让 数组和 最大呢?

贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。

局部最优可以推出全局最优。

那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。

那么本题的解题步骤为:

  • 第一步:将数组按照绝对值大小从大到小排序,「注意要按照绝对值的大小」
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和
/**
 *贪心算法
 *  第⼀步:将数组按照绝对值⼤⼩从⼤到⼩排序,注意要按照绝对值的⼤⼩
 *  第⼆步:从前向后遍历,遇到负数将其变为正数,同时K--
 *  第三步:如果K还⼤于0,那么反复转变数值最⼩的元素,将K⽤完
 *  第四步:求和
 */
class Solution {
    public int largestSumAfterKNegations(int[] A, int K) {
        int sum = 0;
        int n = A.length;
        Integer[] AA = new Integer[n];

        for(int i = 0; i < n; i++)
            AA[i] = A[i];              //new Integer(A[i]);自动装箱
            Arrays.sort(AA, (a,b)->{    //逆序排序
            return Math.abs(b) - Math.abs(a);
        });

        for (int i = 0; i < n; i++) {
            if (AA[i] < 0 && K > 0) {
                AA[i] *= -1;
                K--;
            }
        }

        if (K != 0) {
            AA[n - 1] = (K % 2 == 1) ? (-AA[n - 1]) : AA[n - 1];
        }

        for (int i = 0; i < n; i++) {
            sum += AA[i];
        }

        return sum;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

860-柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

思路:只需要维护三种金额的数量,5,10和20。

有如下三种情况:

  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5

所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。

/**
 * 贪心算法
 * 有如下三种情况:
 * - 情况一:账单是5,直接收下。
 * - 情况二:账单是10,消耗一个5,增加一个10
 * - 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
 */
class Solution {
    public boolean lemonadeChange(int[] bills) {


        int five = 0, ten = 0, twenty = 0;

        for (int bill : bills){

            //情况一:账单是5,直接收下。
            if (bill == 5) five++;

            //情况二:账单是10,消耗一个5,增加一个10
            if (bill == 10){
                if (five <= 0) return false;
                ten++;
                five--;
            }

            //情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
            if (bill == 20){

                // 优先消耗10美元,因为5美元的找零用处更大,能多留着就多留着
                if (five > 0 && ten > 0){
                    five--;
                    ten--;
                    twenty++;  // 其实这行代码可以删了,因为记录20已经没有意义了,不会用20来找零
                }else if (five >= 3){
                    five -= 3;
                    twenty++; // 同理,这行代码也可以删了
                }else{
                    return false;
                }
            }
        }

        return true;
    }
}

复杂度分析

时间复杂度:O(n),其中 n是 bills的长度

空间复杂度:O(1)

贪心中等题

376-摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。输出: 6

图片

「局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值」

「整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列」

​ 实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。

**注意:**当nums.length>1,则至少都会有1个数字可以当作是摆动序列,所以result的初始值为1。

class Solution {
    public int wiggleMaxLength(int[] nums) {

        if (nums.length <= 1) return nums.length;

        int curDiff = 0; // 当前一对差值

        int preDiff = 0; // 前一对差值

        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值,至少有1个数能保证是摆动序列

        for (int i = 1; i < nums.length;i++){

            curDiff = nums[i] - nums[i - 1];

            if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)){    // 出现峰值

                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

738-单调递增的数字

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]–,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数

全局最优:得到小于等于N的最大单调递增的整数

那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

class Solution {
    public int monotoneIncreasingDigits(int n) {

        char[] arr = Integer.toString(n).toCharArray();

        // flag用来标记赋值9从哪里开始
        // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
        int flag = arr.length;

        for (int i = arr.length - 1; i > 0;i--){

            if (arr[i - 1] > arr[i]){
                flag = i;
                arr[i - 1]--;
            }
        }
        for (int i = flag;i < arr.length;i++){

            arr[i] = '9';
        }

        return Integer.parseInt(new String(arr));

    }
}

复杂度分析

时间复杂度:O(logN)。数字为 N,位数为 logN。遍历一遍即可出结果。
空间复杂度:O(logN)。用于存储 char 数组。

贪心解决股票问题

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

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

本题首先要清楚两点:

  • 只有一只股票!
  • 当前只有买股票或者买股票的操作

想获得利润至少要两天为一个交易单元。

图片

假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

「此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!」

第一天是没有利润的,至少要第二天才会有利润,所以利润的序列比股票序列少一天!

从图中可以发现,其实我们需要收集每天的正利润就可以,「收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间」

那么只收集正利润就是贪心所贪的地方!

「局部最优:收集每天的正利润,全局最优:求得最大利润」

/**
 * 贪心算法:
 * 假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。
 * 相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
 * 「局部最优:收集每天的正利润,全局最优:求得最大利润」。
 */
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);
        }
        return result;

    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

714-买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

/**
 * 贪心思想:当我们卖出一支股票时,我们就立即获得了以相同价格并且免除手续费买入一支股票的权利。
 */
class Solution {
    public int maxProfit(int[] prices, int fee) {

        int n = prices.length;
        int buy = prices[0] + fee;
        int profit = 0;
        
        for (int i = 1; i < n;i++){
            if (prices[i] + fee < buy){
                
                buy = prices[i] + fee;
                
            }else if (prices[i] > buy){
                
                profit += prices[i] - buy;
                
                buy = prices[i];
            }
        }
        
        return profit;
    }
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为数组的长度。
  • 空间复杂度:O(1)。

两个维度权衡问题

在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。

135-分发糖果

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

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

本题采用了两次贪心的策略:

  • 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
  • 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。

1 先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

图片

2 再确定左孩子大于右孩子的情况(从后向前遍历)

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。

那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

图片

class Solution {

    public int candy(int[] ratings) {

        int[] candyVec = new int[ratings.length];
        Arrays.fill(candyVec,1);

        // 从前向后----右孩子大于左孩子
        for (int i = 1; i < ratings.length;i++){

            if (ratings[i] > ratings[i -1]) candyVec[i] = candyVec[i - 1] + 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 result = 0;

        for (int i = 0; i < candyVec.length; i++) result += candyVec[i];

        return result;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 是孩子的数量。我们需要遍历两次数组以分别计算满足左规则或右规则的最少糖果数量。

空间复杂度:O(n),其中 n 是孩子的数量。我们需要保存所有的左规则对应的糖果数量。

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]] 是重新构造后的队列。

**思路:**身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。

「此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!」

那么只需要按照k为下标重新插入队列就可以了

图片

「局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性」

「全局最优:最后都做完插入操作,整个队列满足题目队列属性」

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

class Solution {

    public int[][] reconstructQueue(int[][] people) {

        //按数组第一个元素进行降序,按第二个元素进行升序
        Arrays.sort(people, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
               if (o1[0] != o2[0]){
                   //第一个元素不相等时,第一个元素降序排序
                   return o2[0] - o1[0];
               }else{
                   //第一个元素相等时,第二个元素升序排序
                   return o1[1] - o2[1];
               }
            }
        });

        //新建一个list,用于保存结果集
        List<int[]> list = new LinkedList<>();

        for (int i = 0; i < people.length; i++) {

            if (list.size() > people[i][1]){

                //结果集中元素个数大于第i个人前面应有的人数时,将第i个人插入到结果集的 people[i][1]位置
                list.add(people[i][1],people[i]);
            }else{

                //结果集中元素个数小于等于第i个人前面应有的人数时,将第i个人追加到结果集的后面
                list.add(list.size(),people[i]);
            }
        }

        //将list转化为数组,然后返回
        return list.toArray(new int[list.size()][]);

    }
}

复杂度分析

时间复杂度:O(n^2),其中 n 是数组 people 的长度。我们需要 O(nlogn) 的时间进行排序,随后需要 O(n^2)的时间遍历每一个人并将他们放入队列中。由于前者在渐近意义下小于后者,因此总时间复杂度为 O(n^2)。

空间复杂度:O(logn),即为排序需要使用的栈空间。

贪心难题

贪心解决区间问题

关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。

55-跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。

图片

/**
 * 方法一:贪心
 *
 *我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x,
 * 如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,
 * 因此我们可以用 x+nums[x] 更新 最远可以到达的位置。
 *
 * 在遍历的过程中,如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True 作为答案。
 * 反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False 作为答案。
 *
 */

class Solution {
    public boolean canJump(int[] nums) {

        int n = nums.length;
        int rightmost = 0;
        
        for (int i = 0; i < n; i++) {
            
            if (i <= rightmost){
                
                rightmost = Math.max(rightmost,i + nums[i]);
                
                if (rightmost >= n-1){
                    return true;
                }
            }
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 为数组的大小。只需要访问 nums 数组一遍,共 n 个位置。

空间复杂度:O(1),不需要额外的空间开销。

45-跳跃游戏II

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。

例如,对于数组 [2,3,1,2,4,2,3],初始位置是下标 0,从下标 0 出发,最远可到达下标 2。下标 0 可到达的位置中,下标 1 的值是 3,从下标 1 出发可以达到更远的位置,因此第一步到达下标 1。从下标 1 出发,最远可到达下标 4。下标 1 可到达的位置中,下标 4 的值是 4 ,从下标 4 出发可以达到更远的位置,因此第二步到达下标 4。

fig1

/**
 * 正向查找可到达的最大位置
 * 在具体的实现中,我们维护当前能够到达的最大下标位置,记为边界。我们从左到右遍历数组,到达边界时,更新边界并将跳跃次数增加 1。
 *
 * 在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。
 * 如果访问最后一个元素,在边界正好为最后一个位置的情况下,我们会增加一次「不必要的跳跃次数」,因此我们不必访问最后一个元素。
 */
class Solution {
    public int jump(int[] nums) {

        int length = nums.length;
        int end = 0;
        int maxPosition = 0;
        int steps = 0;

        for(int i = 0; i < length -1 ;i++){

            maxPosition = Math.max(maxPosition,i + nums[i]);

            if(i == end){
                end = maxPosition;
                steps++;
            }
        }
        return steps;
    }
}

复杂度分析

  • 时间复杂度:O(n)),其中 n 是数组长度。
  • 空间复杂度:O(1)。

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

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。

可以射出的弓箭的数量没有限制。弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

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

「如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭」

图片

可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。

class Solution {

    public int findMinArrowShots(int[][] points) {

        if(points.length == 0) return 0;

        // 根据第二个元素进行升序排列
        Arrays.sort(points, new Comparator<int[]>() {
            
            @Override
            public int compare(int[] o1, int[] o2) {
                
                // return o1[1] - o2[1];  按照每个区间的结尾进行升序排序
                return Integer.compare(o1[1], o2[1]);
            }
        });

        int res = 1;
        int right = points[0][1];

        for (int i = 1; i < points.length;i++){

            if (points[i][0] > right) {  // 后一个的startIndex > 前一个的endIndex
                res++;
                right = points[i][1];
            }
        }
        return res;
    }
}

复杂度分析

时间复杂度:O(nlogn),其中 n 是数组points 的长度。排序的时间复杂度为O(nlogn),对所有气球进行遍历并计算答案的时间复杂度为 O(n),其在渐进意义下小于前者,因此可以忽略。

空间复杂度:O(logn),即为排序需要使用的栈空间。

435-无重叠区间–与452类似

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

图片

「我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了」

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {

        if (intervals.length == 0) return 0;

        // 按照第二个元素升序排序
        Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        
        int ans = 1;
        int right = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {

            if (intervals[i][0] >= right){
                ans++;    //记录非交叉区间
                right = intervals[i][1];
            }
            
        }
        return intervals.length - ans;

    }
}

复杂度分析

时间复杂度:O(nlogn),其中 n 是数组points 的长度。排序的时间复杂度为O(nlogn),对所有气球进行遍历并计算答案的时间复杂度为 O(n),其在渐进意义下小于前者,因此可以忽略。

空间复杂度:O(logn),即为排序需要使用的栈空间。

763-划分字母区间

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

在遍历的过程中相当于是要找每一个字母的边界,「如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了」。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

如图:

图片

class Solution {

    public List<Integer> partitionLabels(String s) {

        int[] hash = new int[26];// i为字符,hash[i]为字符出现的最后位置

        for (int i = 0; i < s.length();i++){  // 统计每一个字符最后出现的位置
            hash[s.charAt(i) - 'a'] = i;
        }

        List<Integer> res = new ArrayList<>();
        int left = 0,right = 0;

        for (int i = 0; i < s.length();i++){

            right = Math.max(right,hash[s.charAt(i) - 'a']);   // 找到字符出现的最远边界

            if (i == right){
                res.add(right - left + 1);
                left = i + 1;
            }
        }
        return res;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 是字符串的长度。需要遍历字符串两次,第一次遍历时记录每个字母最后一次出现的下标位置,第二次遍历时进行字符串的划分。

空间复杂度:O(∣Σ∣),其中 Σ 是字符串中的字符集。这道题中,字符串只包含小写字母,因此 ∣Σ∣=26。

56-合并区间

给出一个区间的集合,请合并所有重叠的区间。

示例 1:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

class Solution {

    public int[][] merge(int[][] intervals) {

        List<int[]> res = new ArrayList<>();
        int length = intervals.length;

        if (length == 0) return res.toArray(new int[0][]);

        // 先按照区间起始位置排序
        Arrays.sort(intervals,  (v1, v2) -> v1[0] - v2[0]);

        boolean flag = false; // 标记最后一个区间有没有合并

        for (int i = 1; i < length;i++){

            int start = intervals[i - 1][0];  // 初始为i-1区间的左边界
            int end = intervals[i - 1][1];   // 初始i-1区间的右边界

            while(i < length && intervals[i][0] <= end){  // 合并区间

                end = Math.max(end,intervals[i][1]);  // 不断更新右区间

                if (i == length -1){
                    flag = true; // 最后一个区间也合并了
                }

                i++;  // 继续合并下一个区间
            }
            // start和end是表示intervals[i - 1]的左边界右边界,所以最优intervals[i]区间是否合并了要标记一下
            res.add(new int[]{start,end});

        }

        // 如果最后一个区间没有合并,将其加入result
        if (flag == false){
            res.add(new int[]{intervals[length - 1][0], intervals[length - 1][1]});
        }

        // res.toArray(new int[0][]);中的0只是为了指定泛型,说明返回的类型
        return res.toArray(new int[0][]);

    }
}

复杂度分析

时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)。

空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(logn) 即为排序所需要的空间复杂度。

public int[][] merge(int[][] intervals) {

    //1、先根据区间开始位置的大小,将 intervals 排序
    Arrays.sort(intervals , (nums1 , nums2) -> nums1[0]-nums2[0]);

    //2、创建一个新的二维数组,用于保存合并后的区间
    int[][] res = new int[intervals.length][2];
    int index = 0;

    //3、开始合并区间
    for (int i = 0; i < intervals.length; i++) {
        //若是第一个区间,或者当前区间的起始位置 > 结果数组res中最后区间的终止位置(注意,结果数组res中最后区间的下标是 index-1)
        //则不需要将当前区间合并到上一个区间,那么当前区间可以赋值给ret,作为一个新的“合并基底”,将后面满足条件的区间向这个基底合并
        if (i == 0 || intervals[i][0] > res[index - 1][1]) {
            res[index++] = intervals[i];//同时将index+1
        } else {
            //否则,需要将当前区间合并到结果数组res的最后区间(即res中下标为index-1的区间),此时index不需要变化
            //此时需要改变 ret[index][1],即ret[index] 数组的右边界可能扩展
            res[index - 1][1] = Math.max(res[index - 1][1], intervals[i][1]);
        }
    }
    //res数组中实际上只有 index 个数组有效,其他都是 (0 , 0),我们原先设置res长度为 intervals.length,需要将有效部分复制返回!
    return Arrays.copyOf(res , index);
}

其他难题

53-最大子序和

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

示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

/**
 * 动态规划
 * 每次求 pre + x 与 x 的最大者,再将最大值存起来,最后进行返回。
 * 如果是求解最大子序和下的数组,
 */
class Solution {
    public int maxSubArray(int[] nums) {

        int pre = 0;
        int maxAns = nums[0];

        for (int x : nums){
            pre = Math.max(pre + x,x);
            maxAns = Math.max(maxAns,pre);
        }
        return maxAns;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。

53-返回最大子序和的数组

class Solution{
    public int[] maxSubArray(int[] nums) {

        int pre = 0;
        int maxAns = nums[0];
        
        int beginIndex = 0;
        int maxIndex = 0;
        int sum = 0;
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();

        for (int i = 0; i < nums.length; i++) {
            pre = Math.max(pre + nums[i],nums[i]);
            maxAns = Math.max(maxAns,pre);

            if (!map.containsKey(maxAns)){
                map.put(maxAns,i);
            }
        }
        //1 找到第一个最大值的索引(最后的索引)
        Object[] indexs = map.values().toArray();
        Arrays.sort(indexs);   //排序后取最大值
        maxIndex = (int) indexs[indexs.length -1];

        //2 找到连续数组的最开始索引
        for (int i = maxIndex; i >= 0 ; i--) {
            sum += nums[i];
            if (sum == maxAns){
                beginIndex = i;
            }
        }
        // 3 返回连续数组
        // int[] newArray = Arrays.copyOfRange(oldArray, startIndex, endIndex);
        //startIndex是要复制的范围的初始索引(包括)
        //endIndex是要复制的范围的最终索引,不包括。 (此索引可能位于数组之外)
        return Arrays.copyOfRange(nums, beginIndex, maxIndex + 1);
    }
}

134-加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

示例 1:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]

输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

「for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!」

贪心思路:

首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。每个加油站的剩余量rest[i]为gas[i] - cost[i]。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。

「那么局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置」

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {

        int curSum = 0;
        int totalSum = 0;
        int start = 0;

        for (int i = 0; i < gas.length;i++){

            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];

            if (curSum < 0){  // 当前累加rest[i]和 curSum一旦小于0
                start = i + 1;  // 起始位置更新为i+1
                curSum = 0;     // curSum从0开始
            }
        }

        if (totalSum < 0) return -1;  // 说明怎么走都不可能跑一圈了
        return start;

    }
}

复杂度分析

时间复杂度:O(n),其中 n 为gs 数组的长度。我们只需要遍历一遍数组即可求得答案。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。

968-监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。

图片

**我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!**此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

此时这道题目还有两个难点:

  1. 二叉树的遍历----后序遍历
  2. 如何隔两个节点放一个摄像头

来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:

有如下三种:

  • 该节点无覆盖
  • 本节点有摄像头
  • 本节点有覆盖

我们分别有三个数字来表示:

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

    int result = 0;

    public int minCameraCover(TreeNode root) {
        // 情况4
        if (traversal(root) == 0){  // root 无覆盖
            result ++;
        }

        return result;
    }

    private int traversal(TreeNode cur){

        // 空节点,该节点有覆盖
        if (cur == null) return 2;

        int left = traversal(cur.left);    // 左
        int right = traversal(cur.right);  // 右

        // 情况1
        // 左右节点都有覆盖
        if (left == 2 && right == 2) return 0;

        // 情况2
        // left == 0 && right == 0 左右节点无覆盖
        // left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        // left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        // left == 0 && right == 2 左节点无覆盖,右节点覆盖
        // left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if (left == 0 || right == 0) {
            result++;
            return 1;
        }

        // 情况3
        // left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        // left == 1 && right == 1 左右节点都有摄像头
        // 其他情况前段代码均已覆盖
        if (left == 1 || right == 1) return 2;

        // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
        // 这个 return -1 逻辑不会走到这里。
        return -1;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值