扔掉你的算法书!1小时零基础拿下贪心算法!(17道题+万字儿童级解析+数十张图解)

Posted 深林无鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了扔掉你的算法书!1小时零基础拿下贪心算法!(17道题+万字儿童级解析+数十张图解)相关的知识,希望对你有一定的参考价值。

前几天的文章中我写到了一些关于零基础学习回溯算法的一些步骤和细节,在刷题的过程中发现了很多贪心算法的题很有趣,于是今天他来了,准备了好17道题来供大家共同学习,并附上了十分详细的题解,与附带了注释的优美代码,每个题的题解都可以说是隔壁牛大爷都看得懂了咯,相信聪明的小伙伴们一定可以快速上手拿下这个有趣的算法思想。有点长,建议收藏反复观看,文章附带进度条!!可视化你的进步~

贪心算法零基础到快速变成高手


上干货~~( 文末有福利喔~
在这里插入图片描述

1.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
在这里插入图片描述
解题思路:
这是一道入门贪心算法十分基础的题目啦~
问题分析:首先我们要想满足更多的孩子,是不是想着尽量用最小尺寸的小饼干去满足孩子,这样就能匀出来尺寸大的小饼干去满足胃口比较大的孩子啦。
问题抽象:将两个数组进行排序,在同时扫描。
实现步骤
1、排序两个数组。
2、扫描饼干尺寸数组,如果能狗满足胃口最小的,就将结果 + 1,并更新胃口数组的下标。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        int ans = 0;
        if (s.length == 0 || g.length == 0) return ans;
        // 使用Arrays类的方法,对数组进行排序
        Arrays.sort(g);
        Arrays.sort(s);
        // 扫描两个数组(本质是扫描一个饼干尺寸数组)
        for (int i = 0, j = 0; i < g.length && j < s.length; j ++) {
        	// 满足 存在最小尺寸的饼干 给胃口最小的孩子
            if (s[j] >= g[i]) {
                ans ++;
                i ++;
            }
        }
        return ans;
    }
}

恭喜你入门啦,完成进度:
在这里插入图片描述


2.摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
在这里插入图片描述
解题思路:
问题分析:先明确题目中摆动序列的概念,要保证大概类似下图的样子:
在这里插入图片描述
不过要是针对题目中删除元素来获得差值序列这种复杂的描述,我们可以更换一个新的思考方式,见下图:
在这里插入图片描述
发现了如果存在这种“中间值”,我们需要删掉,来保证数组中的每个元素都属于波峰或者波谷。 什么是波峰波谷呢?顾名思义 波峰就是在各点的两边的元素都比它小, 波谷就是两边都比它大。 这样我们不需要删除元素,仅仅需要忽略过这种不是波峰波谷的值,在扫描数组的时候根据坡度的变化来更新ans就可以咯。
问题抽象:
扫描数组,根据差值确定是否更新ans值。
实现步骤:
1、定义currDiff表示当前元素与上一个元素的差值(也可以理解为坡度)
2、定义prevDiff表示上一个坡度。
3、遍历数组,坡度相反的时候,更新ans

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if(nums.length <= 1) return nums.length;
        // 上一个坡度, 与 当前坡度初始化
        int prevDiff = 0, currDiff = 0, ans = 1;
        for (int i = 1; i < nums.length; i ++) {
            currDiff = nums[i] - nums[i - 1];
            // 当前坡度与上一个坡度相反,出现波峰或波谷
            if ((currDiff > 0 && prevDiff <= 0) || (currDiff < 0 && prevDiff >= 0)) {
            	// 更新坡度
                prevDiff = currDiff;
                // 更新答案
                ans ++;
            }
        }
        return ans;
    }
}

恭喜你距离掌握贪心算法又近了一步~
在这里插入图片描述


3.最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
在这里插入图片描述
解题思路:
问题分析
刚刚接触到这道题的时候,我们不难想出下面这样的暴力思维:根据不同的起始位置扫描后面全部数组元素,将最大值随时记录下来,于是就有了下面这种效率很低的暴力代码:

int ans = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i ++) {
    int base = 0;
    for (int j = i; j < nums.length; j ++) {
        base += nums[j];
        ans = Math.max(ans, base);
    }
}

那么这道题可以优化成贪心的地方在哪里呢? 仔细观察我们的代码,在外循环每次更新的起始位置效率很低,每次只更新1,如果我们在扫描数组元素的时候根据数组元素的特性更新怎么样呢? 比如,当计数累加到遇到负数元素时,我们直接放弃当前序列(“拖油瓶”),遍历后面的元素,并对答案保持更新。这样,我们就少了一层确定起始位置的循环。
问题抽象:
一层循环控制数组下标,遍历的同时确定base是否舍弃。
实现步骤:
1、将最大值置为最小的整数值,用来更新后续最大值。
2、扫描数组,base不断累加当前元素。
3、如果base大于最大值,更新ans
4、如果base累加到负数,立即舍弃“拖油瓶”。

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 1) return nums[0];
        // 初始化变量
        int ans = Integer.MIN_VALUE, base = 0;
        for (int i = 0; i < nums.length; i ++) {
        	// 计数累加
            base += nums[i];
            // 时刻更新最大值
            ans = Math.max(ans, base);
            // 出现拖油瓶 立即舍弃
            if (base <= 0) base = 0;
        }
        return ans;
    }
}

恭喜你,更进一步
在这里插入图片描述


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

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
在这里插入图片描述
解题思路:
问题分析
给了股票每天的价格,想要赚钱,那肯定就是便宜的时候买入,涨价了就卖出咯。这个时候,就能看出来当买入(数组元素)小于卖出(数组元素)的时候就能累加到我们的答案中,但是应该什么时候卖呢?
[1, 5, 3] 这种情况很显然在跌到3之前,火速先卖出去,赚一波大的
如果存在下面这种情况该怎么卖呢:
[1, 3, 5] 存不存在“小的”时候我先存着,等“大了”我在卖出去?我们不难发现,其实是一样的,我们可以在1买入3卖出,同时3买入,5卖出。这样子: [3 - 1 + 5 - 3] = 4 == [5 - 1] 是一样的!!
这样子问题就好办了,我们只需要在上升的时候累加,下降的时候更新什么时候买入就好啦。只要不能卖钱的时候,我们就一直更新最低价格的时候再买入。
问题抽象:
扫描数组,数组元素上升时,做差累加。减少时,更新base
实现步骤:
1、初始化base,即买入时候的价格。
2、扫描数组,上升时候就卖出。
3、下降的时候更新base,即重置买入的价格。

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length <= 1) return 0;
        // 初始化
        int ans = 0, base = prices[0];
        for (int i = 1; i < prices.length; i ++) {
        	// 有赚头,火速卖掉
            if (prices[i] > base) {
                ans += prices[i] - base;
                base = prices[i];
              // 要亏了,不买不买,找到最便宜的时候买
            } else {
            // 注意这个位置,可以直接更新为最新值,因为如果存在比他大的,就直接卖掉了,不需要保持最小。
                base = Math.min(base, prices[i]);
            }
        }
        return ans;
    }
}

恭喜你,掌握了3分之1哦~
在这里插入图片描述


5.跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
在这里插入图片描述
解题思路:
问题分析
根据题目描述,我们要想跳到终点,需要怎么跳才能跳过去呢?怎么确定一次到底跳几个格子呢? 这样子想,问题就想复杂了!其实跳到哪里都无所谓,每次跳跃其实都只是获得了你最远能跳到哪里的信息,如图:
在这里插入图片描述
不难发现通过遍历可以跳到位置的时候每次更新可以跳到的最远距离,如果最远距离可以达到最后元素的位置,我们就可以直接返回。
问题抽象:
在可以跳到的最远距离内扫描数组,时刻更新最大距离,满足条件即返回true,如果扫描完,说明最大距离无法达到数组末端,返回false
实现步骤:
1、初始化最远距离maxDistnums[0]
2、在最远距离范围内逐步扫描数组,并更新最远距离。
3、判断如果最远距离大于数组长度,返回true
4、返回false

class Solution {
    public boolean canJump(int[] nums) {
        if(nums.length == 1) return true;
        // 初始化
        int maxDist = nums[0], jumpDist = 0;
        // 最远距离内逐步扫描
        for (int i = 0; i <= maxDist; i ++) {
        	// 判断是否满足条件
            if (maxDist >= nums.length - 1) return true;
            // 更新最远距离
            maxDist = Math.max(maxDist, i + nums[i]);
        }
        return false;
    }
}

继续努力哦~
在这里插入图片描述


6.跳跃游戏II

给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
在这里插入图片描述
解题思路:
问题分析
这道题和上一道题有一点点不一样,要求返回的是最少的跳跃次数。这个时候可让大家绞尽了脑汁,该怎么确定啥时候计数器+1呢;不慌不慌,看看这个图就一目了然啦:
在这里插入图片描述
不难发现通过遍历可以跳到当前步数内最远位置的时候每次更新步数和下一个步数内可以跳到的最远距离,如果最远距离可以达到最后元素的位置,我们就可以直接返回。
问题抽象:
逐步扫描,在可以跳到当前步数内的最远距离时,更新步数和下一个步数内可以到达的最远距离,如果最远距离可以到达数组长度,返回步数。
实现步骤:
1、初始化最远距离maxDist为0,当前步数可以跳到的最远距离currDist为0。
2、在currDist范围内更新maxDist,确定下一个步数内的maxDist
3、到达currDist时候更新步数。判断下一个步之内能否到达终点。
4、返回步数。

class Solution {
    public int jump(int[] nums) {
        if (nums.length == 1) return 0;
        // 初始化步数
        // 下一个步数可以到达的最远距离
        // 当前步数内可以到达的最远距离
        int ans = 0, maxDist = 0, currDist = 0;
        for (int i = 0; i < nums.length; i ++) {
        	// 当前步数内确定下一步一步之内能跳到的最远距离
            maxDist = Math.max(maxDist, i + nums[i]);
            // 跳到当前步数能到达的最大位置了
            if (i >= currDist) {
            	// 更新步数
                ans ++;
                // 更新下一步能跳到的最远距离
                currDist = maxDist;
                // 满足条件,返回
                if (maxDist >= nums.length - 1) return ans;
            }
        }
        return ans;
    }
}

掌握一半啦!你已经超过全世界一半的人啦~
在这里插入图片描述


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

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
在这里插入图片描述
解题思路:
问题分析
这是一道简单题,根据题目要求,可以对多次同一个索引操作,我们可以知道,如果在一个数组中经过k次取相反数后能够获得最大sum,必须将最小的值进行取相反数。这样子如果是负数,那么sum增大的越多,如果是正数,那么扣除的就越少,我们采用这种贪心的方式后。如果条件是比较复杂的情况该怎么处理呢,下面给出一个较为一般化的例子来分析:
[1, -2 , 3, -1, 2, -3] K = 5,首先将这个复杂的数组排序,如图:
在这里插入图片描述
此时按照我们正常人的思考方式,肯定是将绝对值最大的负数进行反转。如果将所有负数都反转后k还有盈余,这时候就可以对绝对值最小的那个数字进行不断的翻转,以求得最大的sum。思路理清了,后面就是将解法转换成程序语言。
问题抽象:
排序数组后,在k次操作内,如果数组中存在负数,就对最小的负数进行不断取相反数。没有盈余就结束,有盈余就对绝对值最小的数字进行操作。
实现步骤:
1、排序数组
2、k次遍历数组,在范围内将最低值进行取相反数
3、如果有负数,就取相反数。
4、与下一位数的绝对值进行比较,来决定是否更新 index

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums);
        int sum = 0;
        // 需要对数组元素取相反数的index
        int index = 0;
        for(int i = 0; i < k; i ++) {
        	// 遇到负数
            if (nums[index] < 0 && i < nums.length - 1) {
            	// 直接取反
                nums[index] = - nums[index];
                // 将index控制在绝对值最小的数组元素上
                if (nums[index] >= Math.abs(nums[index + 1])) index ++;
                continue;
            }
            // 非负数直接取相反数
            nums[index] = - nums[index];
        }
        for (int i = 0; i < nums.length; i ++) sum += nums[i];
        return sum;
    }
}

加油加油,坚持就是胜利!在这里插入图片描述


8.加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
1、如果题目有解,该答案即为唯一答案。
2、输入数组均为非空数组,且长度相同。
3、输入数组中的元素均为非负数。
在这里插入图片描述
解题思路:
问题分析
根据题意,有两种情况,一种是存在这么一个加油站,使得可以完成一圈,另一种就是无法跑完一圈。那么如何界定能否跑完呢?我们可以这样想,把所有加油站的油都加起来,如果比所有需要的油加起来还多,那肯定就是能跑完了,只不过就是在哪里开始出发的问题啦。这样我们进入能跑完的分支,我们就得分析分析你跑完一个路程,所加上的油能否比消耗的油多,至少不能路上抛锚吧,所以我们需要记录车里所剩余的油量,通过它的正负来判断一开始选的位置能否满足跑一圈的需求。如果不满足,我们就根据当前加油站的位置,更换新的起点。
问题抽象:
遍历数组的同时记录当前剩余的油量是否为正数,并依次来更新,出发点的位置,同时还需要记录总油量的差值,如果跑完一圈下来总油量是负的,那就说明跑不完,返回-1
实现步骤:
1、定义rest 当前剩余油量,totalGas总剩余油量,index记录出发点。
2、遍历两个数组,记录差值为当前一趟所剩余的油量,累加到当前剩余油量以及总剩余油量。
3、如果当前剩余油量为负,更换起点,当前油量置0。
4、最后,如果总油量小于0,返回-1;否则返回起点。

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        // 初始化油量
        int rest = 0, totalGas = 0;
        int index = 0; // 记录出发地点
        for (int i = 0; i < n; i ++) {
	        // 计算从出发地点开始所剩余的油量
            rest += gas[i] - cost[i]; 
            // 计算从计位点0开始所剩余的总油量
            totalGas += gas[i] - cost[i];
            // 选点错误,更换出发地点
            if (rest < 0) {
                index = (i + 1) % n;
                rest = 0; // 新出发地开始的油量置空
            }
        }
        if (totalGas < 0) return -1; // 无法跑完行程
        return index;
    }
}

马上就掌握三分之二啦~在这里插入图片描述


9.分发糖果

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
1、每个孩子至少分配到 1 个糖果。
2、评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
在这里插入图片描述
解题思路:
问题分析
leetcode上标了困难题,那么这道题难在哪里呢,难在它的贪心策略上,我们如果从头开始遍历数字,既要考虑这个数字会不会比左边大,又要考虑这是数字会不会比右边大,大家可以清晰的感觉到,啊,好难决策,万一后面的都很小呢,又要回过头来修改前面的数,简直难受到爆。那么根据这种情况,我们换一种思路,采用贪心策略中:多个局部最优解组成全局最优解的性质–> 采用左视角和右视角,只要两个视角都符合条件,那么结果一定符合条件。因为题目中仅有两点要求,第一点每个人都要有糖,用来初始化,第二点就是用来用来确定左右时图中的模样,有点像搭积木一样。左视图:保证后面越大,发的越多,小的看不见的就略过并重新开始搭积木。右视图:从小开始搭积木,遇到大的和自己看到的和左视图的拿来比较取最大的那个(否则将破坏左视角的最优局部解)。
问题抽象:
数组赋1,遍历评分数组,升序+1,降序置1重新升序。倒叙遍历评分数组,与升序数组比较,插入。
实现步骤:
1、将糖果数组赋1
2、遍历评分数组,如果后面的比前面的大,就让后面的是前面的糖果树+1
3、倒序遍历数组,如果前面的比后面的大,就让前面的是后面的糖果数加1 或者 为从步骤2中得到的更大的数字。

class Solution {
    public int candy(int[] ratings) {
        int ans = 0;
        // 初始化,并赋1
        int[] candies = new int[ratings.length];
        for (int i = 0; i < candies.length; i ++) candies[i] = 1;
        // 左视图遍历
        for (int i = 1; i < ratings.length; i ++) {
            if (ratings[i] > ratings[i - 1]) candies[i] = candies[i - 1] + 1;
        }
        // 右视图遍历
        for (int i = ratings.length - 2; i >= 0; i --) {
            if (ratings[i] > ratings[i + 1]) candies[i] = Mat

以上是关于扔掉你的算法书!1小时零基础拿下贪心算法!(17道题+万字儿童级解析+数十张图解)的主要内容,如果未能解决你的问题,请参考以下文章

扔掉你的算法书!1小时零基础拿下贪心算法!(17道题+万字儿童级解析+数十张图解)

扔掉你的算法书!1小时零基础拿下贪心算法!(17道题+万字儿童级解析+数十张图解)

有哪些值得推荐的好的算法书?

看完一本小的算法书一个总结吧

零基础学贪心算法

《算法零基础100讲》(第17讲) 线性枚举 - 最值算法