注:按照代码随想录的刷题指南进行,自己总结补充,以加深印象
参考链接:https://leetcode-cn.com/circle/article/wGp7Y9/
题目来源:力扣(LeetCode)
一、基础知识
单调栈顾名思义,利用栈的结构和特性解决问题。
相当于用空间换时间
关键点在于栈内元素的单调顺序, 以及栈内存放的元素。
使用时机:
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
二、相关题目
739-每日温度-中等
- 给定每日温度,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数 question
- 解题思路:利用单调栈来保存未找到答案的元素: 栈底到栈顶顺序递减,对于一个新元素,比较栈顶是否大于,如果大于栈顶元素,弹出并记录
- 注意:栈里存储的是数组的下标,因为这样才能计算两者的距离
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;//存放的是索引
vector<int> result(temperatures.size(), 0);
st.push(0);
for(int i = 1; i < temperatures.size(); i++){
int now = temperatures[i];
while(!st.empty() && now > temperatures[st.top()]){
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return result;
}
496-下一个更大元素I-简单
- 对于两个数组num1 和num2, num1是num2的子集,对于num1的每一个元素,找出num2里它对应位置右边的第一个大于它的元素 question
- 解题思路:只需要把该题看作每日温度的题,区别就是需要格外判断一下nums2[i]是否存在于nums[1],如果存在,才把nums[i]放入栈中
- 在判断是否存在的实现上,最初用了hash数组,后来改用unordered_set, 节省空间。
503-下一个更大元素II-中等
- 给定一个循环数组,返回每个元素右边对应的第一个比它大的元素 question
- 和每日温度的套路差不多,只需要把循环数组处理一下即可
- 循环数组: 可以两个数组拼接,但显然很麻烦;所以通常操作是取模实现。
for(int i = 1; i < nums.size() * 2; i++){
int index = i%nums.size();
}
42-接雨水-困难
- 给定一组高低不同的柱子,判断可以接多少雨水, 经典题目 question
- 接雨水肯定需要三个柱子,中间要有一个凹槽
- 单调栈:从栈尾到栈头从大到小,
- 关键点是如果当前要添加的元素大于栈头元素,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
- 第二个是如何计算雨水面积,看高度和宽度的计算代码
int trap(vector<int>& height) {
stack<int> st;
int ans = 0;
st.push(0);
for(int i = 1; i < height.size(); i++){
int num = height[i];
while(!st.empty() && num > height[st.top()]){
//栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
int mid = height[st.top()];
st.pop();
if(!st.empty()){
int h = min(height[st.top()], num) - mid;//记得要减去mid, 左边的柱子并不pop出来
int w = i - st.top() - 1;
ans += (h * w);
}
//如果相等,不做处理
}
st.push(i);
}
return ans;
}
- 双指针思想: 核心思想即对于每一列柱子,找出其两侧最高的柱子,就可以得到它能存储的雨水量。注意第一个柱子和最后一个柱子不能接水,不计算。
- 会超时
时间O(n^2), 超出时间限制,
int trap(vector<int>& height) {
int left = 0, right = 0;
int ans = 0;
//第一个和最后一个柱子不能接水,不计算
for(int i = 1 ; i < height.size() - 1; i++){
left = right = height[i];
for(int k = i - 1; k >= 0; k--){
if(height[k] > left)
left = height[k];
}
for(int k = i + 1; k < height.size(); k++){
if(height[k] > right)
right = height[k];
}
ans += min(left, right) - height[i];
}
return ans;
}
-
优化: 为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。
-
动态规划:当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
int trap(vector<int>& height) {
int ans = 0;
vector<int> leftHeight(height.size(), 0);
vector<int> rightHeight(height.size(), 0);
leftHeight[0] = height[0];
for(int i = 1; i < height.size(); i++){
leftHeight[i] = max(leftHeight[i - 1], height[i]);
}
int size = height.size();
rightHeight[size - 1] = height[size - 1];
for(int i = size - 2; i >= 0; i--){
rightHeight[i] = max(rightHeight[i + 1], height[i]);
}
for(int i = 1; i < size - 1; i++){
ans += min(leftHeight[i] , rightHeight[i]) - height[i];
}
return ans;
}
84-矩形图中最大的矩阵-困难
- 给定一组柱子的高度,找出这些柱子能构成的矩形的最大面积 question
- 直接按列的组合遍历,超时
- 单调栈,单调栈,栈底到栈顶从小到大。以本题单调栈的顺序正好与接雨水反过来。接雨水是找每个柱子左右两边第一个大于该柱子高度的柱子,本题是找每个柱子左右两边第一个小于该柱子的柱子,理由如下:
- 对于一个高度为h的柱子i,我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 hh 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 hh,并且就是 i能够扩展到的最远范围
- 此时其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
- 注意点:哨兵思想:在heights前后添加0,用来应对一下两种情况
a. 弹栈的时候,栈为空;
b. 遍历完成以后,栈中还有元素;
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
int area = 0;
int best = 0;
heights.insert(heights.begin(), 0);
heights.push_back(0);
st.push(0);
for(int i = 1; i < heights.size(); i++){
int num = heights[i];
while(!st.empty() && num < heights[st.top()]){
int mid = heights[st.top()];
st.pop();
if(!st.empty()){
int left = heights[st.top()];
area = mid * (i - st.top() - 1);
if(area > best) best = area;
}
}
st.push(i);
}
return best;
}
};
三、总结
1、单调栈的使用时机
2、循环数组的处理 503题
3、分析题目就是一个抽丝剥茧的过程,找到最本质的问题,然后在找和本质问题的特殊性