单调栈 = 保持栈内元素单调递增或单调递减的栈。
专门解决「找左边/右边第一个比当前大/小的元素」这类问题。
看到以下特征,直接想单调栈:
- 找下一个更大元素(Next Greater Element)
- 找下一个更小元素(Next Smaller Element)
- 找前一个更大/更小元素
- 柱状图中最大矩形、接雨水 这类经典题
时间复杂度: O(n),每个元素入栈出栈各一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 输入 nums,返回每个元素右边第一个比它大的元素值,没有返回 -1
vector<int> nextGreaterElement(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st; // 存下标,不是值
// 从后往前遍历
for (int i = n - 1; i >= 0; i--) {
// 栈顶比当前小,肯定不是答案,弹出
while (!st.empty() && nums[st.top()] <= nums[i]) {
st.pop();
}
// 此时栈顶就是右边第一个更大的
if (!st.empty()) res[i] = nums[st.top()];
st.push(i);
}
return res;
}
|
| 目标 | 遍历方向 | 弹出条件 | 单调性 |
|---|
| 右边第一个更大 | 从后往前 | <= 当前 | 栈底→栈顶 递减 |
| 右边第一个更小 | 从后往前 | >= 当前 | 栈底→栈顶 递增 |
| 左边第一个更大 | 从前往后 | <= 当前 | 栈底→栈顶 递减 |
| 左边第一个更小 | 从前往后 | >= 当前 | 栈底→栈顶 递增 |
口诀:找大就弹小,找小就弹大
找右边第一个更高温度的天数差。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> res(n, 0);
stack<int> st;
for (int i = n - 1; i >= 0; i--) {
while (!st.empty() && temperatures[st.top()] <= temperatures[i]) {
st.pop();
}
if (!st.empty()) res[i] = st.top() - i; // 存下标,算天数差
st.push(i);
}
return res;
}
|
数组是环形的,把数组翻倍(遍历 2n 次,下标取模)即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st;
for (int i = 2 * n - 1; i >= 0; i--) {
while (!st.empty() && st.top() <= nums[i % n]) {
st.pop();
}
if (i < n && !st.empty()) res[i] = st.top();
st.push(nums[i % n]);
}
return res;
}
|
经典中的经典,核心:对每个柱子,找左右第一个比它矮的,宽度 = 右-左-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
vector<int> left(n, -1); // 左边第一个更小的下标
vector<int> right(n, n); // 右边第一个更小的下标
stack<int> st;
// 找左边第一个更小
for (int i = 0; i < n; i++) {
while (!st.empty() && heights[st.top()] >= heights[i]) {
st.pop();
}
if (!st.empty()) left[i] = st.top();
st.push(i);
}
// 清空栈,找右边第一个更小
while (!st.empty()) st.pop();
for (int i = n - 1; i >= 0; i--) {
while (!st.empty() && heights[st.top()] >= heights[i]) {
st.pop();
}
if (!st.empty()) right[i] = st.top();
st.push(i);
}
// 计算面积
int ans = 0;
for (int i = 0; i < n; i++) {
ans = max(ans, heights[i] * (right[i] - left[i] - 1));
}
return ans;
}
|
也可以用单调栈做(也有双指针解法)。
- 判断类型:找大还是找小?左边还是右边?
- 确定遍历方向:找右边从后往前,找左边从前往后
- 确定弹出条件:找大弹小(<=),找小弹大(>=)
- 栈存什么:通常存下标(方便算距离),也可以存值
- 初始化答案:默认 -1 或 0 或 n
等号要不要加?
- 严格更大:用
<= 弹出(相等的也弹掉) - 大于等于:用
< 弹出 - 看题目要求,「更大」还是「大于等于」
存下标还是存值?
- 需要算距离/索引 → 存下标
- 只需要值 → 存值也可以
循环数组:遍历 2n 次,下标取模 n
栈为空时:说明没有符合条件的元素,答案为 -1(或边界值)
| 题目 | 类型 | 难度 |
|---|
| 496. 下一个更大元素 I | 模板题 | 简单 |
| 739. 每日温度 | 模板题 | 中等 |
| 503. 下一个更大元素 II | 循环数组 | 中等 |
| 84. 柱状图中最大的矩形 | 经典难题 | 困难 |
| 42. 接雨水 | 多种方法 | 困难 |
| 901. 股票价格跨度 | 左边第一个更大 | 中等 |
| 316. 去除重复字母 | 单调栈+贪心 | 中等 |
| 402. 移掉 K 位数字 | 单调栈+贪心 | 中等 |