前沿
最近接触到了一种很有意思的数据结构,叫单调队列,可以用来维护一个给定大小的区间的最值,这个区间就像滑动窗口。时间复杂度是O(n),n是给定序列的长度。
一、什么是单调队列
1.1、单调队列的含义
单调队列,顾名思义,是一种具有单调性的队列。单调队列分为单调递增队列和单调递减队列两种。
-单调递增队列:队列的头部元素(左端)是当前区间的最小值,用来维护区间的最小值
-单调递增队列:队列的头部元素(左端)是当前区间的最大值,用来维护区间的最大值
1.2、一个简单的例子
先来看一个简单的单调队列的例子。
序列为:[3 ,1 ,5 , 7 ,4 ,2 , 1],现在要维护区间长度为3的最大值。如下图:
可以看出,有元素入队列的操作,也有元素出队列的操作。并且,虽然维护的长度是3,但是单调队列的元素个数并不一定总是和窗口的长度是一致的,这是有别于优先队列的地方。因为单调队列只是维护有效的以及有可能有效的最大值。
二、单调队列的实现思路
2.1 大体思路
单调队列的实现步骤大概分为三步:“掐头去尾”然后取队头元素。
1.去尾操作。 队尾元素出列。每当有新的元素加入的时候,就要添加到单调队列的队尾(也就是右端进)。然后不断删除影响队列单调性的元素**,遇到小于(大于)新元素的队尾元素就要删除掉。每删除一个队尾元素,就要重新判断新元素是否可以加入队列,直到队尾元素大于等于新元素、或者队列为空为止
2.掐头操作。 队头元素出列。判断头元素(下标)是否在当前待求解的区间之内(因为窗口是不断滑动的),如果不在区间内,就要剔除。直到队头元素满足要求为止。
3.取头元素。 此时,单调队列的头部维护的就是当前区间的最大(小)值,取头元素作为返回值即可。
2.2 具体实现流程
2.2.1 去尾操作:队尾元素出队列
假设需要维护一个 区间长度为L 的最大值,显然,我们需要一个 单调递减队列。
现在有一个新元素new(序号为new_id)待放入队列,在新元素new入队列之前,需要先执行下面的操作:
- 如果当前 队列为空,则 直接将new放入队列 。否则,执行下一步。
- 当前 队列不为空 。(假设队列的尾元素为rear)
- 如果rear<new,则 尾元素rear出队列,直到 当前队列为空 或者 rear<new不再满足。紧接着,元素new入队列。
- 如果rear>=new,直接将元素new放入队列。
2.2.2 删头操作:队头元素出队列
将新元素new入队列之后,我们还需要判断当前队列中 队头元素 是否在 待求解的区间范围 内。
假设队列的头元素为front(序号为front_id)。
如果此时当前 队列不为空 ,且 front_id < new_id-L+1 ,那么将 队列头元素front出队列 。不断重复此过程,直至front_id>=new_id-L+1。
(也就是说,将队列中序号不在区间[ new_id-L+1,new_id ]的元素删除)
2.2.3 取解操作
经过上面的操作,此时 队列的头元素 就是 区间[ new_id-L+1,new_id] 的最大值。
三、C++代码实现
单调队列的典型题目leetcode链接:239. 滑动窗口最大值 - 力扣(LeetCode)
C++代码实现:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int len = nums.size();
vector<int>res;
deque<int>que;//单调队列做法,里面放的是数组元素的下标
//这里必须是下标,因为后面要根据下标来判断队列头部的元素是否还在滑动窗口里面
for(int i = 0;i < len;i ++)//i代表的是滑动窗口的前端(右端)
{
while(!que.empty() && nums[i] > nums[que.back()] )//去尾操作:队尾元素出队列
{
que.pop_back();
}
que.push_back(i);
//que.push_back(nums[i]);
//要时刻谨记que里面元素的含义,que里面保存的是nums数组元素的下标,而不是元素值,一开始这里写错了
if(i >= k - 1)//当i == k - 1的时候,就说明滑动窗口已经包含k个元素le,单调队列维护的是k个元素的最大值
{
while(!que.empty() && que.front() < i - k + 1)//删头操作:队头元素不符合要求的出队列
que.pop_front();
res.push_back(nums[que.front()]);//取满足条件的当前队列的最大值
}
}
return res;
}