数据结构之单调栈(按单减栈)

1. 单调(减)栈是什么

单调栈是这样一个栈,它里面的元素从栈底到栈顶依次递减。

2. 单调栈怎么生成

生成算法:
假如我们有一个数组nums[n]=[4,2,0,3,2,5],和一个空的栈stack。
我们遍历数组,对遇到的每一个元素num:

  1. 如果栈不空num值大于(等于)栈顶元素top,则将栈顶元素出栈,然后与新的top比较直到栈空或者num小于top;
  2. 否则,num进栈。

性质:

由上面的算法可知,单调栈有以下几条性质(分别用bottom, top, pop, num表示栈底元素4、新的栈顶元素3、旧的栈顶元素2、栈外元素等着进栈的元素5):
在这里插入图片描述

  1. num是pop的右边第一个更大的数;
    通俗地讲就是,谁把你从栈里赶出来的,谁就是你右边第一个更大的数。
  2. top是pop的左边第一个更大的数;
    也就是在栈中你的左邻居是你左边第一个更大的数。
  3. bottom是pop的左边最大的数。
    当然了根据单调减栈的性质,栈里所有的元素都比栈底小,也就是说栈里所有的元素的左边最大值都是栈底元素。

下面是栈的变化情况:

4
42
420
42
43
432
43
4
5

实现:

Stack<Integer> stack = new Stack<>();     
for (int i = 0; i < nums.length; i++){
    int num = nums[i];
    //注意下面的写法,出栈要用循环
    while(!stack.empty() && num > nums[stack.peek()]){
        stack.pop();
    }
    stack.push(i);
}

3. 单调栈可以干什么

既然是单调的,基本上遇到的问题就是和曲线的上升下降有关的,比如求极值(连续入栈时的第一次,连续出栈时的第一次),以及更多地,与极值(最值)有关的一些算法问题。
在这里插入图片描述

  1. 美学区间问题:
    如果对于区间[Si,Sj] (1<=i<j<=n)中任意的花篮都比Si高且比Sj低,那么这个区间称为一个美学区间。例如nums中的子数组[2,0,3],[4,2,0,3,2,5]。
    如果根本不存在美学区间,输出-1。
    如果存在美学区间,那么如果任意区间的长度都小于等于k,那么输出最大的长度,否则输出最大长度比k大多少(MaxLength-k)。
    解法:
    利用单调栈,我们可以很轻松求解。观察这两个子数组和在单调栈中的特点发现,只要是出现num大于top的,那么num就和top构成一个美学区间。此时,只需要记录num和top的最远距离即可。

  2. 牛视野问题:
    有一群牛站成一排,每头牛都是面朝右的,每头牛可以看到他右边身高比他小的牛。给出每头牛的身高,要求每头牛能看到的牛的总数。
    类似的还有:给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。
    解法:
    观察上面的折线图,每个点相当于一头牛,它能往右边看到多少取决于右边第一个比它高的牛,利用单调栈。每当出现一头比栈顶还高的牛,说明这是栈顶牛第一次遇到的比它自己高的牛,栈顶牛会被当前牛挡住自己的视线,因此此时就需要计算他俩之间的牛有几头,这就是栈顶牛可以看到的牛的个数。
    通过这两个例子,我们可以看到,单调栈的特点就是,栈顶元素遇到的第一个比自己大的元素就是将自己“赶出”栈的那个元素,谁把你赶出栈,谁就是你的“祖宗”,基本上的题目都是围绕着这个“祖宗”考察的。

  3. 下一个更大元素1
    给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集(子集的意思是nums1中元素相对顺序有可能发生变化)。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。
    nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
    示例 1:
    输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
    输出: [-1,3,-1]
    解释:
    对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。
    对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。
    对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。
    解法:
    已经提示的很明显了,右边第一个比x大的元素,这正是单调栈的特点。还是那句话,就看谁把你“赶出来”的,谁就是你的下一个更大的元素。类似的还有下一个更大元素2

  4. 单调栈的另一个特性:
    前面所说的都是求数组中元素右边的第一个比它更大的元素,即把栈顶元素赶出栈的那个。
    我们来看一下左边,数组中元素的左边最大的元素是什么?注意区别,区别在于两点,其一,最大;其二没有说是第一个(废话了,最大值是唯一的,不考虑有重复)。
    观察栈中情况可以很容易发现,这个最大的数就是栈顶元素。观察上图,对于4,后面的2,0,3,2在出栈的时候栈底元素都是4,并且她们左边的最大元素显然是4。这个东西其实画个折线图很容易理解。
    有时候要求右边最大,可以将数组元素逆序遍历。
    看一个问题接雨水
    给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
    在这里插入图片描述

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这 种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

计算雨水的方式决定了我们会采用单调栈的哪一个特性:

  1. 竖着算:
    在这里插入图片描述
    如图所示,每一个柱子的上面能够接住多少,取决于它左边最高的柱子和右边的最高的柱子.如果它的两边最高柱子中的较小者还是比它自己高,那就说明它可以接到雨水,该柱子(高设为num)上方雨水柱的高度为: m i n ( m a x L e f t , m a x R i g h t ) − n u m min(maxLeft, maxRight) - num min(maxLeft,maxRight)num
    显然这是利用的单调栈的第二个性质,即栈底元素是栈顶的最值.因为栈一般是没有取栈底元素的方法的,想要利用这个性质,要么可以自己实现,要么使用双端队列.
    实现:
class Solution {
    public int trap(int[] height) {
        int sum = 0;
        int[] leftMax = new int[height.length];
        int[] rightMax = new int[height.length];
        int max = -1;
        //计算每个柱子左边得最高得柱子
        for (int i = 0; i < height.length; i++){
            leftMax[i] = max;//如果左边没有柱子,那他左边得最大值是-1.
            if(height[i] > max) max = height[i];
        }
        max = -1;
        //计算每个柱子右边最高的柱子
        for (int i = height.length - 1; i > -1; i--){
            rightMax[i] = max;
            if(height[i] > max) max = height[i];
        }
        //计算每个柱子上面得雨水柱
        for (int i = 0; i < height.length; i++){
            int h = Math.min(leftMax[i], rightMax[i]);
            if(h > height[i]){
                sum += (h - height[i]);
            }
        }
        return sum;
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/zhu-zi-shang-mian-de-yu-shui-de-ji-suan-fang-fa-by/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  1. 横着算:
    在这里插入图片描述
    如上图所示,每一个横雨水柱的计算此时刚好和单调栈的第一个性质完全符合了.例如图中第3个横雨柱(宽度为3的那个),高为2,1,0的黑色柱子依次进栈,然后是高为1的黑色柱子站在栈前,"盯着"栈顶的柱子0,一看你没我高,还不赶紧滚出来,滚出来以后栈顶也是1,那么滚出来的0两边都比它高,说明是可以存水的,存的水量这么计算: ( r i g h t I n d e x − l e f t I n d e x − 1 ) ∗ ( m i n ( l e f t H , r i g h t H ) − s e l f H ) (rightIndex - leftIndex- 1) * (min(leftH, rightH) - selfH) (rightIndexleftIndex1)(min(leftH,rightH)selfH)
    什么意思呢,就是雨水柱的宽度,是由被赶出来的那个柱子两边比它高的柱子的距离-1确定的,高度呢,是由两边较低的那个柱子减去它自己的高度得到的.因此(6-4-1)*(1-0)=1;然后此时栈顶也是1,栈外也是1,怎么办,要么里面的1出来,要么外面的1进去,其实都可以.我们按照严格单调栈来处理,那么里面的1出来,这时计算出来的容水量为0;然后外面的1再和栈顶比较,小于栈顶的2,1进栈了,然后轮到高为3的黑柱子,冷冷一笑,小子,你以为你把你双胞胎兄弟都赶走了,就能在这个山洞里(栈)长待吗,你还是给我滚出来吧你.然后1往外一看,打不过打不过(1比3小),还是老老实实出来吧.然后计算一下自己的雨水柱,栈外黑柱子三的索引位置是7,栈顶的柱子索引是3,二者相距4个单位,减去1得到主子的宽度为3,柱子的高度为2-1=1,因此雨水量就是3了…
    实现:
class Solution {
    public int trap(int[] height) {
        int sum = 0;
        Stack<Integer> stack = new Stack<>();//单调栈
        for (int i = 0; i < height.length; i++){
            while(!stack.empty() && height[i] >= height[stack.peek()]){//出栈条件
                int pop = stack.pop();
                if(!stack.empty()){//计算容量
                    int top = stack.peek();
                    sum += (i - top - 1) * (Math.min(height[top], height[i]) - height[pop]);
                }
            }
            stack.push(i);
        }
        return sum;
    }
}

接雨水的问题是个难题,是因为还有一个更巧妙的办法,但是如果我们能够想到单调栈的解法,也足够了.
双指针法:
双指针法其实是竖着计算的进一步优化.根据竖着算的思路,计算一个柱子上方的雨水量,我们需要知道该柱子左边最高的柱子和右边最高的柱子二者之间的最小值.那么也就是说其实我们最终只需要的是它们两者之间的最小值,如果能想到一个办法,不必将两者都求出来,也即只求出来最小的那个.
这就是双指针的思想,两个指针分别从左和从右开始遍历,谁小谁就先走,这样的话,先找到的那个最值一定是左右两边最值的最小值了.
实现:

class Solution {
    public int trap(int[] height) {
        int sum = 0;
        int left = 0, right = height.length-1, leftMax=-1, rightMax = -1;
        while(left < right){
            if(height[left] < height[right]){
                if(height[left] < leftMax) sum += leftMax - height[left];
                else leftMax = height[left];
                left++;
            }else{
                if(height[right] < rightMax) sum += rightMax - height[right];
                else rightMax = height[right];
                right--;
            }
        }
        
        return sum;
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/shuang-zhi-zhen-fa-xu-yao-dui-jie-fa-2you-shen-ke-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  1. 今天又做了一个题,柱状图中最大的矩形,也是利用的单调栈。
    在这里插入图片描述
    思路:
    遍历每一根柱子,然后找到它左边的边界和右边的边界。边界就是连续不小于它的柱子里离它最远的。例如对于第五个柱子,它的左边界是第三个柱子,右边界是最后一个柱子。
    最简单的实现方法就是按照上面的思路直接找:
class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights.length == 0) return 0;

        int max = 0;
        for(int i = 0; i < heights.length; i++){
            int l = i;
            while((l-1) > -1 && heights[l-1] >= heights[i]) --l;
            int r = i;
            while((r+1) < heights.length && heights[r+1] >= heights[i]) ++r;
            max = Math.max(max, (r-l+1)*heights[i]);
        }
        return max;
    }
}

但是对于找边界这个工作,可以交给我们的单调栈,不过这次是找左边(右边)第一个更小的值,然后间接地找到边界。
以第3个柱子为例,它左边第一个更小的柱子是第2个,左边界是它右边的柱子,即第三个(它自己);右边第一个更小的柱子是第五个,右边界是第四个,如下图所示。
在这里插入图片描述
找到边界以后,面积为: ( r i g h t − l e f t + 1 ) ∗ h e i g h t i (right - left + 1) *height_{i} (rightleft+1)heighti
heighti,right,left分别是当前柱子高度,右边界和左边界,因为我们使用的是单调栈,求出来的实际上不是边界,而是边界往右或左多了一位,因此对上面的公式进行修正: ( r i g h t − l e f t − 1 ) ∗ h e i g h t i (right - left - 1) *height_{i} (rightleft1)heighti
heighti,right,left分别是当前柱子高度,右边第一个更小的值的索引和左边第一个更小的值的索引。
实现:

class Solution {
    public int largestRectangleArea(int[] heights) {
        // if(heights.length == 0) return 0;
        Stack<Integer> stack = new Stack<>();
        stack.push(-1);
        int max = 0;
        for(int i = 0; i < heights.length; i++){
            while(stack.size() > 1 && heights[i] < heights[stack.peek()]){
                int cur = heights[stack.pop()];
                int top = stack.peek();
                max = Math.max(max, (i - top - 1)*cur);
            }
            stack.push(i);
        }
        while(stack.size() > 1){
            int cur = heights[stack.pop()];
            int top = stack.peek();
            max = Math.max(max, (heights.length - top - 1)*cur);
        }
        return max;
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/dan-diao-zeng-zhan-xiang-by-fang-wen-chu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  1. 每日温度
    在这里插入图片描述
    解法: 使用严格单调递减栈存储的是当天的下标而不是温度,每当出栈一个下标时,计算栈外下标和栈底的差值,即为所求。
    实现:
class Solution {
    public int[] dailyTemperatures(int[] T) {
        //典型的单调栈问题
        Stack<Integer> stack = new Stack<>();
        int[] daily = new int[T.length];
        for(int i = 0; i < T.length; i++){
            while(!stack.empty() && T[i] > T[stack.peek()]){
            //出栈时计算天数
                int top = stack.pop();
                daily[top] = i - top;
            }
            //进栈的是下标
            stack.push(i);
        }
        //最后还留在栈中的都是后面没有温度超过它,因此置0.
        while(!stack.empty()){
            daily[stack.pop()] = 0;
        }
        return daily;
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/daily-temperatures/solution/dan-diao-zhan-by-fang-wen-chu-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度: 时间复杂度为O(n),因为只需要遍历一遍数组;空间复杂度为O(n),因为栈最大时是温度呈下降趋势,此时栈大小为n,并且返回值也需要空间。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值