题目描述
思路解析
一句话答案: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 只剩一步转化。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心一句:栈顶被一根更矮的柱「挡住」时弹出结算,它能往两边扩到的边界就是左右各第一根比它矮的柱。
轮到柱 0(高 2)。栈是空的,没有更矮的栈顶要结算,直接把它入栈。
没有更矮的栈顶要结算了,把柱 0(高 2)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到柱 1(高 1)。看栈顶柱 0(高 2):当前这根更矮,说明栈顶柱再往右扩就被挡住了,该弹出它、以它为高算矩形。
弹出柱 0(高 2)。它向左只能扩到最左端,向右扩到柱 1(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 2 × 宽 1 = 2。比之前的 0 大,更新最大面积为 2。
没有更矮的栈顶要结算了,把柱 1(高 1)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到柱 2(高 5)。看栈顶柱 1(高 1):当前不比栈顶矮,栈还能保持递增,直接入栈。
没有更矮的栈顶要结算了,把柱 2(高 5)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到柱 3(高 6)。看栈顶柱 2(高 5):当前不比栈顶矮,栈还能保持递增,直接入栈。
没有更矮的栈顶要结算了,把柱 3(高 6)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到柱 4(高 2)。看栈顶柱 3(高 6):当前这根更矮,说明栈顶柱再往右扩就被挡住了,该弹出它、以它为高算矩形。
弹出柱 3(高 6)。它向左只能扩到柱 2(更矮,挡住),向右扩到柱 4(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 6 × 宽 1 = 6。比之前的 2 大,更新最大面积为 6。
弹出柱 2(高 5)。它向左只能扩到柱 1(更矮,挡住),向右扩到柱 4(更矮,挡住),所以高亮的这 2 根就是它能罩住的最宽范围。矩形 = 高 5 × 宽 2 = 10。比之前的 6 大,更新最大面积为 10。
没有更矮的栈顶要结算了,把柱 4(高 2)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到柱 5(高 3)。看栈顶柱 4(高 2):当前不比栈顶矮,栈还能保持递增,直接入栈。
没有更矮的栈顶要结算了,把柱 5(高 3)压入栈顶。注意栈里从底到顶的柱高始终是递增的——这正是「单调递增栈」名字的由来。
轮到末尾哨兵柱(高 0,灰色那根)。它比任何真实柱都矮,作用就是逼着把栈里还没结算的柱子全部弹出来算一遍。
弹出柱 5(高 3)。它向左只能扩到柱 4(更矮,挡住),向右扩到柱 6(更矮,挡住),所以高亮的这 1 根就是它能罩住的最宽范围。矩形 = 高 3 × 宽 1 = 3。不及当前最大 10,最大面积不变。
弹出柱 4(高 2)。它向左只能扩到柱 1(更矮,挡住),向右扩到柱 6(更矮,挡住),所以高亮的这 4 根就是它能罩住的最宽范围。矩形 = 高 2 × 宽 4 = 8。不及当前最大 10,最大面积不变。
弹出柱 1(高 1)。它向左只能扩到最左端,向右扩到柱 6(更矮,挡住),所以高亮的这 6 根就是它能罩住的最宽范围。矩形 = 高 1 × 宽 6 = 6。不及当前最大 10,最大面积不变。
哨兵入栈(其实扫描已结束)。栈里只剩它,所有真实柱都结算完毕,最终最大矩形面积 = 10。
所有柱子都被结算过一次,最大的那个矩形以高 5(柱 2)向右罩住柱 2、柱 3 两根,宽 2,面积 = 5 × 2 = 10。这就是答案。
单根 / 等高 / 递增三种边界先想清楚。
两个高频追问,单调栈是一整类「找左右边界」题的模板。
参考代码
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 best复杂度
- 时间:O(n),每根柱子进栈、出栈各一次
- 空间:O(n),最坏整体递增,栈装下所有下标
易错点
面试追问把动画讲成自己的话
追问为什么单调栈能做到 O(n)?
追问这题和「接雨水」「最大全 1 矩形」有什么关系?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
字符串解码
LeetCode 394 · 中等 · 沿着 栈 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题