给定一个数组nums,对于数组nums中的任意位置 i ,求:
1.向左找到第一个值小于等于位置 i 的值的元素下标
2.向右找到第一个值小于 位置i的值的元素下标
当将上面的“小于”替换成“大于”,用单调栈仍然能够求解
下面通过核心代码定义单调栈:
stack<int> st;
nums.push_back(INT_MIN);//添加哨兵
for(int i=0;i<nums.size();++i){
while(!st.empty() && nums[st.top()] > nums[i]){
int cur = st.top();
st.pop();
//在这里写确定边界和求中间结果的代码
}
st.push(i);
}
nums.erase(nums.end()-1); //删除哨兵
关键词:for, while, pop, push
单调栈的性质:
首先,需要明确一点,单调栈内的元素的值是单调从栈低到栈顶单调递增的,这样能够辅助我们用反证法分析各种可能的情形
然后,以栈顶元素st.top为中心研究性质1,2:
1,2总结起来就是,能向左找到第一个值小于等于栈顶值的元素,也能向右找到第一个值小于栈顶值的元素
其次,为保证数组nums中所有元素都被遍历到可以:
4. 在nums末尾添加哨兵后,保证所有元素均会被出栈一次,从而进入计算结果环节,换句话说就是每个元素都会被遍历而进入:向左边找最近的小于等于当前元素值下标,向右找小于当前元素值的最近元素下标
如果理解不了单调栈的性质,可以记住凡是有上面提到的那个需求,就能套单调栈模板解决问题,多看看下面单调栈的应用,学会如何运用,待熟悉以后再回来仔细推导单调栈的性质。提示:可以用反证思维证明性质1,2
左求最近小于等于 右求最近 小于
int monotic_stack(vector<int>& nums) {
int ans = 初值;
stack<int> st;
//在数据末尾添加哨兵,保证所有元素都会出栈,从而被遍历到
//为了保证heights没有被更改,可以在程序末尾删除此哨兵
nums.push_back(INT_MIN);
for(int i=0;i<nums.size();++i){
while(!st.empty() && nums[st.top()] > nums[i]){
int cur = st.top();
st.pop();
//利用单调栈的性质确定当前出栈元素的左右边界
int left = st.empty()? -1 : st.top();
int right = i;
//利用当前元素cur 和 左右边界 left , right 求解待求问题的中间结果
int tem_ans = f(cur,left,right);// f 是一个函数
ans = update(ans,tem_ans);
}
st.push(i);
}
nums.erase(nums.end()-1); //删除哨兵
return ans;
}
在学会上面的朴素版单调栈后,利用单调栈的性质,可以将模型升级为:求左右最近的值 大于 当前列值的元素下标
此时栈内元素是从栈底到栈顶单调递减
//单调栈实现左右找最近 大于
int monoticUpdate(vector<int>& nums) {
//套单调栈模板
int ans=0;
//这里要用数组模拟栈才能实现寻找左右最近【大于】当前栈顶位置值的边界
// 这是因为需要利用下标进行元素的遍历
vector<int> st;
//哨兵的设置要保证栈内所有元素均会出栈
nums.push_back(INT_MAX);
for(int i=0;i<nums.size();++i){
//注意是值的比较而不是下标位置的比较
while(!st.empty() && nums[st.back()] < nums[i]){
int left= -1, right = i;
// 分栈中只有一个个元素 和 至少两个元素讨论
int j;
// 从栈顶的倒是第二个位置开始遍历找大于栈顶元素值的位置,实际上找到的是
// 跳过相等元素的下边界
for(j=st.size()-2;j>=0;--j){
//注意是值的比较而不是下标位置的比较
if(nums[st[j]]>nums[st.back()]){
left = st[j];
break;
}
}
// 计算从栈顶开始向下过程中相等元素的个数
int num_eqEle = st.size()-1 - j;
while(num_eqEle --){
//计算中间结果,并更新最后的答案
int cur = st.back();
st.pop_back();
//测试用
cout<<cur<<" "<<left<<" "<<right<<endl;
ans = ...
}
}
st.push_back(i);
}
return ans;
}
升级版的核心是:当栈中出现值连续相等的元素时应该如何处理
leetcode 84
https://leetcode-cn.com/problems/largest-rectangle-in-histogram/
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
先看看官方题解中暴力解法的思路再来看以下代码
基本思路为对于当前位置,向左找到最近的值小于等于当前列值的边界的下标,向右找最近的值小于当前列值的边界的下标
//1. 先研究相邻元素都不相等时的情况
//2. 后再分析相邻元素存在相等时会怎么样
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int ans = 0;
stack<int> st;
//在数据末尾添加哨兵,保证所有元素都会出栈,从而被遍历到
//为了保证heights没有被更改,可以在程序末尾删除此哨兵
heights.push_back(INT_MIN);
for(int i=0;i<heights.size();++i){
while(!st.empty() && heights[st.top()] > heights[i]){
int cur = st.top();
st.pop();
//利用单调栈的性质确定当前出栈元素的左右边界
int left = st.empty()? -1 : st.top();
int right = i;
//计算当前出栈元素扩展构成的最大矩形面积
ans = max(ans,((right-1)-(left+1)+1)*heights[cur]);
}
st.push(i);
}
heights.erase(heights.end()-1); //删除哨兵
return ans;
}
};
更多利用单调栈性质相关的题目:
496 下一个更大元素 I(简单) 暴力解法、单调栈
739 每日温度(中等) 暴力解法 + 单调栈
901 股票价格跨度(中等) 「力扣」第 901 题:股票价格跨度(单调栈)
42 接雨水(困难) 暴力解法、优化、双指针、单调栈
581 最短无序连续子数组 单调栈、双指针
以下几题形似单调栈,但没有用到单调栈的性质,而是利用了贪心的思想
402 移掉K位数字
321 拼接最大数
316 去除重复字母(困难)
题解:https://blog.csdn.net/m0_50344530/article/details/116036182
部分题解:
496 秒杀!
//单调栈
//注意区分值和下标!
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int> dict;
vector<int> ans(nums1.size());
//用map记录值对应的位置
for(int i=0;i<nums1.size();++i){
dict[nums1[i]] = i;
}
nums2.push_back(INT_MAX);//哨兵的设置要能使栈内元素全部出栈
stack<int> st;
for(int i=0;i<nums2.size();++i){
while(!st.empty() && nums2[st.top()] < nums2[i]){
int cur = st.top();
st.pop();
//判断是否为需要计算的值
if( dict.count(nums2[cur]) ){
ans[dict[nums2[cur]]] = i == nums2.size()-1 ? -1 : nums2[i];
}
}
st.push(i);
}
return ans;
}
};
739 秒杀!
//套单调栈模板秒杀
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
vector<int> ans(T.size());
stack<int> st;
T.push_back(INT_MAX);//添加哨兵保证元素均会出栈
for(int i=0;i<T.size();++i){
while(!st.empty() && T[st.top()] < T[i]){
int cur = st.top();
st.pop();
int right = i;
ans[cur] = right==T.size()-1 ? 0 : right-cur;
}
st.push(i);
}
return ans;
}
};
901
理解单调栈性质后,改写得到
//单调栈
//在理解单调栈的性质后作修改,得到以下版本
class StockSpanner {
public:
//pair中,first表示值 second 表示 位置
stack<pair<int,int>> st;
int cur;
StockSpanner() {
cur = -1;
}
int next(int price) {
++cur;
while(!st.empty() && st.top().first <= price){
st.pop();
}
int left;
if(st.empty()){
left = -1;
} else{
left = st.top().second;
}
st.push({price,cur});
return cur - left;
}
};
/**
* Your StockSpanner object will be instantiated and called as such:
* StockSpanner* obj = new StockSpanner();
* int param_1 = obj->next(price);
*/
42 接雨水 经典题!
首先看看4种没有涉及栈的解法:
https://leetcode-cn.com/problems/trapping-rain-water/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-8/
单调栈解法:
原理看题解:
https://leetcode-cn.com/problems/trapping-rain-water/solution/trapping-rain-water-by-ikaruga/
// 5.单调栈 利用单调栈找当前位置的 左最近值大于等于 和 右最近值大于的 左右边界
// 由1逐层遍历的思路升级得到
//遍历以当前高度为可能的长条底边构造储水长方条
class Solution {
public:
int trap(vector<int>& height)
{
int ans = 0;
height.push_back(INT_MAX);//添加哨兵保证所有元素都会经历出栈
stack<int> st;
for(int i=0;i<height.size();++i){
while(!st.empty() && height[st.top()] < height[i]){
int cur = st.top();
st.pop();
int left = st.empty() ? -1 : st.top();
int right = i;
if(left!=-1 && right!= height.size()-1){
int rec_h = min(height[left],height[right]) - height[cur];
ans += rec_h*(right-left-1);
}
}
st.push(i);
}
return ans;
}
};
581 最短无序连续子数组
单调栈版:
//画出数据的折线图,思考左右边界在哪里
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
stack<int> st;
int left = nums.size(),right = -1;
//求左边界
for(int i=0;i<nums.size();++i){
while(!st.empty() && nums[st.top()] > nums[i]){
st.pop();
}
int l = st.empty()? -1 : st.top();
if(i != l+1){
left = min(left,l);
}
st.push(i);
}
//注意,要记得清空栈才能进入求右边界的过程!
// st.clear(); //无法使用这个,注意
//法一
// while(!st.empty()){
// st.pop();
// }
//法二
stack<int>().swap(st);
//求右边界
nums.push_back(INT_MAX);
for(int i=0;i<nums.size();++i){
while(!st.empty() && nums[st.top()] <= nums[i]){
int cur = st.top();
st.pop();
int r = i;
if(r != cur+1){
right = max(right,r);
}
}
st.push(i);
}
return right == -1 ? 0 : right-left - 1;
}
};
双指针版:
//v2 双指针
//观察折线图可发现:
// 1.右边界为从左到右遍历的小于max_val的最靠后的数字的位置
// 可分析具体曲线,分析下面算法是如何运作的
// 2.左边界为从右到左遍历的大于min_val的最靠前的数字的位置
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
int n = nums.size();
if(n <= 1) return 0;
int max_val = INT_MIN;
int min_val = INT_MAX;
//赋初值,使得当序列本身就是递增时,使得输出
//right - left + 1 = -1 - 0 + 1 = 0 , 从而满足题目的输出要求
int left = 0, right = -1;
//左往右遍历确定右边界
for(int i = 0; i < n; ++i){
if(max_val > nums[i]){
right = i;
} else{
max_val = nums[i];
}
}
//右往左遍历确定左边界
for(int i = n-1; i>=0;--i){
if(nums[i] > min_val){
left =i;
} else{
min_val = nums[i];
}
}
return right - left + 1;
}
};
其他题解待补充。。。
参考:
https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/bao-li-jie-fa-zhan-by-liweiwei1419/