← 目录 / 算法文档 · 模块六 单调栈与单调队列 / 6.1 单调栈

6.1 单调栈

栈里始终保持递增或递减——靠这一个小规则,把"找右边第一个更大的数"从 O(n²) 降到 O(n)。

本页目录
① 为什么需要单调栈

有一类常见问题:给一个数组,对每一个数,都要找出它右边第一个比它大的数。最直接的想法是枚举法——对每个数,往右一个个找,直到找到更大的为止。这种写法最坏情况下是 O(n²)(2.1 节讨论过,数据量一大就容易超时)。

单调栈能把这件事优化到 O(n)。它是一种特殊用法的栈:在往栈里塞新数据之前,先把栈顶那些"不再有用"的数据清理掉,让栈里剩下的数据始终保持递增或递减的顺序——这正是"单调"这个名字的来源。

② 用单调栈找右边第一个更大的数

用数组 {3, 1, 4, 1, 5, 9, 2, 6} 演示。维护一个栈,栈里存的是下标,并且要求栈底到栈顶对应的数值递减。从左到右扫描每个数 arr[i]:只要栈顶元素对应的数值arr[i],说明栈顶那个数找到了它"右边第一个更大的数"——就是 arr[i] 本身,把栈顶弹出并记录答案;重复这个弹出动作,直到栈顶比 arr[i] 大(或者栈空了)为止,再把 i 压入栈。

C++ · 单调栈求右边第一个更大的数
1vector<int> NextGreater(vector<int> arr)
2{
3 int n = arr.size();
4 vector<int> result(n, -1); // 默认-1,表示没有找到更大的数
5 stack<int> st; // 存下标,栈里的数值保持递减
6
7 for (int i = 0; i < n; i++)
8 {
9 while (!st.empty() && arr[st.top()] < arr[i])
10 {
11 result[st.top()] = arr[i]; // arr[i]就是栈顶元素要找的答案
12 st.pop();
13 }
14 st.push(i);
15 }
16 return result;
17}
{3, 1, 4, 1, 5, 9, 2, 6} —— 单调栈逐步执行过程
i=0, i=1:3、1 直接入栈(栈顶比新数大,不用弹)
栈(从下到上):3 1
1
3
i=2(值4):栈顶1<4弹出 → result[1]=4;栈顶3<4弹出 → result[0]=4;4入栈
栈(从下到上):4
4
1
3
i=3(值1):栈顶4>1,不弹,1直接入栈
栈(从下到上):4 1
1
4
i=4(值5):栈顶1<5弹出 → result[3]=5;栈顶4<5弹出 → result[2]=5;5入栈
栈(从下到上):5
5
1
4
i=5(值9):栈顶5<9弹出 → result[4]=9;9入栈
栈(从下到上):9
9
5
i=6(值2):栈顶9>2,不弹,2直接入栈
栈(从下到上):9 2
2
9
i=7(值6):栈顶2<6弹出 → result[6]=6;栈顶9>6,不弹;6入栈
栈(从下到上):9 6
6
9
2
灰色格子是仍留在栈里的数,粉色虚线格子是这一步被弹出、同时确定了答案的数。扫描结束后,栈里剩下的 96 始终没有被弹出——它们右边再也没有比自己更大的数了,对应的答案保持默认值 -1
最终结果:每个数右边第一个比它大的数
3
原数组
1
4
1
5
9
2
6
4
结果
4
5
5
9
-1
6
-1
和暴力枚举法逐个验证的结果完全一致:{4,4,5,5,9,-1,6,-1}9 是整个数组里最大的数,右边不可能有比它更大的,所以结果是 -16 是数组最后一个数,右边没有任何数,同样是 -1
③ 为什么单调栈是 O(n) 的

看起来代码里有一层 for 套一层 while,很像两层嵌套循环,容易让人误以为是 O(n²)——但其实不是。关键在于:每个数最多只会被压入栈一次,也最多只会被弹出一次。一旦某个数被弹出,它就再也不会被重新压回栈里了。

🎯
整个数组扫描一遍,是 n 次入栈操作;而出栈操作的总次数,无论 while 循环在哪些位置触发了多少次,加起来也不可能超过 n 次(因为总共只有 n 个数能被弹出)。所以入栈、出栈两类操作加起来一共最多 2n 次,整体时间复杂度是严格的 O(n),而不是 O(n²)——这是单调栈类问题里非常经典的"均摊复杂度"分析思路。
20 个随机数 —— 暴力法 vs 单调栈,比较次数对比
暴力枚举
57 次
单调栈
33 次
这组数据规模还比较小,差距看起来不算夸张;但暴力法是 O(n²) 增长,单调栈是 O(n) 增长——数据量一旦变大(比如几十万个数),两者的差距会迅速拉开到几个数量级,暴力法很可能超时,单调栈依然能轻松应对。
④ 完整代码与对比
做法核心思路时间复杂度
暴力枚举对每个数,往右逐个找更大的数O(n²)
单调栈维护递减的栈,提前清理不再有用的数据O(n)
💡
"找右边第一个更大的数"只是单调栈最基础的用法。同样的框架稍作调整:把比较符号从 < 换成 >,就能找"右边第一个更小的数";把循环方向从左到右改成从右到左,就能找"左边第一个更大/更小的数"。这套"维护单调性、提前清理无用数据"的思路,后面在直方图最大矩形、股票买卖等问题里都会反复出现——掌握了这一节的框架,遇到变体时只需要调整比较条件,不需要重新设计算法。