力扣-84-柱状图中最大的矩形

方法一、暴力法

从左到右,枚举(固定)高,从左到右循环枚举每一根柱子的高度,然后以当前柱子的高度为整个矩形的高度。随后从这根柱子开始,往左右两侧延伸,直到遇到高度小于矩形高度的柱子,这样就确定了矩形的左右边界。代码如下:

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;

        if(len == 0){
            return 0;
        }

        if(len == 1){
            return heights[0];
        }

        int ans = -1;
        for(int index = 0; index < len; index++){
            int preheight = heights[index];
            int l = index, r = index;

            while((l-1>=0) && heights[l-1] >= preheight){  //while找到当前柱子的左边界
                l--;
            }

            while((r+1<len) && heights[r+1] >= preheight){  //while找到当前柱子的右边界
                r++;
            }

            ans = Math.max(ans, (r - l + 1) * preheight);  //更新整个矩形面积的最大值
        }   

        return ans;
    }
}

时间复杂度:O($n^{2}$)

 

方法二、单调栈

我们回顾方法一,看看有哪些可以优化的地方,我们发现求解当前柱子的左右边界的方法非常暴力,我们考虑用什么样数据结构进行优化。那么如何求一根柱子的左侧且最近的小于其高度的柱子。我们先看一个结论:

对于两根柱子$j_{0}$以及$j_{1}$,如果$j_{0}<j_{1}$,并且heights[$j_{0}$] ≥heights[$j_{1}$],那么对于任意在他们之后出现的柱子$i$,$j_{0}$一定不会是$i$左侧最近的且小于其高度的柱子,因$j_{1}$会挡住$j_{0}$,确定$i$的左边界。那么我们可以构造一个单调的栈,从小到大存放柱子的index,且还要保证他们的高度也满足递增的关系。这样我们在枚举到第$i$根柱子的时候,就可以先把所有高度大于等heights[i]的值全部移除,栈中剩下的全部小于$i$的高度,那么栈顶的值即为$i$左边最近且高度小于$i$的左边界。同样的方法我们可以确定每个高度的右边界。

这里会有一种特殊情况。如果我们移除了栈中所有的值,那就说明$i$左侧所有柱子的高度都大于$i$,那么我们可以认为$i$左侧且最近的小于其高度的柱子在位置$index = -1$,我们可以理解它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。

代码如下:

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        
        if(len == 0) {
            return 0;
        }
        
        if(len == 1) {
            return heights[0];
        }
        
        Deque<Integer> stack = new LinkedList<Integer>();
        int[] left = new int[len];//保存每个高度对应的左边界
        int[] right = new int[len];    //保存每个高度对应的右边界
        
     //确定每个柱子的左边界:从左往右遍历heights数组 for(int i=0; i<len; i++) { while(!stack.isEmpty() && heights[i] <= heights[stack.peek()]) { stack.pop(); } left[i] = stack.isEmpty() ? -1:stack.peek();  //如果stack为空,说明柱子的左边界为-1 stack.push(i); } stack.clear();
    
     //确定每个柱子的右边界:从右往左遍历heights数组
for(int i = len-1; i>=0; i--) { while(!stack.isEmpty() && heights[i] <= heights[stack.peek()]) { stack.pop(); } right[i] = stack.isEmpty()? len:stack.peek();  //如果stack为空,说明柱子的右边界为n stack.push(i); } int ans = 0; for(int i=0; i<len; i++) { ans = Math.max(ans, (right[i]-left[i]-1)*heights[i]);  //更新矩形的最大高度 } return ans; } }

 

方法三、单调栈优化

看了方法二求解左边界和右边界,你会感觉跟结构很相似,那能不能在一次遍历中,同时求得左边界和右边界呢?

其实是可以的。在方法二中,我们在对位置$i$进行入栈操作时,确定了$i$的左边界。那么那些被弹出栈元素的右边界时,是不是就是$i$呢?仔细想想的确是这样的,那我们就可以同时维护左右边界了(一般元素是先入栈确定左边界,然后在它弹出栈的时候确定了右边界,这两个过程不是在同一次遍历中完成的)。

那这个右边界没有问题吗?仔细想想还是有一点问题的,如果存在相同高度的元素怎么办?相同高度的元素依次弹出栈,却只有最后一个弹出的确定了正确的右边界。但因为这些弹出元素的高度本身是相同的,所以最终并不影响结果。

代码如下:

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        
        if(len == 0) {
            return 0;
        }
        
        if(len == 1) {
            return heights[0];
        }
        
        Deque<Integer> stack = new LinkedList<Integer>();
        int[] left = new int[len];//保存每个高度对应的左边界
        int[] right = new int[len];    //保存每个高度对应的右边界
        
        //在遍历结束后,栈中仍然非空,这说明这些元素之后没有高度比他们小的元素,故他们对应的右边界为len,我们可以初始化一下
        Arrays.fill(right, len);
        
        for(int i=0; i<len; i++) {
            while(!stack.isEmpty() && heights[i] <= heights[stack.peek()]) {
                right[stack.peek()] = i;
                stack.pop();
            }
            
            left[i] = stack.isEmpty() ? -1:stack.peek();
            stack.push(i);
        }
    
        int ans = 0;
        for(int i=0; i<len; i++) {
            ans = Math.max(ans, (right[i]-left[i]-1)*heights[i]);
        }
        return ans;
    }
}

 


 

posted @ 2020-11-05 12:06  Peterxiazhen  阅读(168)  评论(0编辑  收藏  举报