【力扣100题】86.柱状图中最大的矩形

一、题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例:

示例 输入 输出
示例1 heights = 2,1,5,6,2,3 10
示例2 heights = 2,4 4

图示(示例1):

复制代码
最大矩形面积 = 2 * 5 = 10(高度为2,宽度从索引0到5)

提示:

  • 1 <= heights.length <= 10^5
  • 0 <= heightsi <= 10^4

二、解题思路总览

拿到这道题,首先思考:最大矩形可能出现在哪里?

对于每个柱子 i,它作为最高矩形的高度时:

  • 向左找连续的、高度 >= heightsi 的柱子
  • 向右找连续的、高度 >= heightsi 的柱子
  • 宽度 = right - left - 1
  • 面积 = heightsi * width

核心难点: 如何快速找到每个柱子"能向左/向右扩展多远"?

暴力解法 O(n^2):

  • 对于每个柱子,向左向右遍历找边界
  • 10^5 数据会超时

关键洞察: 利用单调递增栈,栈中存储索引,栈顶元素的下方是"左边界",弹栈时自然找到"右边界"。

核心思路:

步骤 思路 说明
1 末尾加哨兵 heights.push_back(-1),处理最后边界
2 维护单调递增栈 栈中索引对应的高度单调递增
3 弹栈计算面积 弹出的柱子 i,其右边界是当前索引,左边界是栈顶
4 更新最大面积 ans = max(ans, heightsi * width)

单调栈的核心思想:

  • 栈顶下方是左边界(第一个比栈顶矮的柱子)
  • 当前遍历位置是右边界(第一个比栈顶矮的柱子)
  • 弹栈时,右边界已知,左边界是新的栈顶

三、完整代码(方法一:单调递增栈)

cpp 复制代码
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        heights.push_back(-1);  // 末尾加哨兵,处理最后边界
        stack<int> st;
        st.push(-1);  // 栈底哨兵,便于处理边界
        int ans = 0;

        for (int right = 0; right < heights.size(); right++) {
            int h = heights[right];
            // 当栈顶高度 >= 当前高度时,弹栈计算面积
            while (st.size() > 1 && heights[st.top()] >= h) {
                int i = st.top();  // 弹出柱子的索引
                st.pop();
                int left = st.top();  // 左边界索引
                // 宽度 = right - left - 1
                ans = max(ans, heights[i] * (right - left - 1));
            }
            st.push(right);
        }
        return ans;
    }
};

四、其他解法

方法二:分治法(递归分割)
cpp 复制代码
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        return divide(heights, 0, heights.size() - 1);
    }

private:
    int divide(const vector<int>& heights, int left, int right) {
        if (left > right) return 0;
        if (left == right) return heights[left];

        int mid = (left + right) / 2;
        int minIdx = mid;
        // 找最小高度的位置
        for (int i = mid; i <= right; i++) {
            if (heights[i] < heights[minIdx]) minIdx = i;
        }
        // 以最小高度为高的矩形
        int area = heights[minIdx] * (right - left + 1);
        // 分治左右两部分
        return max({area,
                    divide(heights, left, minIdx - 1),
                    divide(heights, minIdx + 1, right)});
    }
};

问题: 最坏 O(n^2),期望 O(n log n),但实现复杂,不推荐


方法三:线段树 + 分治(进阶)
cpp 复制代码
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        // 构建线段树找最小值位置,然后分治
        // 实现较复杂,面试中较少考察
        return 0;
    }
};

问题: 实现太复杂,不推荐


方法四:暴力解法(O(n^2),仅作对比)
cpp 复制代码
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int n = heights.size();
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int h = heights[i];
            // 向左扩展
            int left = i;
            while (left > 0 && heights[left - 1] >= h) left--;
            // 向右扩展
            int right = i;
            while (right < n - 1 && heights[right + 1] >= h) right++;
            ans = max(ans, h * (right - left + 1));
        }
        return ans;
    }
};

问题: 两层循环,最坏 O(n^2),不可用于生产


五、算法流程图

以输入 heights = [2,1,5,6,2,3] 为例,逐步展示算法执行过程:

复制代码
初始状态:
        heights: [2, 1, 5, 6, 2, 3, -1]
        indices:  0  1  2  3  4  5 6
        ans: 0
        st: [-1] (栈底哨兵)

Step 1: right=0, h=2
        - heights[st.top()](2) >= 2? 是,但 st.size()=1,只入栈
        st.push(0)
        heights: [2, 1, 5, 6, 2, 3, -1]
             ^
        st: [-1, 0]  (高度: -, 2)

Step 2: right=1, h=1
        - heights[st.top()](2) >= 1? 是,弹出 i=0
          left = st.top() = -1
          area = 2 * (1 - (-1) - 1) = 2 * 1 = 2
          ans = max(0, 2) = 2
        - heights[st.top()](-1) >= 1? 否,停止弹栈
        st.push(1)
        heights: [2, 1, 5, 6, 2, 3, -1]
              ^
        st: [-1, 1]  (高度: -, 1)

Step 3: right=2, h=5
        - heights[st.top()](1) >= 5? 否,只入栈
        st.push(2)
        heights: [2, 1, 5, 6, 2, 3, -1]
                   ^
        st: [-1, 1, 2]  (高度: -, 1, 5)

Step 4: right=3, h=6
        - heights[st.top()](5) >= 6? 否,只入栈
        st.push(3)
        heights: [2, 1, 5, 6, 2, 3, -1]
                      ^
        st: [-1, 1, 2, 3]  (高度: -, 1, 5, 6)

Step 5: right=4, h=2
        - heights[st.top()](6) >= 2? 是,弹出 i=3
          left = st.top() = 2
          area = 6 * (4 - 2 - 1) = 6 * 1 = 6
          ans = max(2, 6) = 6
        - heights[st.top()](5) >= 2? 是,弹出 i=2
          left = st.top() = 1
          area = 5 * (4 - 1 - 1) = 5 * 2 = 10
          ans = max(6, 10) = 10
        - heights[st.top()](1) >= 2? 否,停止弹栈
        st.push(4)
        heights: [2, 1, 5, 6, 2, 3, -1]
                    ^
        st: [-1, 1, 4]  (高度: -, 1, 2)

Step 6: right=5, h=3
        - heights[st.top()](2) >= 3? 否,只入栈
        st.push(5)
        heights: [2, 1, 5, 6, 2, 3, -1]
                         ^
        st: [-1, 1, 4, 5]  (高度: -, 1, 2, 3)

Step 7: right=6, h=-1 (哨兵)
        - heights[st.top()](3) >= -1? 是,弹出 i=5
          left = st.top() = 4
          area = 3 * (6 - 4 - 1) = 3 * 1 = 3
          ans = max(10, 3) = 10
        - heights[st.top()](2) >= -1? 是,弹出 i=4
          left = st.top() = 1
          area = 2 * (6 - 1 - 1) = 2 * 4 = 8
          ans = max(10, 8) = 10
        - heights[st.top()](1) >= -1? 是,弹出 i=1
          left = st.top() = -1
          area = 1 * (6 - (-1) - 1) = 1 * 6 = 6
          ans = max(10, 6) = 10
        st.push(6)
        heights: [2, 1, 5, 6, 2, 3, -1]
                              ^
        st: [-1, 6]  (高度: -, -1)

最终结果:
        ans = 10 ✓

**最大矩形:**高度为 5,宽度为 2(索引2到3),面积 = 5×2 = 10


六、逐行解析(方法一)

cpp 复制代码
heights.push_back(-1);

哨兵技巧: 在末尾加一个高度为 -1 的柱子,这样遍历到最后时,所有剩余栈中的柱子都会被弹出并计算面积,避免额外处理边界。


cpp 复制代码
stack<int> st;
st.push(-1);

栈初始化: 栈底放入 -1 作为哨兵,这样当栈中只有一个哨兵时,st.size() > 1 的判断可以统一处理边界。


cpp 复制代码
for (int right = 0; right < heights.size(); right++) {

遍历: right 是当前柱子的索引,也是弹出柱子的"右边界"。


cpp 复制代码
    while (st.size() > 1 && heights[st.top()] >= h) {

弹栈条件: 当栈顶高度 >= 当前高度时,说明栈顶柱子的"右边界"找到了。弹栈计算以栈顶柱子为高的矩形面积。


cpp 复制代码
        int i = st.top();
        st.pop();
        int left = st.top();
        ans = max(ans, heights[i] * (right - left - 1));

计算面积: i 是弹出的柱子索引(它的高度是矩形的高),left 是新的栈顶索引(第一个比 i 矮的左边界),right 是当前遍历位置(第一个比 i 矮的右边界),所以宽度 = right - left - 1。


七、复杂度分析

各方法复杂度对比
方法 时间复杂度 空间复杂度 说明
方法一:单调递增栈 O(n) O(n) 标准解法,稳定高效
方法二:分治法 O(n log n)(期望) O(log n) 最坏 O(n^2),不稳定
方法三:线段树+分治 O(n log n) O(n) 实现复杂
方法四:暴力解法 O(n^2) O(1) 不可用

核心: 方法一是 O(n),因为每个柱子最多入栈一次、出栈一次。

方法一详细复杂度分析

时间复杂度:O(n)

  • 遍历一次:O(n)
  • 每个柱子最多入栈一次、出栈一次
  • 总弹栈次数 = O(n)

空间复杂度:O(n)

  • 最坏情况:heights 单调递增,所有柱子都入栈
  • 加上哨兵占位符

弹栈分析:

情况 弹栈次数 说明
单调递增 O(n) 每次都弹出前一个
单调递减 0 一次都不弹
随机 均摊 O(1) 总弹栈次数 = O(n)

八、面试追问 FAQ

问题 回答要点
Q1: 为什么要在末尾加哨兵 -1? 避免最后单独处理边界。如果没有哨兵,遍历结束后栈中还有柱子,需要额外弹出计算。哨兵是最常用的技巧。
Q2: 栈底哨兵 -1 有什么用? 统一处理边界。当弹栈后栈变空时,栈顶是 -1,可以用 right - (-1) - 1 = right 正确计算宽度。
Q3: 为什么条件是 heights[st.top()] >= h 而不是 > h >= 保证栈中相同高度只保留最右边的一个,避免重复计算。如果用 >,会漏掉一些边界情况。
Q4: 单调栈能解决哪些其他问题? 每日温度、接雨水、最大矩形、下一个更大元素等。单调栈是处理"边界"问题的通用技巧。
Q5: 如果要返回最大矩形的起始和结束位置怎么办? 在弹栈时记录 left 和 right 的位置即可,start = left + 1end = right - 1
Q6: 面试时推荐哪种方法? 首选方法一(单调递增栈),代码简洁高效,面试高频题。

九、相关题目

题号 题目 难度 关联点
84 柱状图中最大的矩形 困难 本题
85 最大矩形 困难 二维扩展
739 每日温度 中等 单调栈入门
42 接雨水 困难 单调栈综合应用
496 下一个更大元素 I 简单 单调栈入门
503 下一个更大元素 II 中等 环形数组

推荐刷题顺序:

  1. 739(每日温度)→ 单调栈入门
  2. 496(下一个更大元素 I)→ 简单变种
  3. **本题(84.柱状图中最大的矩形)**→ 单调栈进阶
  4. 42(接雨水)→ 综合应用
  5. 85(最大矩形)→ 二维扩展

十、总结

| 维度 | 内容 |

|------|------|------|

| 考察知识点 | 单调栈、边界计算 |

| 难度 | 困难 |

| 核心思维 | 栈顶是当前高矩形,右边界已知,左边界是栈顶下方 |

| 关键技巧 | 末尾哨兵、栈底哨兵、弹栈计算面积 |

| 推荐解法 | 方法一(单调递增栈) |

| 变形题 | 最大矩形(二维)、接雨水 |

一句话总结: 柱状图最大矩形的核心是单调递增栈:弹栈时栈顶柱子的"右边界"是当前遍历位置,"左边界"是新的栈顶,宽度 = right - left - 1。哨兵技巧是简化代码的关键。


相关推荐
MrZhao4008 小时前
一个最小 Agent 是怎么跑起来的:Agent Loop 与工具使用全链路
算法
Keven_118 小时前
算法札记:二分
算法·二分
TCW11218 小时前
AI底层系列:用C++实现线性代数的公式推导与算法设计-6.线性方程组的解集
c++·人工智能·算法
luoyayun3618 小时前
从零实现 EBU R128 LUFS 响度分析:K-weighting 滤波、双门限算法
算法·lufs响度分析
小糯米6018 小时前
JS 数组
数据结构·算法·排序算法
小欣加油8 小时前
leetcode3612 用特殊操作处理字符串I
数据结构·c++·算法·leetcode·职场和发展
拳里剑气8 小时前
C++算法:链表
c++·算法·链表
凌波粒9 小时前
LeetCode--90.子集II(回溯算法)
数据结构·算法·leetcode
旧曲重听19 小时前
2026前端技术从「夯」到「拉」
前端·程序人生·职场和发展·软件工程
旖-旎9 小时前
《LeetCode 417 太平洋大西洋水流问题 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill