数据结构之单调栈

需求:

给定一个数组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. 使栈顶出栈的元素i一定是出栈元素st.top向右找的第一个值小于st.top的值的元素,即nums[i]<nums[st.top]
  2. 新的栈顶一定是旧栈顶向右找到的第一个大于等于旧栈顶值的元素。反过来看就是:当栈中至少存在两个元素时,元素st.top2一定是元素st.top1向左找到的第一个值小于等于st.top1的值的元素,即nums[st.top2]<=nums[st.top1],若栈中只有一个元素时,可以设其左边有个虚拟的元素-1,即设top2 = -1;

1,2总结起来就是,能向左找到第一个值小于等于栈顶值的元素,也能向右找到第一个值小于栈顶值的元素

其次,为保证数组nums中所有元素都被遍历到可以:
4. 在nums末尾添加哨兵后,保证所有元素均会被出栈一次,从而进入计算结果环节,换句话说就是每个元素都会被遍历而进入:向左边找最近的小于等于当前元素值下标,向右找小于当前元素值的最近元素下标

如果理解不了单调栈的性质,可以记住凡是有上面提到的那个需求,就能套单调栈模板解决问题,多看看下面单调栈的应用,学会如何运用,待熟悉以后再回来仔细推导单调栈的性质。提示:可以用反证思维证明性质1,2

完整伪C++模板

左求最近小于等于 右求最近 小于

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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值