一、问题描述:
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
二、解题思路:
遍历数组,以每个 i 为中心,向左找第一个小于 heights[i] 的位置 left;向右找第一个小于 heights[i] 的位置 right,以 i 为中心的最大面积为area[i] = heights[i] * (right - left - 1)。
为什么到左边第一个小于 heights[i] 的 left 就停止了?
如果把 left 加入矩形,因为heights[left] < heights[i],所以矩形就变成了以 left 为中心,面积就变为area[left] ,这种情况我们已经在前面的遍历中计算过,同理,右侧找第一个小于的位置right[i]也是一样的,都是避免了重复计算。
并不是说再向两侧扩展不可能找到更大的面积,还是有可能的!!!
已知暴力方法的时间复杂度为 O(n2)。
三、代码示例:
思路1:用两个数组来记录左右第一个小于 heights[i] 的位置。
首先,代码创建了两个数组 left 和 right,分别用于存储每个柱子向左和向右第一个小于自身高度的柱子的索引。
接着,通过两次遍历 heights 数组,分别计算出 left 和 right 数组的值。
最后,再次遍历 heights 数组,计算以每个柱子为高度的矩形的面积,并更新最大面积。
class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) return 0;
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
left[0] = -1;
right[n - 1] = n;
int res = 0;
for (int i = 1; i < n; i++) {
int tmp = i - 1;
while (tmp >= 0 && heights[tmp] >= heights[i]) tmp = left[tmp];
left[i] = tmp;
}
for (int i = n - 2; i >= 0; i--) {
int tmp = i + 1;
while (tmp < n && heights[tmp] >= heights[i]) tmp = right[tmp];
right[i] = tmp;
}
for (int i = 0; i < n; i++) res = Math.max(res, (right[i] - left[i] - 1) * heights[i]);
return res;
}
}
- 时间复杂度分析:第一个遍历计算 left 数组的时间复杂度为 O(n),第二个遍历计算 right 数组的时间复杂度也为 O(n)。第三个遍历计算矩形面积的时间复杂度同样为 O(n)。
因此,总的时间复杂度为 O(n) + O(n) + O(n) = O(n)。
思路2:用单调栈法保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
①构造新的直方图数组:在原始直方图的头部和尾部各添加一个高度为0的柱子,这样可以处理直方图两侧边界的情况。
②使用单调栈:创建一个单调递增栈,栈中存放直方图的索引。遍历新的直方图数组,依次处理每个柱子。
③维护单调栈:在遍历过程中,如果当前柱子的高度小于栈顶柱子的高度,说明找到了右边界,可以计算以栈顶柱子为高度的最大矩形面积。
④计算最大矩形面积时,栈顶柱子的右边界为当前遍历到的柱子索引,左边界为栈中次顶元素的索引。
⑤更新最大矩形面积:计算出当前矩形的宽度和高度后,更新最大矩形面积。
⑥重复步骤3-步骤5,直到遍历完所有柱子。
⑦返回最大矩形面积。
class Solution {
public int largestRectangleArea(int[] heights) {
int result = 0; // 初始化最大矩形面积为0
Stack<Integer> s = new Stack<>(); // 创建一个栈用于存放直方图的索引
int[] newheights = new int[heights.length + 2]; // 新的直方图数组,首尾增加0,方便处理边界情况
for(int i = 0; i < heights.length; i++) newheights[i+1] = heights[i];
newheights[0] = 0; // 在新的直方图数组头部添加一个0
newheights[heights.length + 1] = 0; // 在新的直方图数组尾部添加一个0
s.push(0); // 将0索引入栈,作为起始索引
for(int i = 1; i < newheights.length; i++){ // 遍历新的直方图数组
while(newheights[i] < newheights[s.peek()]){ // 如果当前高度小于栈顶索引对应的高度,右边第一个比栈顶元素小的
int mid = s.pop(); // 弹出栈顶索引
int w = i - s.peek() - 1; // 计算矩形的宽度
int h = newheights[mid]; // 获取弹出索引对应的高度
result = Math.max(result, w * h); // 更新最大矩形面积
System.out.println(w + " " + h);
}
s.push(i); // 将当前索引入栈
}
return result; // 返回最大矩形面积
}
}
- 时间复杂度分析:O(n),其中n为直方图中柱子的数量。在循环中,每个索引最多入栈和出栈一次,因此时间复杂度为O(n)。