84. 柱状图中最大的矩形
思路一:暴力法
枚举每个柱子为高度的最大矩形的面积
实现:对每个高度的柱子向两边扩张,试图寻找以当前高度为矩形的最大宽度
1 class Solution { 2 public int largestRectangleArea(int[] heights) { 3 4 // 对每个高度的柱子向两边扩张,试图寻找以当前高度为矩形的最大宽度 5 int maxArea = 0; 6 int len = heights.length; 7 for(int i = 0; i < len; i++){ 8 int leftBorder = i-1; 9 for(; leftBorder >= 0 && heights[leftBorder] >= heights[i]; leftBorder--); 10 int rightBorder = i+1; 11 for(; rightBorder < len && heights[rightBorder] >= heights[i]; rightBorder++); 12 maxArea = Math.max(maxArea, heights[i] * (rightBorder - leftBorder - 1)); 13 } 14 return maxArea; 15 } 16 }
leetcode 执行用时:938 ms > 19.41%, 内存消耗:40.2 MB > 40.10%
复杂度分析:
时间复杂度:O(n2)。双层for循环,所以时间复杂度为O(n2)。
空间复杂度:O(1)。
思路二:递减栈
这个思路其实和思路一是一样的,同样是对每个高度的柱子,找到对应的最大宽度,得出对应的面积。但是这次计算宽度借助了递减栈,而非直接遍历数组得到。具体过程如下:
递减栈介绍:递减栈,从栈顶到栈底的序列是一个降序序列
如果当前高度大于栈顶下标对应的高度,直接入栈,否则说明栈顶下标对应的柱子高度找到了右边界,那么出栈栈顶下标,当前下标是该柱子的右边界,左边界需要判断栈是否为空,如果不为空则左边界为栈顶下标,因为栈是递减的,所以栈顶下标对应的柱子高度肯定小于等于上一个出栈的下标对应的柱子高度,所以把栈顶下标作为左边界,如果栈为空,则左边界为-1。根据左右边界可以计算出宽度,更新最大面积。循环出栈,直至当前高度大于等于栈顶高度或者栈为空。随后将当前柱子高度下标入栈。
一次遍历之后,出栈栈中剩余元素,栈中下标对应的高度的右边界都是数组长度。
出栈过程中如果当前出栈的元素等于栈顶下标对应的元素,持续出栈,直到栈顶下标元素小于当前元素的下标为止,因为高度相等的话只需要统计最左边的柱子和他的宽度即可,中间的没必要统计,因为都一样。此时右边界都是数组长度, 如果栈为空,左边界为 - 1, 否则左边界为栈顶下标
1 class Solution { 2 public int largestRectangleArea(int[] heights) { 3 4 Stack<Integer> stack = new Stack<>(); 5 int len = heights.length; 6 int maxArea = 0; 7 for(int i = 0; i < len; i++){ 8 // 如果当前高度大于栈顶下标对应的高度,直接入栈,否则循环出栈,直至当前高度大于栈顶高度或者栈为空 9 while(!stack.isEmpty() && heights[i] < heights[stack.peek()]){ 10 int index = stack.pop(); 11 int width = 0; 12 // 计算宽度,如果栈为空,左边界为0,右边界为i, 否则左边界为栈顶下标,右边界为数组长度 13 if(stack.isEmpty()){ 14 width = i; 15 }else{ 16 width = i - stack.peek() - 1; 17 } 18 maxArea = Math.max(maxArea, width * heights[index]); 19 } 20 stack.push(i); 21 } 22 // 一次遍历之后,出栈栈中剩余元素, 23 while(!stack.isEmpty()){ 24 int index = stack.pop(); 25 // 出栈过程中如果当前出栈的元素等于栈顶下标对应的元素, 26 // 持续出栈,直到栈顶下标元素小于当前元素的下标为止 27 while(!stack.isEmpty() && heights[stack.peek()] == heights[index]){ 28 stack.pop(); 29 } 30 int width = 0; 31 // 如果栈为空,左边界为0, 否则左边界为栈顶下标,右边界为数组长度 32 if(stack.isEmpty()){ 33 width = len; 34 }else{ 35 width = len - stack.peek() - 1; 36 } 37 maxArea = Math.max(maxArea, width * heights[index]); 38 } 39 return maxArea; 40 } 41 }
leetcode 执行用时:12 ms > 71.41%, 内存消耗:39.9 MB > 63.66%
复杂度分析:
时间复杂度:O(n)。每个元素下标最多只被入栈一次以及出栈一次,所以 时间复杂度为O(n)。
空间复杂度:O(n)。空间花费取决于栈的大小,栈的大小最大为O(n-1), 即当heights[]数组递增排列时,所有元素的下标都会入队,所以空间复杂度为O(n)。
思路三:哨兵 + 单调栈
思路虽然可以实现题目要求,但是有点繁琐冗余,可以尝试把原数组拷贝到一个大小为 len + 2的新数组中,在数组的首尾加上高度为0的哨兵,这样可以减少每次求左边界时判断栈是否为空,因为栈底是高度为0的柱子的下标,没有任何柱子的高度比它矮,所以它永远不可能出栈,另外可以减少第一轮迭代结束后对栈的再次迭代,因为最后一个柱子的高度为0, 比栈中任何柱子高度都矮,所以会在最后一次循环中把栈中所有有效柱子下标都出栈。1 class Solution { 2 public int largestRectangleArea(int[] heights) { 3 4 // 哨兵加单调栈 5 if(heights.length == 0){ 6 return 0; 7 } 8 9 // 将数组拷贝到一个新的数组中,在数组的首尾加上高度为0的哨兵 10 int len = heights.length; 11 int[] newHeights = new int[len + 2]; 12 newHeights[0] = 0; 13 System.arraycopy(heights, 0, newHeights, 1, len); 14 newHeights[len+1] = 0; 15 int maxArea = 0; 16 len += 2; 17 18 // 把哨兵入栈 19 Stack<Integer> stack = new Stack<>(); 20 stack.push(0); 21 for(int i = 1; i < len; i++){ 22 // 如果当前高度大于栈顶下标对应的高度,直接入栈,否则循环出栈,直至当前高度大于栈顶高度或者栈为空 23 while(!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]){ 24 int index = stack.pop(); 25 int width = 0; 26 width = i - stack.peek() - 1; // 栈不可能为空,因为栈底始终有一个高度为0的下标作为哨兵 27 maxArea = Math.max(maxArea, width * newHeights[index]); 28 } 29 stack.push(i); 30 } 31 return maxArea; 32 } 33 }
leetcode 执行用时:12 ms > 71.40%, 内存消耗:39.7 MB > 82.67%
复杂度分析:
时间复杂度:O(n)。只遍历了一次新数组,对每个高度的柱子下标最多入栈和出栈一次,所以时间花费为O(n)。另外还需要O(n)的时间复杂度来将原数组拷贝到新数组,所以时间复杂度为O(n)。
空间复杂度:O(n)。需要一个大小为 (len + 2) 新数组,所以空间复杂度为O(n)。