力扣239 - 滑动窗口的最大值【单调队列的原理】
@TOC
一、题目描述
原题传送门
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
- 本题为力扣上的困难题,既是数组章节滑动窗口部分的一道典型应用题,也是栈与队列中的高频面试题,想要在面试中游刃有余,本题就要好好细究
二、思路分析
优先队列【priority_queue】?
- 我们首先来分析一下本题的思路,根据题目意思,是想要让我们寻找每个大小为k的滑动窗口中的最大值,
- 从示例1就可以很清晰的看到,当k 取3时,每次的滑动窗口大小即为3,==也就是这个窗口中的元素个数要为3个==,一个不多一个不少,要实现这样的场景那就只能一个个划过去,这就有了滑动窗口这个词的由来
- 那我们要怎么去实现它呢,有些小伙伴脑瓜子很灵光,一上来看到要求找出最大值,然后又是栈与队列部分的面试题,马上就想到了优先队列,这个思维是可以,但是优先队列真的有用吗,因为是要取最大值,所以会想到用一个降序队列,也就是大顶锥,但是对于大顶锥的话它每次弹出的只能是队首也就是最大的元素,但是最大的元素又是我们要保留、要维护的,那你把它弹出去了,后面要怎么到这个元素呢,所以我们不可以用优先队列
单调队列【✔】
什么是单调队列呢?大部分人可能只听说过队列、双端队列这些。所谓单调队列,那就是一个单调的队列,那什么又是单调呢,所谓单调指得就是单调增或者单调减
- 那怎么去实现这个单调队列呢,是有现成的像队列queue,双端队列deque那样的吗?
- 那没有,对于单调队列是需要我们自己去实现的,但是这要怎么去实现呢?
- 对于队列操作的灵活程度,肯定是双端队列deque来的好一些,因为是要去找每个滑动窗口的最大值,那我们就可以把这个队列看成是一个滑动窗口,当这个窗口滑动的时候,处于窗口的第一个数字被删除,同时再窗口末尾添加一个新的元素
- 要实现如上的操作只需要在我们自己定义的双端队列中写一个pop()函数、一个push()函数以及一个front()函数,就足以模拟这个窗口的滑动
三、具体实现分析
了解了本题可以使用单调队列来进行解题,接下来我们来细细地探究一下要如何去将其实现
==2 3 4 2 6 2 5 1==
- 我们以上面这组数据为例做讲解,首先将2入队,接着是3,但这个时候我们要将2弹出,为什么呢?这就要思考了,3 > 2,那2是不会成为这个窗口中的最大值的,所以将其弹出这个时候队列中就只剩下一个数字3了,然后将窗口中的最后一个数4入队,这时3又小于4,所以要将3出队,此时队列中便只剩下4这个数字了
- 可以看到,此时滑动窗口中的最大值就是最前面的4
- 接下去处理第四个数字2,2 < 4 ,可能会成为下一个滑动窗口的最大值,所以将其入队
- 此时滑动窗口的最大值也是最前面的4
- 下一个是数字6,这个时候因为前面的两个数字4与2都比6小,所以不可能再会成为下一个滑动窗口的最大值了,因为将它们全部出队
- 可以看到,此时队列的首部也就是6即为滑动窗口的最大值
- 第六个数字是2,可以看到它比队列中已有的数字6小,所以将2也从队列尾部存入
- 然后可以清晰地看出此时滑动窗口的最大值即为队头元素6
- 第七个数字是5,因为2小于5,所以2不可能再会是下一个滑动窗口中的最大值,但是6又在前面挡着,此时我们就不能从队头弹出元素,这个时候双端队列的优势就来了,我们可以使用pop_back()将队尾元素弹出,然后在push_back()将从队尾入队
- 可以看到,此时滑动窗口的最大值还是队头
- 此时论到最后一个数为1,它是想到若是将1入队,前面三个数6 2 5均不符合出队的条件,那要怎么办呢,这个就需要判断了,若是当前队列的元素值大于给出的滑动窗口的限定元素个数,那么就要执行pop_front()将最前面的元素也就是6弹出
结论
随着上述的分析讨论,我们得出一个结论,那就是每次的队头元素即为滑动窗口的最大值
四、单调队列的实现
知道了每次滑动窗口的最大值是出于队列元素的第一个,现在我们要去实在地实现这个队列,刚才说了要用deque双端队列
- 这是它的整体框架
class MyQueue{
public: //利用双端队列实现单调队列
deque<int> deq;
void pop(int val);
void push(int val)
int front();
}
void pop(int val)
{
if(!deq.empty() && val == deq.front())
deq.pop_front();
}
- 首先是pop()函数,此时我们要去判断当前需要pop的元素值是否与队首元素相同,而且的队列还不可以为空,我们不可以对空队列进行一个操作
- 为什么似乎与队首元素相同就可以pop呢?上面不是说了若是这个值在后面的滑动窗口中不会为最大值那它要被pop。
- 你想的这个操作我们接下来会在push当中实现,因为这个操作是随着元素的插入我们需要先行去判断去除的,但是在pop元素的时候,我们只需要判断其是否与当前队列的队首元素相同即可,因为上面说了队首即为最大的滑动窗口的元素
void push(int val)
{
while(!deq.empty() && val > deq.back())
deq.pop_back();
deq.push_back(val);
}
- 接着来说一说它的push函数,还是一样,队列不可以为空,然后去判断当前要push进队列的值是否比前面那个数大,也就是是否比当前队尾元素大,若成立,则使用pop_back()将当前队尾元素出队
- 有一点要注意的是这里是一个while循环哦,可不是简单的if分支判断,上面我们有演示到,要出队的元素可能不止一个,因为即将要入队的元素可能比队列的前面几个元素都要来的大
int front()
{
return deq.front();
}
- 最后的话就是这个front()获取当前队列的队首元素,因为这才是真正的滑动窗口的最大值
五、主接口的实现
讲完了单调队列,接下里我们来说一说这个主接口,这里也是有一些小细节要特别注意的
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue q;
vector<int> result;
//1.先将前k个字符入队
for(int i=0;i < k;++i)
{
q.push(nums[i]);
}
//2.从前k个字符中取出最大值存入reuslt中
result.push_back(q.front());
//3.入队后i-k个字符
for(int j=k;j<nums.size();++j)
{
q.pop(nums[j-k]);
q.push(nums[j]);
result.push_back(q.front()); //记录当前后续字符中的最大值
}
return result;
}
- 首先看到,定义了我们刚才自己实现的一个单调队列,然后是一个收集滑动窗口最大值的集合
- 第一点要注意,我们只需要将前k个nums数组中的元素入队即可,在这个入队的过程中调用push()函数,他就会自动使用我们自定义的push()函数,从而就产生了我们第三模块在具体分析这个队列的元素如何进出的场景
- 然后在我们入队其他元素前,要先取出当前队列的队首元素,也就是最大的那一个,那有些同学就会疑惑了,这怎么就是最大的那一个呢?
- 问出这个问题你可能还没有把这个push()和pop()的原理搞清楚,再去第三模块琢磨一下吧,上面说到,我们在将前k个元素push()进队列时,就已经产生了一个不断出队的操作,那么当我们执行到这一步的时候,当前队列的队头就一定会是滑动窗口中最大的那一个
- 最后一步就是将后面i - k 个元素依次入队,也是要涉及到我们第三模块的知识,因为前k个元素已经入队了,所以从k开始就可以,==nums[j - k]就指的是当前队列的队首元素==,因为k是不变的,j 是一直在变动的,这个大家可以去自己推敲一下
- 然后当pop()完成后,我们再去push(),做好通过front()获取队头元素即可,最后返回result集合所收集的所有滑动窗口的最大值集合
六、总结与拓展
本文我们通过一道力扣题求解滑动窗口的最大值,了解了什么是单调队列,以及它的实现原理,如果觉得自己可以完成了就去试着做一下吧
- 下面这道题也是有关数组章节滑动窗口的经典题型,求的是长度最小的子数组,有兴趣的小伙伴可以去了解一下
若有疑问请于评论区留言或私信我,感谢您对本文的观看
- 点赞
- 收藏
- 关注作者
评论(0)