
| 🔭 个人主页: 散峰而望 |
|---|
《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》
《C++》《算法竞赛从入门到获奖》《人工智能》《AI Agent》
愿为出海月,不做归山云
🎬博主简介



【数据结构】单调栈与单调队列深度解析:从模板到实战,一网打尽
- 前言
- [1. 单调栈](#1. 单调栈)
-
- [1.1 什么是单调栈](#1.1 什么是单调栈)
- [1.2 单调栈解决的问题](#1.2 单调栈解决的问题)
- [1.3 【模板】单调栈](#1.3 【模板】单调栈)
- [1.4 发射站](#1.4 发射站)
- [1.5 HISTOGRA - Largest Rectangle in a Histogram](#1.5 HISTOGRA - Largest Rectangle in a Histogram)
- [2. 单调队列](#2. 单调队列)
-
- [2.1 什么是单调队列](#2.1 什么是单调队列)
- [2.2 单调队列解决的问题](#2.2 单调队列解决的问题)
- [2.3 单调队列 / 滑动窗口](#2.3 单调队列 / 滑动窗口)
- [2.3 质量检测](#2.3 质量检测)
- 结语
前言
数据结构是算法设计的核心工具,单调栈和单调队列作为两种特殊的线性结构,在解决特定类型问题时展现出极高的效率。单调栈通过维护元素的单调性,能够快速处理最近邻相关的问题;单调队列则擅长在滑动窗口中动态维护极值。这两种结构广泛用于数组优化、区间查询等场景。
本解析从基础模板出发,逐步深入典型例题,涵盖发射站能量传递、柱状图最大矩形等经典问题,同时结合滑动窗口、质量检测等实际案例,系统剖析单调栈与单调队列的应用范式。通过代码实现与复杂度分析,帮助掌握这两种高效工具的底层逻辑与实战技巧。
1. 单调栈
1.1 什么是单调栈
单调栈是一种特殊的栈数据结构,它要求栈中的元素保持严格的单调递增或单调递减的顺序。这种单调性可以是从栈底到栈顶单调递增(单调递增栈),也可以是从栈底到栈顶单调递减(单调递减栈)。
实现方式:
以单调递增栈为例,其基本实现逻辑如下:
- 遍历数组元素
- 对于当前元素:
- 如果栈不为空且栈顶元素大于当前元素,则弹出栈顶元素
- 重复上述操作直到栈为空或栈顶元素小于等于当前元素
- 将当前元素压入栈中
维护单调栈的意义:
-
高效解决特定问题:
- 可以快速找到数组中某个元素左边/右边第一个比它大或小的元素
- 典型应用包括:
- 柱状图中的最大矩形面积问题
- 接雨水问题
- 每日温度问题(找下一个更高温度的天数)
-
时间复杂度优势:
- 每个元素最多入栈和出栈各一次
- 总体时间复杂度为 O(n),比暴力解法更高效
-
空间复杂度优化:
- 只需要额外的栈空间
- 最坏情况下空间复杂度为 O(n)
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e6 + 10;
int a[N], n;
void test1()
{
stack<int> st;//维护一个单调递增的栈
for(int i = 1; i <= n; i++)
{
//栈里面大于等于a[i]的元素全部出栈
while(st.size() && st.top() >= a[i]) st.pop();
st.push(a[i]);
}
}
void test2()
{
stack<int> st;//维护一个单调递减的栈
for(int i = 1; i <= n; i++)
{
//栈里面小于等于a[i]的元素全部出栈
while(st.size() && st.top() <= a[i]) st.pop();
st.push(a[i]);
}
}
1.2 单调栈解决的问题
单调栈能帮助我们解决以下四个问题:
- 寻找当前元素左侧,离它最近,并且比它大的元素在哪;
- 寻找当前元素左侧,离它最近,并且比它小的元素在哪;
- 寻找当前元素右侧,离它最近,并且比它大的元素在哪;
- 寻找当前元素右侧,离它最近,并且比它小的元素在哪。
虽然是四个问题,但是原理是一致的。因此,只要解决一个,举一反三就可以解决剩下的几个。
- 寻找当前元素左侧,离它最近,并且比它大的元素在哪
从左往右 遍历元素,构造一个单调递减的栈。插入当前位置的元素的时:
- 如果栈为空,则左侧不存在比当前元素大的元素;
- 如果栈非空,插入当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈里面存的是每个元素的下标。
【测试用例】
输入:
9
1 4 10 6 3 3 15 21 8
输出:
0 0 0 3 4 4 0 0 8
【代码实现】
cpp
//寻找当前元素左侧,离它最近,并且比它大的元素在哪
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e6 + 10;
int a[N], n;
int ret[N];
void test()
{
stack<int> st;
for(int i = 1; i <= n; i++)
{
while(st.size() && a[st.top()] <= a[i]) st.pop();
//如果此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i] = st.top();
st.push(i);//存的是下标
}
for(int i = 1; i <= n; i++)
{
cout << ret[i] << " ";
}
cout << endl;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
test(); cout << endl;
return 0;
}
【测试结果】

- 寻找当前元素左侧,离它最近,并且比它小的元素在哪
从左往右 遍历元素,构造一个单调递增的栈。插入当前位置的元素的时:
- 如果栈为空,则左侧不存在比当前元素小的元素;
- 如果栈非空,插入当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈里面存的是每个元素的下标。
【测试用例】
输入:
9
1 4 10 6 3 3 15 21 8
输出:
0 1 2 2 1 1 6 7 6
【代码实现】
cpp
//寻找当前元素左侧,离它最近,并且比它小的元素在哪
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e6 + 10;
int a[N], n;
int ret[N];
void test()
{
stack<int> st;
for(int i = 1; i <= n; i++)
{
while(st.size() && a[st.top()] >= a[i]) st.pop();
//如果此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i] = st.top();
st.push(i);//存的是下标
}
for(int i = 1; i <= n; i++)
{
cout << ret[i] << " ";
}
cout << endl;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
test(); cout << endl;
return 0;
}
【测试结果】

- 寻找当前元素右侧,离它最近,并且比它大的元素在哪
从右往左 遍历元素,构造一个单调递减的栈。插入当前位置的元素的时:
- 如果栈为空,则左侧不存在比当前元素大的元素;
- 如果栈非空,插入当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈里面存的是每个元素的下标。
【测试用例】
输入:
9
1 4 10 6 3 3 15 21 8
输出:
2 3 7 7 7 7 8 0 0
【代码实现】
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e6 + 10;
int a[N], n;
int ret[N];
void test()
{
stack<int> st;
for(int i = n; i >= 1; i--)
{
while(st.size() && a[st.top()] <= a[i]) st.pop();
// 此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i] = st.top();
st.push(i); // 存的是下标
}
for(int i = 1; i <= n; i++)
{
cout << ret[i] << " ";
}
cout << endl;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
test(); cout << endl;
return 0;
}
【测试结果】

- 寻找当前元素右侧,离它最近,并且比它小的元素在哪
从右往左 遍历元素,构造一个单调递增的栈。插入当前位置的元素的时:
- 如果栈为空,则左侧不存在比当前元素小的元素;
- 如果栈非空,插入当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈里面存的是每个元素的下标。
【测试用例】
输入:
9
1 4 10 6 3 3 15 21 8
输出:
0 5 4 5 0 0 9 9 0
【代码实现】
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 1e6 + 10;
int a[N], n;
int ret[N];
void test()
{
stack<int> st;
for(int i = n; i >= 1; i--)
{
while(st.size() && a[st.top()] >= a[i]) st.pop();
// 此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i] = st.top();
st.push(i); // 存的是下标
}
for(int i = 1; i <= n; i++)
{
cout << ret[i] << " ";
}
cout << endl;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
test(); cout << endl;
return 0;
}
【测试结果】

总结:
- 找左侧,正遍历;找右侧,逆遍历;
- 比它大,单调减;比它小,单调增。
1.3 【模板】单调栈

算法原理:
右侧离它最近并且比它大的元素:
- 逆序遍历数组;
- 构造一个单调递减的栈;
- 进栈时,栈顶元素就是最终结果。
参考代码:
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int n;
int a[N];
int ret[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
stack<int> st;
for(int i = n; i >= 1; i--)
{
while(st.size() && a[st.top()] <= a[i]) st.pop();
if(st.size()) ret[i] = st.top();
st.push(i);
}
for(int i = 1; i <= n; i++) cout << ret[i] << " ";
cout << endl;
return 0;
}
1.4 发射站

算法原理:
寻找当前元素右侧,离它最近,并且比它大的元素在哪 以及寻找当前元素左侧,离它最近,并且比它大的元素在哪的结合。
剩下的就是简单的模拟过程。
参考代码:
cpp
#include <iostream>
#include <stack>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int n;
LL h[N], v[N];
LL sum[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> h[i] >> v[i];
stack<int> st;
//找左边
for(int i = 1; i <= n; i++)
{
while(st.size() && h[st.top()] <= h[i]) st.pop();
if(st.size())
{
sum[st.top()] += v[i];
}
st.push(i);
}
//找右边
while(st.size()) st.pop();//清空
for(int i = n; i >= 1; i--)
{
while(st.size() && h[st.top()] <= h[i]) st.pop();
if(st.size())
{
sum[st.top()] += v[i];
}
st.push(i);
}
LL ret = 0;
for(int i = 1; i <= n; i++) ret = max(ret, sum[i]);
cout << ret << endl;
return 0;
}
1.5 HISTOGRA - Largest Rectangle in a Histogram
HISTOGRA - Largest Rectangle in a Histogram(洛谷)
HISTOGRA - Largest Rectangle in a Histogram(SPOJ)

算法原理:
- 扫描线
一种常用于计算几何和计算机图形学中的高效算法,特别适用于解决平面或空间中的覆盖问题。其基本思想是:
- 将问题转化为在一条虚拟的扫描线移动过程中处理事件
- 通常从左到右或从上到下扫描
- 维护当前扫描线状态的数据结构(如线段树)
- 在关键事件点(如线段端点)触发处理逻辑
- 单调栈
对于 x 位置子矩阵,找到左侧离它最近并且比它小的位置 y ,那么 [x+1, y] 之间就是该矩阵能到达的左端。
同理再找到右侧离它最近并且比它小的位置 z ,那么 [y, z−1] 之间就是该矩阵能到达的右端。
对于每一个子矩阵,求出它向左以及向右能延伸的最大长度即可。

参考代码:
cpp
#include <iostream>
#include <stack>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n;
LL h[N];
LL x[N], y[N];
int main()
{
while(cin >> n, n)
{
for(int i = 1; i <= n; i++) cin >> h[i];
stack<int> st;
//找左边
for(int i = 1; i <= n; i++)
{
while(st.size() && h[st.top()] >= h[i]) st.pop();
if(st.size()) x[i] = st.top();
else x[i] = 0;
st.push(i);
}
while(st.size()) st.pop();
//找右边
for(int i = n; i >= 1; i--)
{
while(st.size() && h[st.top()] >= h[i]) st.pop();
if(st.size()) y[i] = st.top();
else y[i] = n + 1;
st.push(i);
}
LL ret = 0;
for(int i = 1; i <= n; i++)
{
ret = max(ret, h[i] * (y[i] - x[i] - 1));
}
cout << ret << endl;
}
return 0;
}
2. 单调队列
2.1 什么是单调队列
单调队列是一种特殊的双端队列,它要求队列中的元素始终保持单调递增或单调递减的特性。与普通队列只能在一端进行入队操作、另一端进行出队操作不同,单调队列允许在两端进行操作,这为实现其单调性维护提供了便利。
具体来说,单调队列可以分为两种类型:
- 单调递增队列:队列中的元素按照从队首到队尾的顺序严格递增
- 单调递减队列:队列中的元素按照从队首到队尾的顺序严格递减
2.2 单调队列解决的问题
一般用于解决滑动窗口内最大值最小值问题,以及优化动态规划。
2.3 单调队列 / 滑动窗口

算法原理:
窗口内最大值:
从左往右遍历元素,维护一个单调递减的队列:
- 当前元素进队之后,注意维护队列内的元素在大小为 k 的窗口内;
- 此时队头元素就是最大值。
窗口内最小值:
从左往右遍历元素,维护一个单调递增的队列:
- 当前元素进队之后,注意维护队列内的元素在大小为 k 的窗口内;
- 此时队头元素就是最小值。
cpp
#include <iostream>
#include <deque>
using namespace std;
const int N = 1e6 + 10;
int n, k;
int a[N];
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
deque<int> q;//存下标
//窗口内最小值 - 单调递增的队列
for(int i = 1; i <= n; i++)
{
while(q.size() && a[q.back()] >= a[i]) q.pop_back();
q.push_back(i);
//判断队列里面的元素是否在合法窗口内
if(q.back() - q.front() + 1 > k) q.pop_front();
if(i >= k) cout << a[q.front()] << " ";
}
cout << endl;
q.clear();//清空队列
//窗口内最大值 - 单调递减的队列
for(int i = 1; i <= n; i++)
{
while(q.size() && a[q.back()] <= a[i]) q.pop_back();
q.push_back(i);
//判断队列里面的元素是否在合法窗口内
if(q.back() - q.front() + 1 > k) q.pop_front();
if(i >= k) cout << a[q.front()] << " ";
}
cout << endl;
return 0;
}
2.3 质量检测

算法原理:
cpp
#include <iostream>
#include <deque>
using namespace std;
const int N = 1e6 + 10;
int n, k;
int a[N];
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
//窗口最小值
deque<int> q;
for(int i = 1; i <= n; i++)
{
while(q.size() && a[q.back()] >= a[i]) q.pop_back();
q.push_back(i);
//维护队列内元素合法
if(i - q.front() + 1 > k) q.pop_front();
//输出结果
if(i >= k) cout << a[q.front()] << endl;
}
return 0;
}
结语
单调栈与单调队列是解决区间极值问题的高效工具。通过维护数据的单调性,它们能够将许多看似复杂的问题优化到线性时间复杂度。掌握这两种数据结构,能够显著提升解决算法问题的能力。从模板到实战,理解其核心思想并灵活应用,是算法学习中的关键一步。
愿诸君能一起共渡重重浪,终见缛彩遥分地,繁光远缀天。
