84. 柱状图中最大矩形(Largest Rectangle in Histogram)

频次 ★★★★ · 难度 🔴 · 高频:阿里/字节

题目

给定 n 个非负整数表示柱状图每根柱子的高度(宽度均为 1),求柱状图中能勾勒出的最大矩形面积。

示例

输入: heights = [2,1,5,6,2,3]
输出: 10   (由高度 5、6 的两根柱子构成,宽 2 高 5)

思路

暴力法枚举每个矩形的左右边界是 O(n²)。更优的思路是:以每根柱子的高度为矩形的高,向左右尽量扩展,找到”以它为高度上限”能撑到的最大宽度,取所有柱子里的最大面积。

单调递增栈维护下标,保持栈内对应的高度递增:

  • 遍历到第 i 根柱子,如果比栈顶矮,说明栈顶那根柱子往右不能再扩展了(被 i 挡住了),弹出栈顶结算它能撑起的矩形面积:高度是被弹出的柱子高度,宽度是”新的栈顶下标”和 i 之间的距离减一。
  • 末尾追加一根高度为 0 的哨兵柱子,保证栈中剩余的柱子都能被弹出结算。

代码

public int largestRectangleArea(int[] heights) {
    Deque<Integer> stack = new ArrayDeque<>(); // 存下标,对应高度递增
    int maxArea = 0;
    int n = heights.length;
 
    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i]; // 末尾哨兵,逼出栈内剩余元素
        while (!stack.isEmpty() && h < heights[stack.peek()]) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        stack.push(i);
    }
    return maxArea;
}

复杂度

  • 时间:O(n) — 每个下标入栈、出栈各一次
  • 空间:O(n) — 最坏情况(高度递增)栈存满所有下标

边界条件

  • 只有一根柱子:哨兵触发一次弹出结算,宽度为 1,面积等于该柱子高度。
  • 所有柱子高度相同(如 [3,3,3]):只有遇到哨兵 0 时才会连续弹出结算,最终能正确算出”整体宽度 × 高度”的最大面积。
  • 高度单调递增(如 [1,2,3]):栈会一路压入不弹出,直到哨兵才开始结算,是最坏空间情况。

变式

  • 最大正方形(矩阵中全 1 的最大正方形,LeetCode 221 题):可以转化为逐行构造”以每个位置为底的柱状图高度”,再对每一行跑一次本题的算法,取所有行里的最大正方形边长。
  • 最大矩形(矩阵中全 1 的最大矩形,LeetCode 85 题):同样是逐行构造柱状图高度,直接复用本题的单调栈算法求每一行的最大矩形面积,取全局最大值。

易错点

  • 宽度计算 stack.isEmpty() ? i : i - stack.peek() - 1:如果栈弹空了,说明这根柱子是当前最矮的,能一直撑到最左边(宽度就是 i);否则宽度是”新栈顶和 i 之间”的柱子数。
  • 末尾的哨兵高度 0 不能省略,否则栈里剩下的递增高度不会被结算,会漏算答案。

面试追问

  • 为什么要在末尾补一根高度为 0 的柱子? 如果不补,栈里”高度递增”的柱子在遍历结束后不会被弹出结算(因为没有更矮的柱子触发弹出条件),漏掉这些柱子可能撑起的矩形面积;补一根高度 0 的哨兵能保证所有柱子最终都被结算一次。
  • 这题和 739. 每日温度 都用单调栈,核心区别是什么? 739 题只需要知道”下一个更大元素的位置”,本题不仅要知道”左右两边第一个更矮的柱子在哪”,还要用这两个边界计算出宽度和面积,是单调栈”确定左右边界”这个能力的进一步应用,属于同一类模板但结算逻辑更复杂。

关联题

  • 同套路:739. 每日温度 —— 单调栈基本功;本题问的是”两侧第一个更小”
  • 进阶:85. 最大矩形 —— 01 矩阵按行累积成柱状图后逐行调用本题;42. 接雨水 —— 单调栈的另一经典应用
  • 知识点:首尾补高度 0 的哨兵,消除”栈不空要收尾”的特判——哨兵思想同链表 dummy 结点