队列
队列的相关知识:
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。(先进先出)
模拟队列(基于数组)
基本思路:
用一个数组 a 保存数据。
用 hh 代表队头,a[hh] 就是队头元素, a[hh + 1] 就是第二个元素。
用 tt 代表队尾, a[tt] 就是队尾元素, a[tt + 1] 就是下一次入队,元素应该放的位置。
[hh, tt] 左闭右闭,代表队列中元素所在的区间。
出队pop:因为 hh 代表队头,[hh, tt] 代表元素所在区间。所以出队可以用 hh++实现,hh++后,区间变为[hh + 1, tt]。
入队push:因为 tt 代表队尾,[hh, tt] 代表元素所在区间。所以入出队可以用 tt++实现,tt++后,区间变为[hh, tt + 1], 然后在a[tt+1]位置放入入队元素。
是否为空empty:[hh, tt] 代表元素所在区间,当区间非空的时候,对列非空。也就是tt >= hh的时候,对列非空。
询问队头query:用 hh 代表队头,a[hh] 就是队头元素,返回 a[hh] 即可。
例题:
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x
,pop
,empty
,query
中的一种。
输出格式
对于每个 empty
和 query
操作都要输出一个查询结果,每个结果占一行。
其中,empty
操作的查询结果为 YES
或 NO
,query
操作的查询结果为一个整数,表示队头元素的值。
数据范围
1≤M≤100000,
1≤x≤10^9,
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
解答:
#include<iostream>
using namespace std;
const int N=100010;
int a[N];int M;
int tt=-1;
int hh=0;
string s;int x;
int main()
{
cin>>M;
while(M--)
{
cin>>s;
if(s=="push")
{
cin>>x;
a[++tt]=x;
}
if(s=="pop")
{
hh++;
}
if(s=="empty")
{
if(hh<=tt)cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
if(s=="query")
{
cout<<a[hh]<<endl;
}
}
return 0;
}
单调队列
例题(滑动窗口):
给定一个大小为 n≤10^6 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k 为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
关于本题:
思路分析:
“队列”的规则是“尾进头出”,所以输出的时候是从队头弹出元素,那么我们就要保证队头的元素一定是符合要求的(本题中即为:找到滑动窗口中的最大值或找到最小值)
如何实现?
以找到最小值为例:
如果当前的滑动窗口中有两个下标 i 和 j ,其中i在j的左侧(i<j),并且i对应的元素不大于j对应的元素(nums[i]>=nums[j]),则:
当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中。这是由于 i 在 j 的左侧所保证的。
因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最小值了,我们可以将nums[i]永久地移除。
因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组nums中对应的值是严格单调递增的。
当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。
为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果新元素大于等于队尾元素,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
由于队列中下标对应的元素是严格单调递增的,因此此时队首下标对应的元素就是滑动窗口中的最小值。
窗口向右移动的时候。因此我们还需要不断从队首弹出元素保证队列中的所有元素都是窗口中的,因此当队头元素在窗口的左边的时候,弹出队头。
思路误区:
以找到最小值为例,为了保证队头是min,那么只要新加入的元素比队头小,我们就要将队头元素删除,直到新出现的队头元素小于新加入元素,若找不到,则让这个新加入的元素成为队头元素(这种思路为什么不可以?参考Eudaimonia2月3日的语音)
最小值和最大值分开来做,两个for循环完全类似,都做以下四步:
1.解决队首已经出窗口的问题;
2.解决队尾与当前元素a[i]不满足单调性的问题;
3.将当前元素下标加入队尾;
4.如果满足条件则输出结果;
需要注意的细节:
1.上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
2.队列中存的是原数组的下标,取值时要再套一层,a[q[]];
3.算最大值前注意将hh和tt重置;
4.此题用cout会超时,只能用printf;
5.在判断单调性的时候,我们是在队尾删除,判断是否在窗口内的时候,我们是在队头删除。所以这不是普通的队列,而是双端队列。
解答:
#include <iostream>
using namespace std;
const int N=1000010;
int a[N],q[N];\\a[N]用来存储所有的元素,q[N]为所开队列(存储所有元素对应的下标)
int hh=0,tt=-1;
int n,k;
int main()
{
cin>>n>>k;
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
\\找出滑动窗口中的最小值
for(int i=0;i<n;i++)
{
if(i-q[hh]+1>k)hh++;\\如果队列的长度超过滑动窗口的长度,就要弹出队头元素
while(hh<=tt&&a[i]<=a[q[tt]])tt--;\\当队列不为空,且队尾元素大于等于新加入的元素时,将队尾元素删除
q[++tt]=i;\\将新加入的元素的下标放入队列中
if(i+1-k>=0)printf("%d ",a[q[hh]]);\\如果当前考虑的数大于窗口长度,输出队头元素
}
printf("\n");
\\重置队头指针和队尾指针的位置
int hh=0,tt=-1;
\\找出滑动窗口中的最大值
for(int i=0;i<n;i++)
{
if(i-q[hh]+1>k)hh++;
while(hh<=tt&&a[i]>=a[q[tt]])tt--;
q[++tt]=i;
if(i+1-k>=0)printf("%d ",a[q[hh]]);
}
return 0;
}
#部分解析参考整合网络内容,侵删