单调队列和单调栈都是在一定范围内排列有序(递增或者递减排列)的,不同点在于队列和栈在操作上的一些区别,比如进出顺序等。
为什么引入单调队列和单调栈呢?
主要是为了解决一些现实问题,比如求解一段数内最大最小数(单调队列–头出尾进),求解一个数左边或者右边第一个比其大/小的数(单调栈)。
单调栈相关问题
问题描述
求一个数组中左边第一个比他大的数。
暴力遍历的复杂度是O(N^2),而使用单调栈的复杂度可以降为O(N),空间也是S(N)【每个数最多进栈出栈一次】。
vector<int> get(vector<int> a, int len)
{
vector<int> res(len, -1);
stack<int> sta;
sta.push(0);
for (int i = len-1; i >=0; i--)
{
// 当前元素大于栈顶元素出栈,当前元素是第一个大于栈顶元素的元素
while (!sta.empty() && a[i]>a[(sta.top())])
{
res[(sta.top())] = a[i];
sta.pop();
}
// 当前元素小于栈顶元素进栈,维持一个递减栈
sta.push(i);
}
return res;
}
单调队列相关问题:
给定一个数列,从左至右输出每个长度为m的数列段内的最小数和最大数。
数列长度:N<=1000,m<=N
-
解法1:
暴力解法,那就是从数列的开头,将长度为m的窗口放上去,找到这最开始的m个数的最大值,然后窗口向后移一个,继续找到m个数中的最大值。
这种方法每求一次m个数中的最大值f(i),都要进行m-1次的比较,复杂度为O(Nm) -
解法2:
使用单调队列,
上面的解法在暴力枚举的过程中,有一个地方是重复比较了,就是在找当前的f(i)的时候,i的前面其它m-1个数在算f(i-1)的时候我们就比较过了。
那么我们能不能保存上一次的结果呢?当然主要是i的前k-1个数中的最大值了。答案是可以,这就要用到单调队列。
–
单调队列实现的大致过程:
1、维护队首(如果队首已经在当前元素的m个之前,则队首后移,head++)
2、在队尾插入(每插入一个就要从队尾开始往前去除冗杂状态,保持单调性)
–
此时每个元素最多入队/出队1次,复杂度降为O(N),空间也是S(N)。
简单举例应用 数列为:6 4 10 10 8 6 4 2 12 14 N=10,K=3; 那么我们构造一个长度为3的单调递减队列:
首先,那6和它的位置0放入队列中,我们用(6,0)表示,每一步插入元素时队列中的元素如下
插入6:(6,0);
插入4:(6,0),(4,1);
插入10:(10,2);
插入第二个10,保留后面那个:(10,3);
插入8:(10,3),(8,4);
插入6:(10,3),(8,4),(6,5);
插入4,之前的10已经超出范围所以排掉:(8,4),(6,5),(4,6);
插入2,同理:(6,5),(4,6),(2,7);
插入12:(12,8);
插入14:(14,9);
那么f(i)就是第i步时队列当中的首元素:6,6,10,10,10,10,8,6,12,14
同理,最小值也可以用单调队列来做。
单调是一种思想,当我们解决问题的时候发现有许多冗杂无用的状态时,我们可以采用单调思想,用单调队列或类似于单调队列的方法去除冗杂状态,保存我们想要的状态。
求最小数代码(直接用数组模拟单调队列):
struct node
{
int x,y;
}v[1000]; //x表示值,y表示位置 可以理解为下标
N, m, mMin[1000];
void getmin(int* a)
{
// 默认起始位置为1 因为插入是v[++tail]故初始化为0
int i,head=1,tail=0;
// 根据题目 前m-1个先直接进入队列
for(i=1; i<m; i++)
{
while(head<=tail && v[tail].x>=a[i]) tail--;
v[++tail].x = a[i], v[tail].y = i;
}
for(; i<=N; i++)
{
// 当前元素比队尾元素小则删除队尾元素
while(head<=tail && v[tail].x>=a[i]) tail--;
// 在队尾插入新元素并维持单调增队列
v[++tail].x = a[i], v[tail].y = i;
// 把已经超出范围的从head开始排出
while(v[head].y < i-m+1) head++;
// 每个队首则是目前m个数的最小值
mMin[i-m+1] = v[head].x;
}
}