题目
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路
一、双指针法(LeetCode超时)
class Solution {
public int largestRectangleArea(int[] heights){
int sum = 0;
for(int i = 0; i < heights.length; i++){
int left = i;
int right = i;
//其实这里双指针的思想已经有单调栈的影子了,核心思想都是两边遍历,找到第一个比自己小的元素就停下
for(;left >= 0; left--){
if(heights[left] < heights[i]) break;
}
for(;right < heights.length; right++){
if(heights[right] < heights[i]) break;
}
int w = right - left - 1;
int h = heights[i];
sum = Math.max(sum, w * h);
}
return sum;
}
}
二、动态规划(了解即可)
本题的动态规划,是要记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。
class Solution {
public int largestRectangleArea(int[] heights) {
int length = heights.length;
int[] minLeftIndex = new int [length];
int[] maxRigthIndex = new int [length];
// 记录左边第一个小于该柱子的下标
minLeftIndex[0] = -1 ;
for (int i = 1; i < length; i++) {
int t = i - 1;
// 这里不是用if,而是不断向右寻找的过程
while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
minLeftIndex[i] = t;
}
// 记录每个柱子 右边第一个小于该柱子的下标
maxRigthIndex[length - 1] = length;
for (int i = length - 2; i >= 0; i--) {
int t = i + 1;
while(t < length && heights[t] >= heights[i]) t = maxRigthIndex[t];
maxRigthIndex[i] = t;
}
// 求和
int result = 0;
for (int i = 0; i < length; i++) {
int sum = heights[i] * (maxRigthIndex[i] - minLeftIndex[i] - 1);
result = Math.max(sum, result);
}
return result;
}
}
三、单调栈(重点)
接雨水是找每个柱子左右两边第一个大于该柱子高度的柱子,然后再取两者最小值和中间柱子高度做差值,而本题是找每个柱子左右两边第一个小于该柱子的柱子
因为是找第一个比自己小的元素,所以维护单增栈,当前元素大于栈顶元素的时候直接入栈,当前元素小于栈顶元素才进行操作(为了维护单调递增栈)
和接雨水类似,一共栈顶和栈顶的下一个元素以及要入栈的三个元素组成题目要求最大面积的高度和宽度
java代码如下:
class Solution {
public int largestRectangleArea(int[] height){
Stack<Integer> stack = new Stack<Integer>();
//数组扩容,头尾各加一个元素,哨兵模式
int[] newHeight = new int[height.length + 2];
newHeight[0] = 0;
newHeight[newHeight.length -1] = 0;
for(int i = 0; i < height.length; i++){
newHeight[i + 1] = height[i];
}
height = newHeight;//覆盖旧数组
stack.push(0);//第一个元素的下标入栈,然后从下标为1的柱子开始遍历
int result = 0;
for(int i = 1; i < height.length; 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 mid = stack.peek();
stack.pop();
int left = stack.peek();
int right = i;
int w = right - left - 1;
int h = height[mid];
result = Math.max(result, h * w);
}
stack.push(i);
}
}
return result;
}
}
上面是很详细的代码,注释也很清楚,每个变量都表示清晰,每一步都分开表示,下面给出优化后的简洁版本
class Solution {
public int largestRectangleArea(int[] height){
Stack<Integer> stack = new Stack<Integer>();
//数组扩容,头尾各加一个元素,哨兵模式
int[] newHeight = new int[height.length + 2];
newHeight[0] = 0; //左侧的哨兵使得不用判空
newHeight[newHeight.length -1] = 0;//右侧的哨兵能够保证在遍历结束时弹出前面的柱子计算面积
for(int i = 0; i < height.length; i++){
newHeight[i + 1] = height[i];
}
height = newHeight;
stack.push(0);
int result = 0;
for(int i = 1; i < height.length; i++){
if(height[i] >= height[stack.peek()]){//这里小于等于可以合并
stack.push(i);
} else {
while(!stack.isEmpty() && height[i] < height[stack.peek()]){
int mid = stack.pop();//这里取栈顶元素和出栈顶元素可以做一步完成
int left = stack.peek();
result = Math.max(result, height[mid] * (i - left - 1));//不使用中间变量名,直接原变量参与运算
}
stack.push(i);
}
}
return result;
}
}