柱状图中最大的矩形 图解题解
柱状图里能圈出的最大矩形,暴力枚举超时——单调栈让每根柱子在弹出时精确算出以自身为高的最大宽度,O(n) 找到答案。
从左往右扫柱子,栈里存已扫过的柱子下标,从底到顶高度递增。新柱子比栈顶矮时,栈顶那根柱子就找到了它的「右边界」(当前柱子),弹出结算:高度 = 弹出柱子的高度;宽度 = 当前下标 i 减去弹出后新栈顶的下标再减 1(左右两侧第一个比它矮的柱子之间的距离)。反复弹到栈顶不比新柱子高为止,再把新柱子下标压栈。扫完后栈里剩的柱子右边再也没有比它矮的,用末尾哨兵(高度 0)强制触发最后一轮结算。
这道题到底在问什么
- 输入
- heights=[2,1,5,6,2,3]
- 输出
- 10
最优解:为什么这么做
一句话答案:LeetCode 84 柱状图最大矩形的最优解是单调递增栈:以每根柱子为高的最大矩形,宽度由左右两侧第一根更矮的柱子界定;递增栈让每根柱子在被更矮者挡住而弹出的那一刻,左右边界同时现成拿到,一次遍历结算全部柱子,时间 O(n)、空间 O(n)。
柱状图最大矩形在问什么
给出每根柱子的高度 heights,矩形可以横跨相邻的多根柱子,但高度受这一段里最矮那根的限制,求能框出的最大面积。直接枚举「哪一段」有 O(n²) 个区间,每段还要找最矮柱,暴力很贵。换一个枚举对象豁然开朗:最大矩形的上边一定顶着某根柱子的头(否则还能整体升高),所以只需枚举「以哪根柱子为高」,问题变成:每根柱子向左右各能扩到多远。
为什么这题会想到单调栈
柱 i 能扩的范围有清晰的刻画:右边界是右侧第一根比它矮的柱,左边界是左侧第一根比它矮的柱——矮柱像墙一样挡住去路,宽度就是两堵墙之间的格数。对每根柱各自向两边扫是 O(n²),而「找左右第一个更矮元素」正是单调栈的看家本领。
具体想法是:从左到右扫,维护一个柱高从底到顶递增的栈。只要当前柱不比栈顶矮就入栈;一旦当前柱更矮,就意味着栈顶柱的右墙出现了——它再也扩不过去,此刻信息齐全,立刻弹出结算它的矩形。
弹出瞬间左右边界为什么都是现成的
设弹出的柱子是 top,当前扫到下标 i。右墙显然是 i:它是右侧第一根更矮的柱。左墙则是弹出 top 之后露出的新栈顶:递增栈保证新栈顶比 top 矮,而 top 与新栈顶之间的柱子都已在更早的回合被弹掉,说明它们全都不比 top 矮、挡不住它。所以新栈顶正是左侧第一根更矮的柱。
于是宽度 = i - left - 1,面积 = heights[top] × 宽度。注意宽不是 i - left:左右两堵墙本身都是扩不进去的位置,各要让出一格。栈弹空时 left 取 -1,表示这根柱子一路矮到底,能扩到最左端。
末尾为什么要补一根高度 0 的哨兵
扫描到末尾时,栈里可能还留着一串递增的柱子——它们右侧始终没出现更矮的柱,从未被触发结算。参考代码在 heights 末尾追加一根高 0 的哨兵柱,它比任何真实柱都矮,会把栈里剩余的柱子逐一逼出来算完。忘加哨兵是这题最常见的漏答来源。以输入 [2,1,5,6,2,3] 为例:最优解是以柱 2 为高、罩住柱 2 和柱 3 的矩形,高 5 宽 2 面积 10,它由下标 4 的矮柱触发结算;而末尾的柱 4、柱 5 右侧再没有更矮的柱,若无哨兵就永远不会被结算。
复杂度与相关题的关系
时间 O(n):每根柱子入栈一次、出栈一次,总操作线性。空间 O(n):最坏整体递增,栈装下全部下标。栈里必须存下标而非高度,因为宽度要用下标差来算。
这套「找左右第一根更矮柱」的框架是一类题的内核:LeetCode 85 最大矩形就是把 0/1 矩阵逐行压成柱状图后,每行调用本题解法取最大值。把 84 吃透,85 只剩一步转化。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3核心一句:栈顶被一根更矮的柱「挡住」时弹出结算,它能往两边扩到的边界就是左右各第一根比它矮的柱。
- 4轮到柱 0(高 2)。栈是空的,没有更矮的栈顶要结算,直接把它入栈。
- 5没有更矮的栈顶要结算了,把柱 0(高 2)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 6轮到柱 1(高 1)。看栈顶柱 0(高 2):当前这根更矮,说明栈顶柱再往右扩就被挡住了,该弹出它、以它为高算矩形。
- 7弹出柱 0(高 2)。它向左只能扩到最左端,向右扩到柱 1(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 2 × 宽 1 = 2。比之前的 0 大,更新最大面积为 2。
- 8没有更矮的栈顶要结算了,把柱 1(高 1)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 9轮到柱 2(高 5)。看栈顶柱 1(高 1):当前不比栈顶矮,栈还能保持递增,直接入栈。
- 10没有更矮的栈顶要结算了,把柱 2(高 5)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 11轮到柱 3(高 6)。看栈顶柱 2(高 5):当前不比栈顶矮,栈还能保持递增,直接入栈。
- 12没有更矮的栈顶要结算了,把柱 3(高 6)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 13轮到柱 4(高 2)。看栈顶柱 3(高 6):当前这根更矮,说明栈顶柱再往右扩就被挡住了,该弹出它、以它为高算矩形。
- 14弹出柱 3(高 6)。它向左只能扩到柱 2(更矮,挡住),向右扩到柱 4(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 6 × 宽 1 = 6。比之前的 2 大,更新最大面积为 6。
- 15弹出柱 2(高 5)。它向左只能扩到柱 1(更矮,挡住),向右扩到柱 4(更矮,挡住),所以高亮的这 2 根就是它能罩住的最宽范围。矩形 = 高 5 × 宽 2 = 10。比之前的 6 大,更新最大面积为 10。
- 16没有更矮的栈顶要结算了,把柱 4(高 2)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 17轮到柱 5(高 3)。看栈顶柱 4(高 2):当前不比栈顶矮,栈还能保持递增,直接入栈。
- 18没有更矮的栈顶要结算了,把柱 5(高 3)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
- 19轮到末尾哨兵柱(高 0,灰色那根)。它比任何真实柱都矮,作用就是逼着把栈里还没结算的柱子全部弹出来算一遍。
- 20弹出柱 5(高 3)。它向左只能扩到柱 4(更矮,挡住),向右扩到柱 6(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 3 × 宽 1 = 3。不及当前最大 10,最大面积不变。
- 21弹出柱 4(高 2)。它向左只能扩到柱 1(更矮,挡住),向右扩到柱 6(更矮,挡住),所以高亮的这 4 根就是它能罩住的最宽范围。矩形 = 高 2 × 宽 4 = 8。不及当前最大 10,最大面积不变。
- 22弹出柱 1(高 1)。它向左只能扩到最左端,向右扩到柱 6(更矮,挡住),所以高亮的这 6 根就是它能罩住的最宽范围。矩形 = 高 1 × 宽 6 = 6。不及当前最大 10,最大面积不变。
- 23哨兵入栈(其实扫描已结束)。栈里只剩它,所有真实柱都结算完毕,最终最大矩形面积 = 10。
- 24所有柱子都被结算过一次,最大的那个矩形以高 5(柱 2)向右罩住柱 2、柱 3 两根,宽 2,面积 = 5 × 2 = 10。这就是答案。
⚠️ 容易写错的地方
✗ 错:栈里存高度
✓ 对:栈里存下标
宽要用下标差 i−left−1 来算,只存高度算不出宽
✗ 错:宽算成 i − left
✓ 对:宽 = i − left − 1
左右两个边界都是「比它矮、扩不进去」的柱,要各去掉一格
✗ 错:不加末尾哨兵
✓ 对:末尾补一根高 0
否则扫完后栈里仍有递增的柱子没被结算,会漏掉答案
完整代码(Python / C++ / Java)
Python
def largestRectangleArea(heights):
heights = heights + [0] # 末尾哨兵,逼出栈中剩余柱
st, best = [], 0 # st 存下标,柱高递增
for i, h in enumerate(heights):
while st and h < heights[st[-1]]:
top = st.pop()
left = st[-1] if st else -1
width = i - left - 1
best = max(best, heights[top] * width)
st.append(i)
return bestC++
int largestRectangleArea(vector<int>& heights){
heights.push_back(0); // 末尾哨兵
stack<int> st; int best = 0; // 存下标,柱高递增
for(int i = 0; i < (int)heights.size(); i++){
while(!st.empty() && heights[i] < heights[st.top()]){
int top = st.top(); st.pop();
int left = st.empty() ? -1 : st.top();
int width = i - left - 1;
best = max(best, heights[top] * width);
}
st.push(i);
}
return best;
}Java
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] h = new int[n + 1]; // 末尾哨兵 0
System.arraycopy(heights, 0, h, 0, n);
Deque<Integer> st = new ArrayDeque<>(); // 存下标,柱高递增
int best = 0;
for (int i = 0; i <= n; i++) {
while (!st.isEmpty() && h[i] < h[st.peek()]) {
int top = st.pop();
int left = st.isEmpty() ? -1 : st.peek();
int width = i - left - 1;
best = Math.max(best, h[top] * width);
}
st.push(i);
}
return best;
}复杂度
时间
O(n)
每根柱子进栈、出栈各一次
空间
O(n)
最坏整体递增,栈装下所有下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 柱状图中最大的矩形 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么单调栈能做到 O(n)?+
每个下标最多入栈一次、出栈一次,总操作线性,远好于暴力枚举边界的 O(n²)。
这题和「接雨水」「最大全 1 矩形」有什么关系?+
都是单调栈找「左右第一个更矮/更高」的边界。85 题「最大矩形」就是把每一行压成柱状图后逐行套用本题。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 柱状图中最大的矩形 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。