单调栈和单调队列

单调栈

问题:找每个数左边第一个比它大的数 (前一个更大元素) 给定数组:[3, 1, 4, 2] 对于数组里每一个元素,求它左侧第一个比它大的数字的下标(从1开始),找不到就记 0。

先手动算一遍:

  • 3:往左看,没有元素记为0
  • 1:往左看,第一个最大元素为1
  • 4:往左看,3,1都比4小,记为0
  • 2:往左看,3,1,4中 4是第一个比2大的数 记录3
  • 答案:[0,1,0,3]

使用暴力方法套两层循环轻而易举解决问题,这里不过多赘述,时间复杂度为O(N^2)

但是如果数组中有一万个元素?一亿个元素?一百万亿个元素呢?时间复杂度就爆了

所以这个时候就要祭出单调栈来解决这个问题了

我们使用单调递减栈 求解「每个元素左侧第一个比它大的元素」,栈内存储的是数组元素的下标,通过下标访问原数组来比较数值大小,仅对数组做一次遍历:

  1. 若栈为空,直接将当前元素的下标压入栈。此时该元素是遍历到的第一个元素,它左侧不存在更大的元素,对应记录答案。
  2. 若栈不为空,持续判断:只要栈顶下标对应的数组值小于当前元素,就说明这些栈内元素,不可能成为后续任何元素的左侧最近更大值 ,可以直接弹出。 为了能够持续、批量弹出这类更小的元素,我们约定栈始终保持自底向上单调递减。
  3. 经过上一步弹出操作后,要么栈已经为空,要么剩下的栈顶下标对应的数值一定大于当前元素,这个栈顶对应的元素,就是当前元素左侧第一个比它大的元素,记录该栈顶下标作为答案;最后再把当前元素的下标压入栈中。

得到找左边第一个比之大的元素的代码:

cpp 复制代码
#include<iostream>
using namespace std;
const int N=1e5+10;
int arr[N],ret[N];
#include<stack>
stack<int> st;
int n;
//口诀:左边正遍历,右边反遍历,找小单调增,找大单调减
void solve1()//arr[i]左边比它要大的最近的数
{
    for(int i=1;i<=n;i++)
    {
        while(st.size()&&arr[st.top()]<=arr[i])st.pop();//存下标,递减栈
        if(st.empty())ret[i]=0;
        else ret[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)cout<<arr[ret[i]]<<" ";
    cout<<endl;
}

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>arr[i];
    solve1();
    return 0;
}
循环 i arri while 弹出过程 弹出后栈 st reti 压入 i 之后栈 st 含义(左边最近更大值下标)
1 0 栈空,无弹出 \[\] 0 1 左边没有更大元素
2 1 arr 1=0 ≤ 1,弹出 1 \[\] 0 2 左边没有更大元素
3 0 arr 2=1 ≤ 0 不成立,无弹出 2 2 2,3 下标 2,值为 1
4 3 arr 3=0 ≤ 3,弹出 3;arr 2=1 ≤ 3,弹出 2 \[\] 0 4 左边没有更大元素

单调栈模板总结:

同理我们可以得出找到当前元素小的左边最近的元素

当前元素大的右边最近的元素、当前元素小的右边最近的元素 这三个问题的求解模板:

找到当前元素大的左边最近的元素 使用单调递减栈,从左向右遍历,元素逐次比对

找到当前元素小的左边最近的元素 使用单调递增栈,从左向右遍历,元素逐次比对

找到当前元素大的右边最近的元素 使用单调递减栈,从右向左遍历,元素逐次比对

找到当前元素小的右边最近的元素 使用单调递增栈,从右向左遍历,元素逐次比对、

口诀:左边正遍历,右边反遍历,找小单调增,找大单调减。

单调栈模板:

cpp 复制代码
//单调栈模板
#include<iostream>
using namespace std;
const int N=1e5+10;
int arr[N],ret[N];
#include<stack>
stack<int> st;
int n;
//口诀:左边正遍历,右边反遍历,找小单调增,找大单调减
void solve1()//arr[i]左边比它要大的最近的数
{
    for(int i=1;i<=n;i++)
    {
        while(st.size()&&arr[st.top()]<=arr[i])st.pop();//存下标,递减栈
        if(st.empty())ret[i]=0;
        else ret[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)cout<<arr[ret[i]]<<" ";
    cout<<endl;
}
void solve2()//arr[i]左边比它要小的最近的数
{
    for(int i=1;i<=n;i++)
    {
        while(st.size()&&arr[st.top()]>=arr[i])st.pop();//存下标,递增栈
        if(st.empty())ret[i]=0;
        else ret[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)cout<<arr[ret[i]]<<" ";
    cout<<endl;
}
void solve3()//arr[i]右边比它要大的最近的数
{
    for(int i=n;i>=1;i--)
    {
        while(st.size()&&arr[st.top()]<=arr[i])st.pop();//存下标,递减栈
        if(st.empty())ret[i]=0;
        else ret[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)cout<<arr[ret[i]]<<" ";
    cout<<endl;
}
void solve4()//arr[i]右边比它要小的最近的数
{
    for(int i=n;i>=1;i--)
    {
        while(st.size()&&arr[st.top()]>=arr[i])st.pop();//存下标,递增栈
        if(st.empty())ret[i]=0;
        else ret[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)cout<<arr[ret[i]]<<" ";
    cout<<endl;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>arr[i];
    solve4();
    return 0;
}

例题精讲:

题目来源: P1901 发射站 - 洛谷

解题思路使用两次单调栈,分别找到当前发射战左边第一个比它高的发射站和右边第一个比它高的发射站,在记录每个发射战接收能量的数组中找到这两个发射站并加上当前发射站发射出的能量。

题解代码:

cpp 复制代码
//洛谷 P1901 发射站
#include<iostream>
#include<algorithm>
#include<stack>
using namespace std;
const int N=1e6+10;
int index_left[N];
int index_right[N];
int arr[N];
struct nums
{
    int h,v;
}nums[N];

int main()
{
    int n;cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>nums[i].h>>nums[i].v;
    }
    stack<int> st;
    //正向遍历,单减栈,找到左侧比它高的发射站
    for(int i=1;i<=n;i++)
    {
        while(st.size()&&nums[st.top()].h<=nums[i].h)st.pop();
        if(st.size())index_left[i]=st.top();
        st.push(i);
    }
    while(st.size())st.pop();
    for(int i=n;i>=1;i--)
    {
        while(st.size()&&nums[st.top()].h<=nums[i].h)st.pop();
        if(st.size())index_right[i]=st.top();
        st.push(i);
    }
    for(int i=1;i<=n;i++)
    {
       int left=index_left[i];
       int right=index_right[i];
       arr[right]+=nums[i].v;
       arr[left]+=nums[i].v;
    }
    int ret=0;
    for(int i=1;i<=n;i++)
    {
        ret=max(ret,arr[i]);
    }
    cout<<ret<<endl;
    return 0;
}

练习:SP1805 HISTOGRA - Largest Rectangle in a Histogram - 洛谷

代码:点拨:注意如果找不到右边比当前长方形矮的长方形,index_right如何赋值比较合适?

cpp 复制代码
//洛谷 SP1805 HISTOGRA - Largest Rectangle in a Histogram
#include<iostream>
#include<stack>
using namespace std;
const int N=1e5+10;
using LL=long long;
LL index_left[N];
LL index_right[N];
LL nums[N];
int main()
{
    int a;
    while(cin>>a,a)
    {
        stack<LL> st;//递增栈,存下标
        for(int i=1;i<=a;i++)index_left[i]=index_right[i]=nums[i]=0;
        for(int i=1;i<=a;i++)cin>>nums[i];
        //找到第i个矩形左边第一个比它矮的矩形
        for(int i=1;i<=a;i++)
        {
            while(st.size()&&nums[st.top()]>=nums[i])st.pop();
            if(st.size())index_left[i]=st.top();
            else index_left[i]=0;
            st.push(i);
        }
        while(st.size())st.pop();
        //找到第i个矩形右边第一个比它矮的矩形
        for(int i=a;i>=1;i--)
        {
            while(st.size()&&nums[st.top()]>=nums[i])st.pop();
            if(st.size())index_right[i]=st.top();
            else index_right[i]=a+1;//不存在,说明i个矩形之后所有矩形都比它高,都可以贡献最大矩形
            st.push(i);
        }
        LL ret=0;
        for(int i=1;i<=a;i++)
        {
            LL x=index_left[i];
            LL y=index_right[i];
            LL sum=nums[i]*(y-x-1);
            ret=max(ret,sum);
        }
        cout<<ret<<endl;
    }
    return 0;
}

单调队列

239. 滑动窗口最大值 - 力扣(LeetCode)

问题:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值。

示例:nums = [1,3,-1,-3,5,3,6,7], k = 3

先手算一遍:

第一个窗口 元素1,3,-1 最大元素为3

第二个窗口 元素3,-1,-3 最大元素为3

第三个窗口 元素-1 -3 5 最大元素为5

第四个窗口 元素 -3 5 3 最大元素为5

第五个窗口 元素 5 3 6 最大元素为6

第六个窗口 元素 3 6 7 最大元素为7

答案:[3,3,5,5,6,7]

不难看出使用暴力解法两层循环依次枚举每个窗口的所有元素,逐一比对找出每个窗口中的最大值,这个代码很好实现,再次不再过多赘述了。时间复杂度为O(kN)

要是k和N一大,时间复杂度就爆了,所以我们要找到一个线性时间复杂度的方法------单调队列来解决这个问题:我们想要线性时间求解滑动窗口最大值,于是用单调队列;队列里面不放数值,存数组下标,依靠下标访问原数组比较大小,全程只遍历数组一次。

处理每一个新遍历到的元素时,分两类情况去维护队列尾部:

  1. 如果队列为空:直接把当前元素的下标入队。
  2. 如果当前元素,比队列中一部分元素对应的数值更大: 这一部分队列里的元素可以直接删掉。往后所有窗口里,最大值不可能再是这些被删掉的元素了,当前这个新元素比它们更大、位置还更靠后,存活时间更长;持续删掉队尾满足这个条件的元素之后,再把当前下标入队;此时当前窗口最大值就是在下标队列中元素的最大值。
  3. 如果当前元素比队列一部分元素对应的数值更小: 直接把当前下标加到队尾。现在队列里记录的最大值,有可能随着窗口滑动,下标滑出窗口被删掉;这个更小的元素可以作为 "后备候选最大值",等原来的最大值出圈之后顶上。

另外要注意两条检查规则:① 当前遍历下标 ≥ k 的时候,才形成一个合法完整窗口,这时才可以记录窗口最大值;下标还不到 k,窗口没成型,不能 标记最大值。 ② 检查队列中的下标:如果下标已经不在当前窗口范围内(已经划出窗口),要删除这个非法下标。

为了方便增删元素 我们选用单调递减双端队列,三种操作对应:

  • 队尾删除:新来元素更大的时候,不断尾删队尾,直到队尾对应数值 > 当前元素(对应上面情况 2)
  • 队尾插入:新来元素更小的时候,直接尾插下标(对应上面情况 3)
  • 队首删除:检查队首下标,滑出窗口就头删(对应注意里的第②条)
i arri 步骤 1:队尾弹出 push 后 dq 步骤 3:dq.back ()-dq.front ()+1 >3? 操作后 dq i>=3? Indexi 当前窗口 i-k+1, i 窗口元素 窗口最大值
1 1 dq 空,不弹 1 1-1+1=1 ≤3,不弹 1 ❌ - - - -
2 3 arr 1=1 ≤3 → pop 1;dq 空 2 2-2+1=1 ≤3,不弹 2 ❌ - - - -
3 -1 arr 2=3 > -1,不弹 2,3 3-2+1=2 ≤3,不弹 2,3 ✅ Index3=2 1,3 1,3,-1 3
4 -3 arr 3=-1 > -3,不弹 2,3,4 4-2+1=3 ≤3,不弹 2,3,4 ✅ Index4=2 2,4 3,-1,-3 3
5 5 arr 4=-3 ≤5 → pop4;arr 3=-1 ≤5 → pop3;arr 2=3 ≤5 → pop2;dq 空 5 5-5+1=1 ≤3,不弹 5 ✅ Index5=5 3,5 -1,-3,5 5
6 3 arr 5=5 > 3,不弹 5,6 6-5+1=2 ≤3,不弹 5,6 ✅ Index6=5 4,6 -3,5,3 5
7 6 arr 6=3 ≤6 → pop6;arr 5=5 ≤6 → pop5;dq 空 7 7-7+1=1 ≤3,不弹 7 ✅ Index7=7 5,7 5,3,6 6
8 7 arr 7=6 ≤7 → pop7;dq 空 8 8-8+1=1 ≤3,不弹 8 ✅ Index8=8 6,8 3,6,7 7

模板:

cpp 复制代码
#include<iostream>
using namespace std;
const int N=1e6+10;
int arr[N];
int Index[N];
#include<deque>
deque<int> dq;//存下标
int n,k;
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		cin>>arr[i];
	}
	//先找到每个窗口的最大值,使用单调递减队列
	for(int i=1;i<=n;i++)
	{
		while(dq.size()&&arr[dq.back()]<=arr[i])dq.pop_back();
		dq.push_back(i);
		if(dq.back()-dq.front()+1>k)dq.pop_front();
		if(i>=k)Index[i]=dq.front();
	}
	for(int i=k;i<=n;i++)cout<<arr[Index[i]]<<" ";
	cout<<endl;
	
	return 0;
}

本题代码:

cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
       deque<int> dq;
       int sz=nums.size();
       vector<int> ret(sz-k+1,0);
       for(int i=0;i<sz;i++)
       {
             while(dq.size()&&nums[dq.back()]<=nums[i])dq.pop_back();
             dq.push_back(i);
             if(dq.back()-dq.front()+1>k)dq.pop_front();
             if(i>=k-1) ret[i-k+1]=nums[dq.front()];
       }
       return ret;
    }
};

我们得出了在滑动窗口中找最大值,使用单减队列,

同理我们可以得出在滑动窗口中找到最小值,要使用单增序列

每次删除由于窗口滑动而出队的下标的判断条件也可以为if(dq.front()<i+1-m) 删除操作,

这样的代码显然更直观。
我们可以通过下面这道模板题来练习:

P1886 【模板】单调队列 / 滑动窗口 - 洛谷

思路:使用两次单调队列,第一次遍历,使用单增队列,获取每个窗口的最小值并输出

第二次遍历,使用单减队列,获取每个窗口的最大值并输出。

cpp 复制代码
#include<iostream>
using namespace std;
const int N=1e6+10;
int arr[N];
#include<deque>
deque<int> dq;//存下标
int n,k;
int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)
    {
        cin>>arr[i];
    }
    //先找到每个窗口的最小值,使用升序队列
    for(int i=1;i<=n;i++)
    {
        while(dq.size()&&arr[dq.back()]>=arr[i])dq.pop_back();
        dq.push_back(i);
        if(dq.back()-dq.front()+1>k)dq.pop_front();
        if(i>=k)cout<<arr[dq.front()]<<" ";
    }
    cout<<endl;
    dq.clear();
    //再找到每个窗口的最大值,使用降序队列
    for(int i=1;i<=n;i++)
    {
        while(dq.size()&&arr[dq.back()]<=arr[i])dq.pop_back();
        dq.push_back(i);
        if(dq.back()-dq.front()+1>k)dq.pop_front();
        if(i>=k)cout<<arr[dq.front()]<<" ";
    }
    cout<<endl;
    return 0;
}

练习:P2251 质量检测 - 洛谷

代码:

cpp 复制代码
//洛谷P2251 质量检测
#include<iostream>
using namespace std;
const int N=1e5+10;
#include<queue>
int nums[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>nums[i];
    }
    deque<int> dq;//单增队列找最小值,存下标
    for(int i=1;i<=n;i++)
    {
        while(dq.size()&&nums[dq.back()]>=nums[i])dq.pop_back();
        dq.push_back(i);
        if(dq.front()<i+1-m)dq.pop_front();
        if(i>=m)cout<<nums[dq.front()]<<endl;
    }
    return 0;
}
相关推荐
UIU1144 小时前
P16237 [蓝桥杯 2026 省 B] 应急布线
数据结构·c++·算法·蓝桥杯
黎相思7 小时前
BTree
数据结构·b树·b+树
UIU11418 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
niyongsheng21 小时前
3D 装箱算法工程化:数据结构、放置启发式与支撑规则
数据结构·算法·rust
叉烧滑蛋1 天前
B站台湾大学郭彦甫|MATLAB 学习笔记|04 变量与档案存取
数据结构
j7~1 天前
【穷举、深搜与回溯算法】一棵决策树讲透全排列与子集
数据结构·c++·算法·leetcode·回溯·递归
not coder1 天前
一文讲通--从树到红黑树
数据结构·算法
爱吃香菜的初学者1 天前
八.数据结构——哈希表(1)
数据结构·散列表
二十七日秋1 天前
算法刷题笔记「Day 1|四道 OJ 入门题里的深坑」
c语言·汇编·数据结构·c++·笔记·python·算法