📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待
本期开始新的篇章,贪心算法题目的讲解。
贪心算法,没有固定的套路,通常根据做题人的做题经验,才能写出贪心算法。且算法有难度,并不总是简单的。贪心算法是一种常识思想,需要在做题中慢慢感觉和领悟。
分发饼干
分发饼干,是一道贪心算法的启蒙题(但是实际上我三道题都未能自己做出来,对贪心算法一无所知)。
题目大意是将一些饼干分发给一些小孩子们,小孩子们胃口各不相同,且每个孩子只能喂食一块饼干,要求是饼干的尺寸大于等于孩子的胃口值,求最多可以使几个孩子吃饱?解题的思路是我们可以创建一个变量count来记述我们已经满足了几个孩子,然后将孩子数组和饼干数组分别排序,使它们变得有序方便操作,用一个循环控制两个变量,当满足时候让饼干下标和孩子下标分别往后走,count自增,不满足只让饼干下标向后走,这样的原因是两数组有序,那么满足这个孩子的饼干如果存在,那么只可能在后面的饼干,而如果连这个孩子都无法满足,那肯定后面的孩子也无法满足。这就是排序的好处,如果不排序,那一个孩子需要遍历全部饼干,同样也要遍历所有孩子,这样会严重影响代码效率,时间为O(N^2)。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s)
{
sort(g.begin(),g.end());sort(s.begin(),s.end());
int count=0;
for(int i=0,j=0;i<g.size()&&j<s.size();)
{
if(s[j]>=g[i])
{
count++;
i++;
j++;
continue;
}
j++;
}
return count;
}
};
排序起初我没有想到,但是看了题解我想到了这种方法,代码看起来略显冗余,下面看一个思路一样但是代码却很精简也不用count来计数的方法。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s)
{
sort(g.begin(),g.end());sort(s.begin(),s.end());
int index=0;
for(int i=0;i<s.size();i++)
{
if(index<g.size()&&g[index]<=s[i])
{
index++;
}
}
return index;
}
};
我们用index来代替count和小孩数组的下标,用i来代替饼干下标,核心的思路是相同的,保证不要越界,然后当饼干能够满足小孩,index就自增,怎么样是不是简洁了不少,而且还巧妙的用孩子下标做了返回值。
摆动序列
在做这道题时候特别懵,没有任何思路,看了题解也很懵,后来想了一阵才慢慢明白。
题的大意是求摆动序列,可以删除改定序列的若干个元素,最后构成一个摆动序列,摆动序列的定义是严格按照数字之间的差值,在正数和负数之间来回摆动。实现预判式的在一个序列里删除某些节点而达到拼成最大摆动序列是十分困难的。
我们给出另一种解题思路,让我们不用删除节点,却起到了删除节点的效果。具体做法为,将一个序列模拟成山峰和山谷的示意图,也就是说把这些数都画出来,看它们的拐向,我们只要拐向一次向上一次向下的那种,如果中间是连续向上或者向下亦或者是平的(两个相邻数相等)我们全都统统不要。那么如何知道中间要删除这些节点,然后还能将删除后的两段连上呢?这是十分巧妙的做法,我们先定义两个变量一个代表上一次两个数的差值,另一个代表的是现在的相邻两个数的差值,如果两个差值是有正负变化的(前一个差值是0现在的差值是大于0或者小于0的同样符合摆动,反过来也是),那么就将计数器加1,且上一个的差值被改成当前的,然后进行下一次的循环。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if(nums.size()<=1)return nums.size();
int cur=0;int pre=0;int count=1;
for(int i=0;i<nums.size()-1;i++){
cur=nums[i+1]-nums[i];
if((cur>0&&pre<=0)||(cur<0&&pre>=0)){
count++;
pre=cur;
}
}
return count;
}
};
还有一些细节,需要交代一下,首先我们知道如果有摆动序列,那么至少要有1个数据来组成该序列,所以我们count要从1开始起,这是很关键的,由于我们让count从1开始,那么有几个拐点直接加在一起,就是此时最大摆动序列的总长度,而为什么要将pre=cur写到if的里面呢?如果if没进去我们就不改写前一个差的数值?实际上我们可以通过模拟得知,这种写法和把它写到外面在一般的测试用例下,跑出来的答案是一样的
但是如果碰到了序列走势是向上–平–向上这种情况呢?这种情况把pre写在if的外面会使拐点多算一次,实际上我们摆动序列是2,这样算起来是3,所以不能这样写,摆动序列的定义不能连续向上,即使中间有平也不行,况且平本来就不算在内,这也是代码的关键所在,它模拟出了,我们在真实删除数据时,是如何将删除后两段又重新链接在一起了,由于判断摆动符合我们才改变pre的值,所以不符合时候它一直是上一次符合时候的差值,当再次符合时进入if我们才改动pre,这也就相当于联系起了删除中间节点后两个链条的连接,这里的代码虽然简短,但是每一步的思路,尤其重要,建议大家用心去体会其过程的巧妙之处。
最大子数组和
求最大的子数组连续和,这道题的题目大意和题目名字相同,首先能想到的一种思路是暴力法,双for来解决问题,一个for循环控制数组起始位置遍历,一个控制终点,两层遍历后,找到最大子数组和是多少返回,这种方法貌似以前是可以通过的,但是现在不行了,直接提交这种暴力解法会提示超时,这道题同样我们也是可以用贪心来做出来的。
思路其实很简单,我们用sum来记录当前总和,再用一个变量result来记录我们历史记录中取到的最大值,和它作比较,比result大则与之交换,而贪的是如果sum小于0,直接sum=0再从当前位置向后与sum相加,再记录,而暴力解法中的数组结束位置,我们用每次的result判断来代替,每一次循环都判断一次也就相当于这一步骤了
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result=INT_MIN;int sum=0;
for(int i=0;i<nums.size();i++){
sum+=nums[i];
if(sum>result)
result=sum;
if(sum<0)sum=0;
}
return result;
}
};
如果数据全是-1呢?那它会求得答案为0吗?肯定是不会的,自行调试便知道,在sum=0之前由于我们result取得是int里最小的数,我们先行进行交换使result=-1,然后sum=0后再加上-1,结果还是-1它不可能变成0,这也就是为什么要把sum+=数据写在前面,result=最小数,是为了避免有负数和的产生。
从以上三题中,我个人认为,求某个条件下的最大的某某数这类题,应该是可以考虑用贪心算法做一下的,除了第二题有些难想外,第一题和第三题实际上确实是“常识”类的解题思路,但是有时候确实没有做过的话,还是无法做得出来,重要的是多多积累和总结,第一题实际上我觉得最重要的是排序,没有排序我们不可能那么轻易的解出来,而不用遍历那么多的数据,第二题是运用峰值的巧妙之处,求取了最长的摆动序列数据个数,第三道题贪的是sum小于0立刻重新计数
为什么当它为负数时候,直接sum=0了却不从之前数组统计的下一个开始计数,而是直接从当前位置向后计数呢?因为之前的数相加时候我们已经用resul来记录了这一段数据中最大的数是多少,数据是要连续的遍历相加。还不懂,不妨举一个例子,例如数据整体序列为{-5,3,4,-8,5}我们知道最大子数组和肯定是5,那我们看前面,走到-5直接跳了出来,然后3+4-8才跳了出来,那你说4-8会比3+4-8还要大吗?
肯定不能,这是一种情况说明了如果前一个数和后一个数都是正数那么实际上从它的下一个位置开始走,不可能找得到比从刚才那个数还大的组合了,那有人会说如果前一个数是负数后一个数是正数不就能找的到了吗也同样的可以模拟如果还是{-5,3,4,-8,5}那样的话第一个-5遇到了后面直接sum=0了,如果是{-5,3,-2,7,5}那从3向后遍历的和实际上是比从正数7大的,很多时候我们都可以用模拟来解决问题。
总结:
今天我们完成了分发饼干、摆动序列、最大子数组和三道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~