力扣-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; } }
作者:Ryanjie
出处:http://www.cnblogs.com/ryanjan/
本文版权归作者和博客园所有,欢迎转载。转载请在留言板处留言给我,且在文章标明原文链接,谢谢!
如果您觉得本篇博文对您有所收获,觉得我还算用心,请点击右下角的 [推荐],谢谢!