单调栈神养成手册
单调栈
单调栈是一种特殊的栈结构,它的特点是栈内元素始终保持单调递增或者单调递减。在处理一些与数组元素的大小关系、位置关系相关的问题时,单调栈能发挥重要作用,其时间复杂度通常为O(n) ,因为每个元素最多进栈和出栈一次。
分类:(从下至上的单调)
- 单调递增栈:栈内元素从栈底到栈顶依次递增。当新元素入栈时,如果新元素小于栈顶元素,则不断弹出栈顶元素,直到满足新元素大于等于栈顶元素,再将新元素入栈。(栈顶最大)
- 单调递减栈:栈内元素从栈底到栈顶依次递减。当新元素入栈时,如果新元素大于栈顶元素,则不断弹出栈顶元素,直到满足新元素小于等于栈顶元素,再将新元素入栈。(栈顶最小)
应用场景:
寻找下一个更大元素:
对于数组中的每个元素,找到它右边第一个比它大的元素。可以使用单调递减栈来解决,如果新元素num大于栈顶元素top时,栈顶元素top的下一个更大元素就是新元素num。
找到右边第一个更大的数的下标(固定下标从1开始)
首先明确:
- 栈中存储的是下标(为了迎合题目)
- 倒序构建单调递减栈(栈顶最小)
- 获取的答案是线性的,即遍历到这个元素的时候就已经可以明确答案
为什么要构建单调递减栈:因为要求的是第一个更大值,而不是最大值
为什么要使用倒序:如果从前向后构建单调栈,获取的是栈顶top是前面(左边)的大值,实际是求的是左边第一个比它大的数,所有才用倒序刚好可以得到右边第一个更大的数。
对于1 4 2 3 5这个样例,每个数对应的下一个更大的数(注意为了方便理解,这里不是下标,注意区分)是 4 5 3 5 无,他的栈最后是这样的(注意这里依旧是存的数)。
1 <---- 栈顶
4
5 <---- 栈底
我们可以发现4比2和3大,所有4把2和3都挤出了栈。而且4是即比2大又比3大,所以读到4要把2和3同时踢出栈,要在遍历到4的时候把2和3都踢了。
为了体现基本逻辑,有以下代码(栈存的都是下标):
for(int i=n;i>0;) {
if(s.empty()) {s.push(i);i--;}
else {
if(a[s.top()]>a[i]) {
ans[i]=s.top();
s.push(i);
i--;
}
else {
s.pop();
}
}
}
初始时为空先入栈,且此时右边界不存在右边更大值。
遍历中,如果栈顶这个栈中最小值大于当前值,栈顶就是当前值右边第一个更大值。如果栈中最小值小于当前值,排出栈顶直到栈中最小值大于当前值。
更简洁更符合逻辑的写法:
for (int i = n ; i > 0; --i) {
// 弹出栈中所有小于等于当前元素的下标(维护单调递减)
while (!st.empty() && a[st.top()] <= a[i]) {
st.pop();
}
// 确定当前元素的下一个更大元素下标
ans[i] = st.empty() ? 0 : st.top();
st.push(i);
}
对于不同的题目a[st.top()] <= a[i] 即st.top()和 i 的关系应当符合题意
这种线性的,答案是根据遍历过的值取得的,所以从右边遍历是左边的值,从右开始是右边的值。
出栈式(殊途同归)
原来我们使用的单调栈是线性获取答案的,即遍历到时获取答案,实际上,还有另一种方式实现单调栈。假设寻找右边第一个更大的数,我们维护一个单调栈,栈低是最大的,也就是单调递减栈。
我们这个出栈式的方式只能存索引,因为他不是线性的寻找答案,他的原理是判断当前遍历到的值是哪几个之前遍历过的值的第一个更大值。
还是对于1 4 2 3 5这个样例,我们先过一遍,首先是【1】(规定右边是栈顶),之后遍历到4,4>1所有4是1右边第一个更大值,所以弹出1变成【4】之后2不大于4,【4 , 2】,3>2,【4 ,3】,5>3,5>4,【5】。
我们可以发现如果当前值大于栈顶,那他就是栈顶的右边的第一个更大值。
这种出栈式对于找不到更大值的没有处理,所有一般最好初始话就设置好不存在的,或者再最后加一步排栈。
for (int i = 0; i < n; ++i) {
while (!st.empty() && a[i] > a[st.top()]) {
ans[st.top()] = i;
st.pop();
}
st.push(i);
}
这种出栈式,从右开始找的是左值,从左开始找的是右值,因为值是后面找到的。
对于更大还是更小改一下大小判断即可。
计算直方图中的最大矩形面积:
给定一组非负整数表示直方图的高度,计算直方图中能够勾勒出的最大矩形面积。可以使用单调递增栈来解决,通过维护栈内元素的单调性,找到每个柱子左右两边第一个比它矮的柱子的位置,从而计算出以该柱子为高的矩形的最大面积。

找到左右第一个更小值,两个更小值夹的就是以当前值为高的最大矩形,因此最大面积计算公式为
ans = max(ans,(r[i] - l[i] - 1) * heights[i]);
如果不存在左侧最小值,那左边全都算入,赋值为-1.
如果不存在右侧最小值,那右边全都算入,赋值为n。
下标为0 1 2 ...... n-1
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
stack<int> st;
vector<int> l(n);
vector<int> r(n);
for(int i = n - 1;i >= 0; --i){
while(!st.empty() && heights[st.top()] >= heights[i]){
st.pop();
}
r[i] = st.empty() ? n : st.top();
st.push(i);
}
stack<int>().swap(st);//清空栈的一种方式
for(int i = 0;i < n; ++i){
while(!st.empty() && heights[st.top()] >= heights[i]){
st.pop();
}
l[i] = st.empty() ? -1 : st.top();
st.push(i);
}
int ans = 0;
for(int i = 0;i < n;++i){
ans = max(ans,(r[i] - l[i] - 1) * heights[i]);
}
return ans;
}
};
还有一种常数优化(作用不太大)
r是出栈式,l是遍历式,因为出栈式是在后面找答案,而遍历是是在前面找答案,出栈式对于找不到的情况不能用三元运算符赋值(因为只要没结束你就不知道能不能找到答案),所以要最后统一给没有答案的赋值(或者初始化时就赋值)
stack<int> st;
vector<int> l(n,-1);
vector<int> r(n,n);
for(int i = 0;i < n; ++i){
while(!st.empty() && heights[st.top()] >= heights[i]){
r[st.top()] = i;
st.pop();
}
l[i] = st.empty() ? -1 : st.top();
st.push(i);
}
这道题这么写完全是赶巧了,因为如果有若干个柱子的高度都等于矩形的高度,那么最右侧的那根柱子是可以求出正确的右边界的。因为你这几个相等的柱子左边第一个比他小的是同一个,因为这些相等的直接只能插入比他大的,然后这几个相等的柱子中除了最右边的那个柱子的右边第一个更大值以外都是错误的,但是计算最右边柱子的时候得到的值是正确的。因此实际上这种方法并没有严格的实现找到每个元素的左边第一个更小的和右边的第一个更小的。
股票价格跨度问题:
给定一个股票价格数组,对于每个价格,计算其价格跨度(即该价格之前连续小于等于它的价格的天数)。可以使用单调递减栈来解决,栈中存储的是元素的索引,通过比较栈顶元素对应的价格和当前价格,计算出价格跨度。
实际上就是找左边第一个更大值罢了。

浙公网安备 33010602011771号