84. 柱状图中最大的矩形
问题描述
给定 n
个非负整数,表示柱状图中各柱子的高度。每个柱子宽度为 1,求在柱状图中能勾勒出的最大矩形面积。
示例:
输入: [2,1,5,6,2,3]
输出: 10
解释: 最大矩形如图所示(阴影部分,面积为 10)
算法思路
单调栈
:
- 核心思想:对于每个柱子,找到其左右两侧
第一个低于它
的柱子,这两个柱子之间的宽度乘以当前柱子的高度即为以当前柱子为高度的最大矩形面积。 单调栈维护
:- 栈中存储柱子下标,对应高度保持
单调递增
- 当遇到高度小于栈顶的柱子时,说明找到了栈顶柱子的右边界,此时可以计算栈顶柱子的矩形面积
- 栈中存储柱子下标,对应高度保持
- 边界处理:
- 在柱状图两端添加高度为 0 的虚拟柱子(左侧 0 避免空栈判断,右侧 0 确保所有柱子出栈计算)
代码实现
class Solution {
public int largestRectangleArea(int[] heights) {
// 处理空数组
if (heights == null || heights.length == 0) {
return 0;
}
int n = heights.length;
// 创建新数组:头尾添加0,中间为原数组
int[] newHeights = new int[n + 2];
newHeights[0] = 0;
System.arraycopy(heights, 0, newHeights, 1, n);
newHeights[n + 1] = 0;
Deque<Integer> stack = new ArrayDeque<>(); // 存储下标
stack.push(0); // 压入第一个虚拟柱子的下标
int maxArea = 0;
// 遍历新数组(从下标1到n+1)
for (int i = 1; i < newHeights.length; i++) {
// 当前高度 < 栈顶高度时,计算栈顶柱子的矩形面积
while (newHeights[i] < newHeights[stack.peek()]) {
int h = newHeights[stack.pop()]; // 弹出栈顶高度
int w = i - stack.peek() - 1; // 计算宽度
maxArea = Math.max(maxArea, h * w);
}
stack.push(i); // 当前索引入栈
}
return maxArea;
}
}
算法分析
- 时间复杂度:O(n)
每个柱子入栈、出栈各一次,共 2n 次操作。 - 空间复杂度:O(n)
额外空间:新数组 O(n),栈最大深度 O(n)。
算法过程
输入 [2,1,5,6,2,3]
-
初始化:
- 新数组:
[0,2,1,5,6,2,3,0]
- 栈:
[0]
(存储下标,对应高度newHeights[0]=0
)
- 新数组:
-
遍历过程:
i=1
:高度 2 > 栈顶高度 0 → 下标 1 入栈(栈:[0,1]
)i=2
:高度 1 < 栈顶高度 2 → 计算:- 弹出栈顶 1(高度 2),宽度 = 2 - 0 - 1 = 1 → 面积 = 2×1 = 2
- 栈顶高度 0 < 当前高度 1 → 下标 2 入栈(栈:
[0,2]
)
i=3
:高度 5 > 栈顶高度 1 → 下标 3 入栈(栈:[0,2,3]
)i=4
:高度 6 > 栈顶高度 5 → 下标 4 入栈(栈:[0,2,3,4]
)i=5
:高度 2 < 栈顶高度 6 → 计算:- 弹出栈顶 4(高度 6),宽度 = 5 - 3 - 1 = 1 → 面积 = 6×1 = 6
- 栈顶高度 5 > 当前高度 2 → 继续弹出:
- 弹出栈顶 3(高度 5),宽度 = 5 - 2 - 1 = 2 → 面积 = 5×2 = 10(最大面积更新为 10)
- 栈顶高度 1 < 当前高度 2 → 下标 5 入栈(栈:
[0,2,5]
)
i=6
:高度 3 > 栈顶高度 2 → 下标 6 入栈(栈:[0,2,5,6]
)i=7
:高度 0 < 栈顶高度 3 → 计算:- 弹出栈顶 6(高度 3),宽度 = 7 - 5 - 1 = 1 → 面积 = 3
- 弹出栈顶 5(高度 2),宽度 = 7 - 2 - 1 = 4 → 面积 = 8
- 弹出栈顶 2(高度 1),宽度 = 7 - 0 - 1 = 6 → 面积 = 6
- 栈顶高度 0 == 当前高度 0 → 下标 7 入栈(栈:
[0,7]
)
-
返回结果:最大面积 10
关键点
单调递增栈
:确保栈中下标对应的高度严格递增。- 左右边界确定:
右边界
:当前遍历位置i
(第一个小于栈顶高度的位置)左边界
:弹出后栈顶元素
(第一个小于原栈顶高度的位置)
- 宽度计算:
w = i - leftIndex - 1
(leftIndex
是弹出后的新栈顶) - 虚拟柱子:
- 头部 0:避免空栈判断,作为左边界基准
- 尾部 0:确保所有真实柱子出栈计算
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] heights1 = {2,1,5,6,2,3};
System.out.println(solution.largestRectangleArea(heights1)); // 10
// 测试用例2:全递增
int[] heights2 = {1,2,3,4,5};
System.out.println(solution.largestRectangleArea(heights2)); // 9(3×3)
// 测试用例3:全递减
int[] heights3 = {5,4,3,2,1};
System.out.println(solution.largestRectangleArea(heights3)); // 9(3×3)
// 测试用例4:相同高度
int[] heights4 = {2,2,2,2};
System.out.println(solution.largestRectangleArea(heights4)); // 8(2×4)
// 测试用例5:空数组
int[] heights5 = {};
System.out.println(solution.largestRectangleArea(heights5)); // 0
// 测试用例6:单元素
int[] heights6 = {5};
System.out.println(solution.largestRectangleArea(heights6)); // 5
}
常见问题
-
为什么要在数组头尾加 0?
- 头部 0:避免空栈判断,作为左边界基准(如全递减数组)。
- 尾部 0:确保遍历结束时栈中剩余柱子能被计算(如全递增数组)。
-
宽度计算中
i - stack.peek() - 1
的含义?i
是右边界(第一个小于栈顶高度的位置)。stack.peek()
是左边界(栈顶弹出后的新栈顶,即第一个小于原栈顶高度的位置)。-1
:排除左右边界,计算中间区域的柱子数量。
-
遇到相同高度如何处理?
- 相同高度时直接入栈(保持递增),因为右边界相同高度的柱子不会影响当前柱子计算(当前柱子计算时会延伸到后面相同高度)。
-
时间复杂度为什么是 O(n)?
- 每个柱子最多入栈、出栈各一次,总操作次数为 2n,时间复杂度 O(n)。