单调栈问题汇总

单调栈(Monotone Stack)

栈的应用中有一类问题称为单调栈(Monotone Stack)问题,可以巧妙的将某些问题的时间复杂度降到「O(n)级别」。那么什么是单调栈呢?

所谓单调栈,就是保持栈中的元素是单调的。假设把数组 [2 1 4 6 5]依次入栈,并保持栈的单调递增性,如下:

  1. 元素2入栈,此时栈中元素为[2]
  2. 元素1入栈,由于此时1小于栈顶元素2,把1入栈的话就不满足单调递增性了,于是先把栈顶元素2弹出,再让元素1入栈,此时栈中元素为[1]
  3. 元素4入栈,由于此时4大于栈顶元素1,可以满足递增性,故入栈,此时栈中元素为[1,4]
  4. 元素6入栈,同上,此时栈中元素为[1,4,6]
  5. 元素5入栈,同第2步,在入栈前先把栈顶元素6弹出,故此时栈中元素为[1,4,5]

由于栈中元素(从栈底至栈顶)保持单调递增性,因此,有这样一个性质:

假设当前元素为a,栈顶元素(若栈非空)就是元素a左侧第一个小于a的元素

同样的,如果维护一个单调递减栈,那么就有:

假设当前元素为a,栈顶元素(若栈非空)就是元素a左侧第一个大于a的元素

下面就来看一下单调栈的应用。

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

img

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

分析:本题是单调栈的典型应用。首先,我们考虑该选取单调递减栈还是单调递增栈?

由于要接到雨水,显然,必须要形成“凹”的形状,由此可以确定,应当是选取递减栈——按递减的序列把高度存进去(递减指的是从栈底至栈顶递减),一旦发现当前的高度大于栈顶元素了,说明形成了凹槽。但是计算凹槽的面积,除了高度,还需要知道宽度,因此,我们应该在栈中存放下标而非直接存放高度。

class Solution {    
    public int trap(int[] height) {
        int n = height.length;
        int total = 0; // 能接到的雨水总量
        Stack<Integer> s = new Stack<>(); // 存放数组下标,而非数组元素
        int i = 0;
        while(i < n) {
            // 维护一个单调递减栈
            if(s.isEmpty() || height[i] < height[s.peek()]) {
                s.push(i);
                i++;
            }else {
                int bottom = s.pop();
                if(s.isEmpty()) continue; // 关键
                int w = i - s.peek() - 1;
                int h = Math.min(height[s.peek()], height[i]) - height[bottom];
                total += w * h;
            }
        }
        return total;
    }
}

84. 柱状图中最大的矩形

方法1:暴力法O(n^2)

算法思路:
从最基础的思路出发,已知矩形面积的计算公式为:高度 × 宽度。就本题而言,每个小矩形的高度是确定,我们可以固定一个小矩形i,以heights[i]为高度,以位置 i 为中心向左右两侧扩散,使得扩展到的柱子的高度均不小于 h,直到到达边界、或者不能再向外延伸了。
换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。

这种做法的时间复杂度是O(n^2),因为遍历数组需要O(n),固定位置i向两侧延伸时最大也需要O(n),即两层for循环。

class Solution {
    /*
    暴力解法
    时间复杂度:O(n^2)
    空间复杂度:O(1)
    */
    public int largestRectangleArea(int[] heights) {
        int maxArea = 0;
        for(int i = 0; i < heights.length; i++) {
            int h = heights[i];
            int left = i, right = i;
            while(left >= 0 && heights[left] >= h) left--;
            while(right < heights.length && heights[right] >= h) right++;
            int w = right - left - 1;
            maxArea = Math.max(maxArea, w * h);
        }
        return maxArea;
    }
}

方法2:单调栈O(n)

class Solution {
    public int largestRectangleArea(int[] heights) {
        // 预处理:添加哨兵
        int n = heights.length;
        int[] temp = new int[n + 1];
        for(int i = 0; i < n; i++) {
            temp[i] = heights[i];
        }
        temp[n] = 0;// 哨兵
        heights = temp;
        // 正式处理
        Stack<Integer> s = new Stack<>();
        int maxArea = 0, i = 0;
        while(i < heights.length) {
            if(s.isEmpty() || heights[i] >= heights[s.peek()]) {
                s.push(i);
                i++;
            }else{
                int t = s.pop();
                if(s.isEmpty()) {
                    maxArea = Math.max(maxArea, i * heights[t]);
                }else {
                    maxArea = Math.max(maxArea, (i - s.peek() - 1) * heights[t]);
                }
            }
        }
        return maxArea;
    }
}

单调栈:不使用Stack,使用Deque

使用双端队列来模拟栈,速度更快一些。因为在Java中,Stack其实不推荐使用的。

class Solution {
    public int largestRectangleArea(int[] heights) {
        // 预处理:添加哨兵
        int n = heights.length;
        int[] temp = new int[n + 1];
        for(int i = 0; i < n; i++) {
            temp[i] = heights[i];
        }
        temp[n] = 0;// 哨兵
        heights = temp;
        // 正式处理
        Deque<Integer> s = new LinkedList<>();
        int maxArea = 0, i = 0;
        while(i < heights.length) {
            if(s.isEmpty() || heights[i] >= heights[s.peekLast()]) {
                s.add(i);
                i++;
            }else{
                int t = s.pollLast();
                if(s.isEmpty()) {
                    maxArea = Math.max(maxArea, i * heights[t]);
                }else {
                    maxArea = Math.max(maxArea, (i - s.peekLast() - 1) * heights[t]);
                }
            }
        }
        return maxArea;
    }
}

方法3:暴力优化(本题最优解)

class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights.length == 0) return 0;

        int maxArea = 0;
        int n = heights.length;
        /*
        left[i]  表示位置i左侧第一个小于heights[i]的位置
        right[i] 表示位置i右侧第一个小于heights[i]的位置
        */
        int[] left = new int[n];
        int[] right = new int[n];
        left[0] = -1;
        for(int i = 1; i < n; i++) {
            int k = i-1;
            while(k >= 0 && heights[k] >= heights[i]) {
                // k--;
                k = left[k];
            }
            left[i] = k;
        }

        right[n-1] = n;
        for(int i = n-2; i >= 0; i--) {
            int k = i+1;
            while(k < n && heights[k] >= heights[i]) {
                // k++;
                k = right[k];
            }
            right[i] = k;
        }

        /*
        计算面积
        对于高度为heights[i]的柱子,以其为中心可以形成的最大矩形面积等于
        heights[i] × (right[i] - left[i] - 1)
        */
        for(int i = 0; i < n; i++) {
            int currArea = heights[i] * (right[i] - left[i] - 1);
            maxArea = Math.max(maxArea, currArea);
        }
        return maxArea;
    }
}
posted @ 2020-07-12 11:23  kkbill  阅读(628)  评论(0编辑  收藏  举报