单调栈学习与应用

单调栈学习与应用

栈的应用,单调栈就是保持栈内元素有序,需要我们自己维持顺序,从栈顶到栈底是从小到大和从大到小两种情况。

  • 若要找第一个小于的,则应该从大到小
  • 若要找第一个大于的,应该从小到大

【LeetCode】84. 柱状图中最大的矩形

给定 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

思路

对于每一个柱子,想要合并成更大的矩形,只有两侧的柱子的高度大于等于当前这个柱子才能够合并,对于暴力解法来说,只要找到每一个柱子左侧第一个小于它高度的柱子,右侧第一个小于它高度的柱子,就能求出每一个柱子的合并最大矩形,遍历每个柱子就可以得到最大值!

若使用单调栈:

  1. 当前柱子高度大于栈顶柱子的高度

    • 入栈
  2. 当前柱子高度等于栈顶柱子的高度

    ​ 1和2代表两种不同的思路:

    1. 弹出栈顶柱子,入栈当前柱子(代表对于相同高度的柱子不重复计算)
    2. 直接入栈(代表遍历每一个柱子的合并面积,取最大值)
  3. 当前柱子高度小于栈顶柱子的高度 矩形面积为 height * width

    1. 弹出栈顶柱子,以当前栈顶柱子高度进行合并 下标为left
    2. 不弹出新栈顶柱子,以新栈顶柱子为左侧第一个小于要合并的高度的柱子 下标为cur
    3. 以当前遍历的柱子为右侧第一个小于要合并的高度的柱子 下标为right
    4. width = right - left - 1
    5. 计算面积并选取面积最大值,如果当前遍历柱子高度仍小于新栈顶柱子高度,则继续重复1-5步骤
    6. 将当前遍历的柱子入栈

因为单调栈是有序的,当找到第一个小于栈顶的,则栈顶的下一个柱子一定是小于栈顶的(如果是相等的也看作是小于的),所以就可以用栈顶柱子,栈顶下一个柱子,当前遍历柱子计算出合并面积!

例子

heights = [2,1,5,6,2,3]
添加哨兵后heights = [0,2,1,5,6,2,3,0]
heights[0]=0,入栈s=[0]
heights[1]=2,入栈s=[0,1]
heights[2]=1 < 2,pop出栈顶元素1,s=[0],可以确定heights[1]=2所能勾勒出的最大矩形面积,左边界为新栈顶元素0,右边界就是当前下标i=2,宽为2-0-1=1,高度为heights[1]=2,ans=max(0,2)=2。1>heights[0]=0,所以不继续pop,入栈s=[0,2]
heights[3]=5,入栈s=[0,2,3]
heights[4]=6,入栈s=[0,2,3,4] 
heights[5]=2 < heights[4]=6,pop出栈顶元素4,s=[0,2,3],可以确定heights[4]=6所能勾勒出的最大矩形面积,左边界为新栈顶元素3,右边界就是当前下标i=5,宽为5-3-1=1,高度为heights[4]=6,ans=max(2,6)=6
             < heights[3]=5,pop出栈顶元素3,s=[0,2],可以确定heights[3]=5所能勾勒出的最大矩形面积,左边界为新栈顶元素2,右边界就是当前下标i=5,宽为5-2-1=2,高度为heights[3]=5,ans=max(6,10)=10。2>heights[2]=1,所以不继续pop,入栈s=[0,2,5]
heights[6]=3,入栈s=[0,2,5,6]
heights[7]=0 < heights[6]=3,pop出栈顶元素6,s=[0,2,5],可以确定heights[6]=3所能勾勒出的最大矩形面积,左边界为新栈顶元素5,右边界就是当前下标i=7,宽为7-5-1=1,高度为heights[6]=3,ans=max(10,3)=10
             < heights[5]=2,pop出栈顶元素5,s=[0,2],可以确定heights[5]=2所能勾勒出的最大矩形面积,左边界为新栈顶元素2,右边界就是当前下标i=7,宽为7-2-1=4,高度为heights[5]=2,ans=max(10,8)=10
             < heights[2]=1,pop出栈顶元素2,s=[0],可以确定heights[2]=1所能勾勒出的最大矩形面积,左边界为新栈顶元素0,右边界就是当前下标i=7,宽为7-0-1=6,高度为heights[2]=1,ans=max(10,6)=10。不继续pop,入栈s=[0,7]
遍历结束,ans=10

tips

若所有柱子全部都是单调递增的,则找不到左右侧第一个小于的,则无法计算合并面积,怎么办?

例如:5 5 6

在开头和末尾加一个高度为0的哨兵即可,这样保证了栈一定不为空,同时,能保证一定能计算所有柱子的合并面积!

前后加0后:0 5 5 6 0,保证每个柱子都找得到比它小的

代码

弹出栈顶柱子,入栈当前柱子(代表对于相同高度的柱子不重复计算)

class Solution {
    public int largestRectangleArea(int[] heights) {
        int[] newHeights = new int[heights.length + 2];
        for(int i = 0; i < heights.length; i++) {
            newHeights[i + 1] = heights[i];
        }
        heights = newHeights;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int res = 0;
        for(int i = 1; i < heights.length; i++) {
            if(heights[i] > heights[stack.peek()]) {
                stack.push(i);
            }else if(heights[i] == heights[stack.peek()]){
                stack.pop();
                stack.push(i);
            }else {
                while(heights[i] < heights[stack.peek()]) {
                    int cur = stack.pop();
                    int left = stack.peek();
                    int right = i;
                    int height = heights[cur];
                    int width = right -left - 1;
                    res = Math.max(res, height * width);
                }
                stack.push(i);
            }
        }
        return res;
    }
}

直接入栈(代表遍历每一个柱子的合并面积,取最大值)

class Solution {
    public int largestRectangleArea(int[] heights) {
        int[] newHeights = new int[heights.length + 2];
        for(int i = 0; i < heights.length; i++) {
            newHeights[i + 1] = heights[i];
        }
        heights = newHeights;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int res = 0;
        for(int i = 1; i < heights.length; i++) {
            if(heights[i] > heights[stack.peek()]) {
                stack.push(i);
            }else if(heights[i] == heights[stack.peek()]){
                stack.push(i);
            }else {
                while(heights[i] < heights[stack.peek()]) {
                    int cur = stack.pop();
                    int left = stack.peek();
                    int right = i;
                    int height = heights[cur];
                    int width = right -left - 1;
                    res = Math.max(res, height * width);
                }
                stack.push(i);
            }
        }
        return res;
    }
}

化简:

class Solution {
    public int largestRectangleArea(int[] heights) {
        int[] newHeights = new int[heights.length + 2];
        for(int i = 0; i < heights.length; i++) {
            newHeights[i + 1] = heights[i];
        }
        heights = newHeights;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int res = 0;
        for(int i = 1; i < heights.length; i++) {
            while(heights[i] < heights[stack.peek()]) {
                int cur = stack.pop();
                int left = stack.peek();
                int right = i;
                int height = heights[cur];
                int width = right -left - 1;
                res = Math.max(res, height * width);
            }
            stack.push(i);
        }
        return res;
    }
}

【LeetCode】42. 接雨水

给定 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

思路

使用单调栈,就是出现有坑的时候就能存储水,怎样判断出现坑?当出现第一个大于栈顶柱子高度的柱子时,出现坑。那么单调栈的顺序从栈顶到栈底就应该是从小到大。单调栈的方法是以行计算雨水量。

在这里插入图片描述

  1. 当前柱子高度小于栈顶柱子的高度

    • 入栈
  2. 当前柱子高度等于栈顶柱子的高度

    ​ 1和2代表两种不同的思路:

    1. 直接入栈(代表对于相同高度的柱子分别成坑计算,比如有两个相同高度的柱子相邻和下图的情况)

      在这里插入图片描述

      1.当①和②中间的高度为0的柱子被弹出后②作为栈顶,①为左侧大于②的柱子(也就是栈顶下一个柱子),②右侧的柱子为右侧大于②的柱子(也就是当前遍历的柱子),形成了坑,其接的雨水为1个宽度

      2.当②弹出后,①作为栈顶,①左侧柱子作为左侧大于①的柱子(也就是栈顶下一个柱子),②右侧的柱子为右侧大于①的柱子(也就是当前遍历的柱子),形成了坑,其接的雨水是两个宽度

    2. 弹出栈顶柱子,入栈当前柱子(直接计算有相同高度柱子的承接雨水最大值,相当于一步到位)

  3. 当前柱子高度大于栈顶柱子的高度 雨水面积为 h * width

    1. 弹出栈顶柱子,以当前栈顶柱子高度进行合并 下标为left
    2. 不弹出新栈顶柱子,以新栈顶柱子为左侧第一个小于要合并的高度的柱子 下标为cur
    3. 以当前遍历的柱子为右侧第一个小于要合并的高度的柱子 下标为right
    4. width = right - left - 1
    5. h = Math.min(height[stack.peek()], height[right]) - height[cur];
    6. 计算雨水并累加,如果当前遍历柱子高度仍大于新栈顶柱子高度,则继续重复1-5步骤
    7. 将当前遍历的柱子入栈

代码

直接入栈

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int sum = 0;
        for(int i = 1; i < len; i++) {
            if(height[i] < height[stack.peek()]) {
                stack.push(i);
            }else if(height[i] == height[stack.peek()]) {
                stack.push(i);
            }else {
                while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
                    int cur = stack.pop();
                    if(!stack.isEmpty()) {
                        int right = i;
                        int left = stack.peek();
                        int h = Math.min(height[left], height[right]) - height[cur];
                        int width = right - left - 1;
                        sum += h * width;
                    }
                }
                stack.push(i);
            }
        }
        return sum;
    }
}

简化:

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int sum = 0;
        for(int i = 1; i < len; i++) {
            while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int cur = stack.pop();
                if(!stack.isEmpty()) {
                    int right = i;
                    int left = stack.peek();
                    int h = Math.min(height[left], height[right]) - height[cur];
                    int width = right - left - 1;
                    sum += h * width;
                }
            }
            stack.push(i);
        }
        return sum;
    }
}

弹出栈顶柱子,入栈当前柱子

class Solution {
    public int trap(int[] height) {
        int len = height.length;
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        int sum = 0;
        for(int i = 1; i < len; i++) {
            if(height[i] < height[stack.peek()]) {
                stack.push(i);
            }else if(height[i] == height[stack.peek()]) {
                stack.pop();
                stack.push(i);
            }else {
                while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
                    int cur = stack.pop();
                    if(!stack.isEmpty()) {
                        int right = i;
                        int left = stack.peek();
                        int h = Math.min(height[left], height[right]) - height[cur];
                        int width = right - left - 1;
                        sum += h * width;
                    }
                }
                stack.push(i);
            }
        }
        return sum;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值