前言
在关于一维数组的各类题目中,存在着一种神奇的思想,它不需要对一个数组来回恶心的遍历,也不需要苦思冥想动态规划方程,这就是单调栈。顾名思义,单调栈是一个栈,需要题目符合先进后出的方式,同时它是单调的,也就是栈里面的元素要么递增,要么递减,一般是边入栈边处理,遇到不单调的情况便进行处理。总之,在我们读取元素是从左到右的,而处理元素是从右到左的,就可以考虑单调栈的思想了。
模板
在这里提前说明,算法题是千变万化的,这个模板只能当作代码的初步骨架或解题思路,是我在一些算法题里总结的,不具有普适性。
//将数据全部假设为int
//s一般存储数组下标而不存储值,因为可以根据下标找值
int func(vector<int>& nums){ //一般参数只有一个题目数组
int n=nums.size();
int res; //结果
stack<int> s; //定义辅助栈
for(int i=0;i<n;i++){ //对原数组进行遍历(也可以用while循环)
//当不满足单调栈,需要对栈顶进行处理
while(!s.empty()&&nums[s.top()]?nums[i]){ //?一般是<或>
... //根据题目做某些操作
s.pop();
}
s.push(i); //存储下标
}
return res;
}
例题1-每日温度
题目描述
739. 每日温度 - 力扣(LeetCode) (leetcode-cn.com)
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指在第 i 天之后,才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
输入: temperatures = [30,60,90]
输出: [1,1,0]
1 <= temperatures.length <= 105
30 <= temperatures[i] <= 100
思路
题目意思简单明了,需要我们找到比当前温度更高的温度位置,典型的从左到右遍历,先处理最右边的数据,那就可以定义单调递减栈,遇到增加的情况不断处理栈顶。
代码如下:
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n=temperatures.size();
stack<int> s;
vector<int> res(n,0); //根据题目,没有升高的元素为0,那就全部初始化为0
for(int i=0;i<n;i++){
//正常栈里都是温度递减,当有增加情况开始处理
while(!s.empty()&&temperatures[s.top()]<temperatures[i]){
res[s.top()]=i-s.top();
s.pop();
}
s.push(i);
}
return res;
}
};
例题2-柱状图中最大的矩形
题目描述
84. 柱状图中最大的矩形 - 力扣(LeetCode) (leetcode-cn.com)
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例 2:
输入: heights = [2,4]
输出: 4
1 <= heights.length <=105
0 <= heights[i] <= 104
思路
这道题先考虑暴力解法,对于每一根柱子进行左右扩散,求它所能达到的最大矩形面积,这种“中心点扩散”的思想外部遍历一遍,内部遍历两遍,时间复杂度很高。于是考虑单调栈思想,设一个递增单调栈,当遇到减少的元素说明栈顶那个柱子确定的矩形面积可以计算了,因此整体只需要遍历一遍就可以解决问题。
代码如下:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n=heights.size();
stack<int> s;
int width,height;
int res=0; //记录结果
for(int i=0;i<n;i++){
while(!s.empty()&&heights[s.top()]>heights[i]){
height=heights[s.top()]; //当前柱子确定的矩形高度
s.pop();
//当前柱子确定的矩形宽度
width=i; //s为空,其实就是i-0
if(!s.empty()){
width=i-s.top()-1; //s不空,左侧是s.top()
}
res=max(res,height*width);
}
s.push(i);
}
while(!s.empty()){
height=heights[s.top()];
s.pop();
width=n;
if(!s.empty()){
width=n-s.top()-1;
}
res=max(res,height*width);
}
return res;
}
};
这里宽度的计算需要仔细推敲,另外代码其实有冗余的地方,可以用哨兵的方式进行简化。
例题3-接雨水
题目描述
42. 接雨水 - 力扣(LeetCode) (leetcode-cn.com)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
思路
这是一道非常经典的题目了,解法也有很多种。可以用动态规划从左到右遍历记录遇到过的最大值,再同样从右向左遍历一遍。也可以采用“中心点扩散”思路,不过时间复杂度较高。这里单调栈只需要一次遍历,我们可以维护一个单调递减栈,当遇到增加的柱子,说明栈顶元素可以形成低洼(前面一直在递减),把积水量记录下来,当遍历完毕,积水量也就计算完毕了。
代码如下:
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
stack<int> s;
int n = height.size();
for (int i = 0; i < n; ++i) {
while (!s.empty() && height[i] > height[s.top()]) {
int top = s.top();
s.pop();
if (s.empty()) {
break;
}
int left = s.top();
int currWidth = i - left - 1;
//接水高度为两边最小减去当前柱子高度
int currHeight = min(height[left], height[i]) - height[top];
res += currWidth * currHeight;
}
s.push(i);
}
return res;
}
};
写在最后
算法题目千千万万,虽说具体问题具体分析,但是题目总逃不过核心知识点。对一些题目做好总结偶尔再回顾一下思路比追求数量好的多。单调栈也是由一般规律演化来的一种思想,很多题目就是这样,先想暴力解法然后看能不能剪枝,或者用空间换时间,数据结构不就是干这事嘛,用特定的结构处理高时间复杂度的问题,所以空间换时间是个大体思想。另外就是,每个问题有其特定场景,尤其是某些细节,光在脑子里勾勒很容易乱,这时可以找个普遍的例子,在纸上写写画画,栈是怎样变化的,中间值是怎样计算的,下标是怎样找的,很快思路就通了。