84. 柱状图中最大的矩形 — 单调栈解法详解#
题目描述#
给定 n 个非负整数,表示柱状图中各柱子的高度,每个柱子宽度为 1。 求柱状图中能勾勒出的矩形的最大面积。
输入: heights = [2,1,5,6,2,3]
输出: 10plaintext █
█ █
█ █
█ █ █
█ █ █ █ █
█ █ █ █ █ █
2 1 5 6 2 3
最大矩形:高=5,宽=2(第2、3根柱子) → 面积=10plaintext暴力思路(先理解本质)#
关键观察:以第 i 根柱子为高度的最大矩形, 宽度 = 向左延伸到第一个比它矮的柱子 + 向右延伸到第一个比它矮的柱子。
对每根柱子 i:
left[i] = 左边第一个严格小于 heights[i] 的下标
right[i] = 右边第一个严格小于 heights[i] 的下标
面积 = heights[i] × (right[i] - left[i] - 1)plaintext暴力做法:对每根柱子向左右各扫一遍 → O(N²) 优化方向:用单调栈一次求出所有 left[] 和 right[]
单调栈核心思想#
维护一个单调递增栈(栈底到栈顶,高度递增)。
当遇到一根比栈顶矮的柱子时:
- 栈顶柱子找到了右边第一个比它矮的柱子(当前柱子)
- 弹出栈顶后,新栈顶就是左边第一个比它矮的柱子
- 此时即可计算栈顶柱子能构成的最大矩形面积
单调递增栈:保证每个元素出栈时,左右边界都已确定plaintext哨兵技巧(简化边界处理)#
在 heights 首尾各加一个高度为 0 的哨兵:
原数组: [2, 1, 5, 6, 2, 3]
加哨兵: [0, 2, 1, 5, 6, 2, 3, 0]
↑ ↑
左哨兵 右哨兵(强制清空栈)plaintext- 左哨兵:保证栈不会空,left 边界统一处理
- 右哨兵:遍历结束时强制把栈中剩余元素全部弹出计算
完整 Java 代码#
class Solution {
public int largestRectangleInHistogram(int[] heights) {
// 原始柱子数量
int n = heights.length;
// 新数组:在首尾各加一个高度为 0 的哨兵,统一边界处理
int[] h = new int[n + 2];
// 左哨兵:保证弹栈后一定还有“左边界”可取
h[0] = 0;
// 右哨兵:在遍历末尾强制触发弹栈,结算所有剩余柱子
h[n + 1] = 0;
// 把原数组拷贝到 h[1..n]
for (int i = 0; i < n; i++) h[i + 1] = heights[i];
// 单调递增栈:存下标,且对应高度从栈底到栈顶递增
Deque<Integer> stack = new ArrayDeque<>();
// 记录扫描过程中的最大矩形面积
int maxArea = 0;
// 遍历包含哨兵的新数组
for (int i = 0; i < h.length; i++) {
// 若当前高度更小,说明栈顶柱子的“右边第一个更矮柱子”已确定为 i
while (!stack.isEmpty() && h[i] < h[stack.peek()]) {
// mid:以它作为矩形高度的候选柱子下标
int mid = stack.pop();
// left:弹出后新的栈顶,就是 mid 左边第一个更矮柱子的下标
int left = stack.peek();
// right:当前遍历位置 i,就是 mid 右边第一个更矮柱子的下标
int right = i;
// 可扩展宽度 = (left, right) 开区间长度,不含两端
int width = right - left - 1;
// 以 h[mid] 为高时的最大面积
int area = h[mid] * width;
// 更新全局最大面积
maxArea = Math.max(maxArea, area);
}
// 当前柱子下标入栈,继续维护递增高度序列
stack.push(i);
}
// 返回最大矩形面积
return maxArea;
}
}java执行过程图解(heights = [2,1,5,6,2,3])#
加哨兵后:h = [0, 2, 1, 5, 6, 2, 3, 0],下标 0~7
i=0 h=0 栈空,直接入栈 stack: [0]
i=1 h=2 2>0,直接入栈 stack: [0,1]
i=2 h=1 1<2,弹出 mid=1
left=0, right=2
宽=2-0-1=1, 面积=2×1=2 maxArea=2
1>0,入栈 stack: [0,2]
i=3 h=5 5>1,入栈 stack: [0,2,3]
i=4 h=6 6>5,入栈 stack: [0,2,3,4]
i=5 h=2 2<6,弹出 mid=4
left=3, right=5
宽=5-3-1=1, 面积=6×1=6 maxArea=6
2<5,弹出 mid=3
left=2, right=5
宽=5-2-1=2, 面积=5×2=10 maxArea=10
2>1,入栈 stack: [0,2,5]
i=6 h=3 3>2,入栈 stack: [0,2,5,6]
i=7 h=0 0<3,弹出 mid=6
left=5, right=7
宽=7-5-1=1, 面积=3×1=3 maxArea=10
0<2,弹出 mid=5
left=2, right=7
宽=7-2-1=4, 面积=2×4=8 maxArea=10
0<1,弹出 mid=2
left=0, right=7
宽=7-0-1=6, 面积=1×6=6 maxArea=10
栈中只剩哨兵 0,结束
最终答案: 10plaintext宽度计算公式说明#
弹出 mid 时:
left = stack.peek() ← 新栈顶,左边第一个比 h[mid] 小的
right = i ← 当前遍历位置,右边第一个比 h[mid] 小的
width = right - left - 1
↑ 不含 left 和 right 两端(它们都比 h[mid] 矮)plaintext例:下标 0 1 2 3 4
高度 0 1 5 1 0
↑
mid=2, h=5
left=1(h=1), right=3(h=1)
width = 3-1-1 = 1 ✓plaintext单调性维护示意#
栈始终保持从底到顶高度递增:
入栈前若违反单调性,先弹出直到满足为止
栈顶
|
[6] ← h=6
[5] ← h=5
[1] ← h=1
[0] ← h=0 (哨兵)
底
遇到 h=2:弹出 6、5,再入栈 2
[2]
[1]
[0]plaintext复杂度分析#
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(N) | 每个元素最多入栈、出栈各一次 |
| 空间 | O(N) | 栈的最大深度 |
易错点汇总#
| 易错点 | 正确做法 |
|---|---|
| 忘记处理遍历结束后栈中的残留元素 | 加右哨兵 h=0 强制清空 |
栈空时 stack.peek() 报错 | 加左哨兵,栈永远不会空 |
宽度算成 right - left | 应为 right - left - 1(不含两端) |
条件用 <= 还是 < | 用严格 <,等高柱子可以合并处理 |
相关题目#
| 题号 | 题目 | 关联点 |
|---|---|---|
| 85 | 最大矩形 | 本题逐行应用 |
| 42 | 接雨水 | 同样用单调栈,方向相反 |
| 239 | 滑动窗口最大值 | 单调队列变体 |