在算法的世界里,数据结构是解决问题的基石,而单调栈绝对是其中 “低调又强大” 的存在。它看似只是普通栈的 “升级版”,却能将原本需要 O (n²) 时间复杂度的问题,一键优化到 O (n),堪称处理 “找最近最值” 类问题的 “神器”。本文将从单调栈的核心原理讲起,结合实战例题,手把手带你吃透单调栈的用法,让你彻底搞懂这一高频面试 / 竞赛考点。下面就让我们正式开始吧!
提到栈,大家首先想到的是 “先进后出” 的线性结构,而单调栈,顾名思义,就是在普通栈的基础上,给元素加上了 “单调性” 的约束 —— 栈内的元素必须严格保持递增或递减(也可根据需求调整为非严格递增 / 递减)。
话不多说,先看基础代码实现。我们以 C++ 为例,分别实现单调递增栈和单调递减栈:
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10; // 适配大数据量场景
int a[N], n;
// 维护单调递增栈:栈内元素从小到大
void monotonicIncreasingStack() {
stack<int> st;
for (int i = 1; i <= n; i++) {
// 关键操作:弹出所有大于等于当前元素的栈顶元素,保证单调性
while (st.size() && st.top() >= a[i]) {
st.pop();
}
st.push(a[i]); // 插入当前元素,栈仍保持递增
}
}// 维护单调递减栈:栈内元素从大到小
void monotonicDecreasingStack() {
stack<int> st;
for (int i = 1; i <= n; i++) {
// 关键操作:弹出所有小于等于当前元素的栈顶元素,保证单调性
while (st.size() && st.top() <= a[i]) {
st.pop();
}
st.push(a[i]); // 插入当前元素,栈仍保持递减
}
}大家可能会疑惑:“为什么要先弹出元素再入栈?” 其实这正是单调栈的核心 ——为了保证栈的单调性不被破坏。
比如维护单调递增栈时,若当前元素a[i]比栈顶元素小,说明栈顶元素 “挡路” 了:如果直接入栈,栈就会出现 “大元素在前、小元素在后” 的情况,违背递增规则。因此需要先弹出所有≥a[i]的元素,直到栈顶元素<a[i](或栈为空),再将a[i]入栈。
举个直观的例子:假设数组a = [5, 3, 7, 2],维护单调递增栈的过程:
最终栈内元素为 [2],完美保持递增特性。
单调栈的核心应用场景,总结起来就是 “找最近最值”—— 给定一个元素,找到它左侧 / 右侧最近的、比它大 / 小的元素的位置。这四类问题看似不同,实则原理相通,掌握一种就能举一反三。
先记住一句 “口诀”:找左侧,正遍历;找右侧,逆遍历;比它大,单调减;比它小,单调增。这句话能帮你快速确定遍历方向和栈的单调性,下文会反复验证。
给定数组a,对于每个元素a[i],找到其左侧第一个比它大的元素的下标;若不存在,返回 0。
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N]; // 存储每个元素的答案
void findLeftLarger() {
stack<int> st; // 单调递减栈,存下标
for (int i = 1; i <= n; i++) {
// 弹出所有≤a[i]的元素,剩下的栈顶就是左侧最近的更大元素
while (st.size() && a[st.top()] <= a[i]) {
st.pop();
}
// 栈非空则栈顶是答案,否则为0
if (st.size()) {
ret[i] = st.top();
} else {
ret[i] = 0;
}
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];
}
findLeftLarger();
return 0;
}输入:
9
1 4 10 6 3 3 15 21 8输出:
0 0 0 3 4 4 0 0 8解释:
给定数组a,对于每个元素a[i],找到其左侧第一个比它小的元素的下标;若不存在,返回 0。
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];
void findLeftSmaller() {
stack<int> st; // 单调递增栈,存下标
for (int i = 1; i <= n; i++) {
// 弹出所有≥a[i]的元素,剩下的栈顶就是左侧最近的更小元素
while (st.size() && a[st.top()] >= a[i]) {
st.pop();
}
ret[i] = st.size() ? st.top() : 0;
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];
}
findLeftSmaller();
return 0;
}输入:
9
1 4 10 6 3 3 15 21 8输出:
0 1 2 2 1 1 6 7 6解释:
给定数组a,对于每个元素a[i],找到其右侧第一个比它大的元素的下标;若不存在,返回 0。
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];
void findRightLarger() {
stack<int> st; // 单调递减栈,存下标
for (int i = n; i >= 1; i--) {
// 弹出所有≤a[i]的元素,剩下的栈顶就是右侧最近的更大元素
while (st.size() && a[st.top()] <= a[i]) {
st.pop();
}
ret[i] = st.size() ? st.top() : 0;
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];
}
findRightLarger();
return 0;
}输入:
9
1 4 10 6 3 3 15 21 8输出:
2 3 7 7 7 7 8 0 0解释:
给定数组a,对于每个元素a[i],找到其右侧第一个比它小的元素的下标;若不存在,返回 0。
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];
void findRightSmaller() {
stack<int> st; // 单调递增栈,存下标
for (int i = n; i >= 1; i--) {
// 弹出所有≥a[i]的元素,剩下的栈顶就是右侧最近的更小元素
while (st.size() && a[st.top()] >= a[i]) {
st.pop();
}
ret[i] = st.size() ? st.top() : 0;
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];
}
findRightSmaller();
return 0;
}输入:
9
1 4 10 6 3 3 15 21 8输出:
0 5 4 5 0 0 9 9 0解释:
光说不练假把式,我们以洛谷经典模板题为例,完整拆解单调栈的解题流程。
题目链接:https://www.luogu.com.cn/problem/P5788

给出项数为n的整数数列a[1...n],定义函数f(i)代表数列中第i个元素之后第一个大于a[i]的元素的下标,即f(i)=min{ j | i<j ≤n, a[j]>a[i] }。若不存在,f(i)=0。试求出f(1...n)。
n,第二行n个正整数a[1...n];n个整数,表示f(1),f(2),...,f(n);这道题本质是 “找右侧最近的更大元素”,直接套用前文的思路:
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10; // 适配3e6的大数据量
int n;
int a[N];
int ret[N]; // 存储每个元素的答案
int main() {
ios::sync_with_stdio(false); // 关闭同步,加速输入输出
cin.tie(0); // 解绑cin和cout
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
stack<int> st; // 单调递减栈,存下标
for (int i = n; i >= 1; i--) {
// 弹出所有≤a[i]的元素,保证栈的单调性
while (st.size() && a[st.top()] <= a[i]) {
st.pop();
}
// 栈顶即为右侧最近的更大元素下标
ret[i] = st.size() ? st.top() : 0;
st.push(i); // 入栈当前下标
}
// 输出结果
for (int i = 1; i <= n; i++) {
cout << ret[i] << " ";
}
cout << endl;
return 0;
}输入:
5
1 4 2 3 5输出:
2 5 4 5 0解释:
a[1]=1,右侧最近的更大元素是a[2]=4 → f(1)=2;a[2]=4,右侧最近的更大元素是a[5]=5 → f(2)=5;a[3]=2,右侧最近的更大元素是a[4]=3 → f(3)=4;a[4]=3,右侧最近的更大元素是a[5]=5 → f(4)=5;a[5]=5,右侧无更大元素 → f (5)=0。除了基础的 “找最近最值”,单调栈还能解决很多经典算法题,下面选取两个高频考点,带你深入理解。
题目链接:https://www.luogu.com.cn/problem/P1901

某地有N个能量发射站排成一行,每个发射站i有高度H[i]和能量值V[i],发射的能量只被两边最近的且比它高的发射站接收。求接收最多能量的发射站的能量值。
#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() {
ios::sync_with_stdio(false);
cin.tie(0);
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;
}输入:
3
4 2
3 5
6 10输出:7
解释:
题目链接:https://www.luogu.com.cn/problem/SP1805

给定n个宽度为 1 的矩形的高度,求包含于这些矩形的最大子矩形面积。
i,找到左侧 / 右侧最近的更矮矩形,确定该矩形能扩展的最大宽度,面积 = 高度 × 宽度;#include <iostream>
#include <stack>
using namespace std;
typedef long long LL; // 防止面积溢出
const int N = 1e5 + 10;
int n;
LL h[N];
LL left_bound[N], right_bound[N]; // 左侧/右侧最近更矮矩形的下标
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> n, n) { // 输入0结束
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();
}
left_bound[i] = st.size() ? st.top() : 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();
}
right_bound[i] = st.size() ? st.top() : n + 1;
st.push(i);
}
// 第三步:计算最大面积
LL ret = 0;
for (int i = 1; i <= n; i++) {
LL width = right_bound[i] - left_bound[i] - 1;
ret = max(ret, h[i] * width);
}
cout << ret << endl;
}
return 0;
}输入:
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0输出:
8
4000解释:
int和long long的选择,避免溢出(如面积、能量值计算);ios::sync_with_stdio(false); cin.tie(0);,否则会超时;while(st.size()) st.pop(););单调栈看似简单,实则是 “贪心思想” 与 “栈结构” 的完美结合。它的核心价值在于将嵌套循环的暴力解法,优化为线性时间复杂度,这也是算法优化的核心思路 —— 用空间换时间,用数据结构约束逻辑。 掌握单调栈,不仅能解决 “找最近最值” 类问题,更能理解 “如何通过维护数据结构的特性来简化问题”。建议大家结合本文的例题,手动模拟栈的入栈、出栈过程,真正吃透每一步操作的意义。相信通过反复练习,你也能熟练运用单调栈,轻松应对算法面试和竞赛中的相关问题! 如果本文对你有帮助,欢迎点赞、收藏、关注~后续还会更新单调队列、并查集等数据结构的实战解析,敬请期待!