bigywater.tech
算法与数据结构 · LeetCode 84

LeetCode 84. 柱状图中最大的矩形

给定一排宽度为 1 的柱子的高度,求柱状图里能勾勒出的最大矩形的面积(矩形必须完全在柱子里面)。一个矩形的高度由它覆盖的柱子里最矮的那根决定,所以对每根柱子 i,以它为最矮柱子的最大矩形,向左向右分别延伸到第一根比它矮的柱子为止。暴力做法是对每个起点向右扩展并维护最小高度,O(n²)。单调栈可以把每根柱子的左右边界一次找出来:栈里保存柱高不减的柱子的下标,遇到更矮的柱子,就说明栈顶那根柱子的右边界找到了,弹出它,弹出之后的栈顶就是它的左边界,立刻算出面积。本文用真实数据展示"末尾没有哨兵"的错误,实测单调栈与暴力的耗时,给出五种语言的实现,并把栈的过程做成分步演示。

30,000
组随机对拍,与暴力解的结果全部一致
15 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。基准解枚举每个左端点,向右扩展并维护区间内的最小高度,用"最小高度 × 宽度"更新答案,是 O(n²) 的另一种写法。一共 14 组用例,覆盖:官方两个示例、单个柱子、高度为 0、递增、递减、全相同、中间有 0 把柱状图隔开:

真实实测 · 本机执行输出
输入 [[2,1,5,6,2,3]]
输出 10
输入 [[2,4]]
输出 4
输入 [[1]]
输出 1
输入 [[0]]
输出 0
输入 [[1,1]]
输出 2
输入 [[1,2,3,4,5]]
输出 9

陷阱:扫描完之后,栈里还有没结算的柱子

只有遇到更矮的柱子时才弹出结算。如果数组扫描完了,栈里还剩下的柱子(比如整体递增的序列)就没有机会被弹出。常见的处理是在数组末尾追加一根高度 0 的"哨兵"柱子,让所有剩下的柱子都被弹出结算。下面是没有哨兵的写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 没有末尾哨兵、扫描完没有清空栈的写法
[2, 1, 5, 6, 2, 3]: 没有哨兵 -> 10    solve -> 10
[1, 2, 3, 4, 5]: 没有哨兵 -> 0    solve -> 9
[2, 4]: 没有哨兵 -> 0    solve -> 4
[5, 4, 3, 2, 1]: 没有哨兵 -> 9    solve -> 9
随机 20000 个数组:没有哨兵的写法出错的有 8408 个(42.0%)

[1, 2, 3, 4, 5](整体递增,没有一根柱子被弹出过)得到 0,正确答案是 9;[2, 4] 得到 0,正确答案是 4。官方示例 [2, 1, 5, 6, 2, 3] 碰巧一致,因为最大矩形在扫描过程中就已经结算了。在随机的 20000 个数组里,没有哨兵的写法出错的有 8408 个,占 42.0%。

单调栈:五种语言的实现

栈里存下标,对应的柱高从栈底到栈顶不减。对每个位置 i(i 从 0 到 n,i = n 时当前高度取 0,即哨兵):只要栈非空并且栈顶的柱高不小于当前高度 h,就弹出栈顶,它的高度是 height;弹出之后,如果栈空,说明左边没有更矮的柱子,左边界是 -1,否则左边界是新的栈顶下标;右边界就是 i(不含),所以宽度是 i - left - 1,面积是 height × 宽度,更新答案。然后把 i 压栈。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(heights): st = [] # 单调栈:存下标,对应的柱高从栈底到栈顶严格不减 best = 0 n = len(heights) for i in range(n + 1): h = heights[i] if i < n else 0 # 在末尾放一根高度 0 的"哨兵"柱子,把栈里剩下的柱子全部弹出结算 while st and heights[st[-1]] >= h: # 当前柱子不比栈顶高:栈顶那根柱子往右最远只能延伸到 i-1 height = heights[st.pop()] left = st[-1] if st else -1 # 弹出之后的栈顶就是往左第一个更矮的柱子(栈空说明左边没有更矮的) best = max(best, height * (i - left - 1)) st.append(i) return best
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[2,1,5,6,2,3]]  →  10
[[2,4]]  →  4
[[1]]  →  1
[[0]]  →  0
[[1,1]]  →  2
[[1,2,3,4,5]]  →  9
Java · 解法代码class Solution { public int largestRectangleArea(int[] heights) { Deque<Integer> st = new ArrayDeque<>(); // 单调栈:存下标,对应的柱高从栈底到栈顶不减 int best = 0, n = heights.length; for (int i = 0; i <= n; i++) { int h = i < n ? heights[i] : 0; // 在末尾放一根高度 0 的"哨兵"柱子,把栈里剩下的柱子全部弹出结算 while (!st.isEmpty() && heights[st.peek()] >= h) { // 当前柱子不比栈顶高:栈顶那根柱子往右最远只能延伸到 i-1 int height = heights[st.pop()]; int left = st.isEmpty() ? -1 : st.peek(); // 弹出之后的栈顶就是往左第一个更矮的柱子 best = Math.max(best, height * (i - left - 1)); } st.push(i); } return best; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[2,1,5,6,2,3]]  →  10
[[2,4]]  →  4
[[1]]  →  1
[[0]]  →  0
[[1,1]]  →  2
[[1,2,3,4,5]]  →  9
Go · 解法代码func largestRectangleArea(heights []int) int { st := []int{} // 单调栈:存下标,对应的柱高从栈底到栈顶不减 best, n := 0, len(heights) for i := 0; i <= n; i++ { h := 0 // 在末尾放一根高度 0 的"哨兵"柱子,把栈里剩下的柱子全部弹出结算 if i < n { h = heights[i] } for len(st) > 0 && heights[st[len(st)-1]] >= h { // 当前柱子不比栈顶高:栈顶那根柱子往右最远只能延伸到 i-1 height := heights[st[len(st)-1]] st = st[:len(st)-1] left := -1 // 弹出之后的栈顶就是往左第一个更矮的柱子 if len(st) > 0 { left = st[len(st)-1] } if area := height * (i - left - 1); area > best { best = area } } st = append(st, i) } return best }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[2,1,5,6,2,3]]  →  10
[[2,4]]  →  4
[[1]]  →  1
[[0]]  →  0
[[1,1]]  →  2
[[1,2,3,4,5]]  →  9
JavaScript · 解法代码function largestRectangleArea(heights) { const st = []; // 单调栈:存下标,对应的柱高从栈底到栈顶不减 let best = 0; const n = heights.length; for (let i = 0; i <= n; i++) { const h = i < n ? heights[i] : 0; // 在末尾放一根高度 0 的"哨兵"柱子,把栈里剩下的柱子全部弹出结算 while (st.length > 0 && heights[st[st.length - 1]] >= h) { // 当前柱子不比栈顶高:栈顶那根柱子往右最远只能延伸到 i-1 const height = heights[st.pop()]; const left = st.length > 0 ? st[st.length - 1] : -1; // 弹出之后的栈顶就是往左第一个更矮的柱子 best = Math.max(best, height * (i - left - 1)); } st.push(i); } return best; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[2,1,5,6,2,3]]  →  10
[[2,4]]  →  4
[[1]]  →  1
[[0]]  →  0
[[1,1]]  →  2
[[1,2,3,4,5]]  →  9
C++ · 解法代码#include <algorithm> #include <vector> using namespace std; class Solution { public: int largestRectangleArea(vector<int>& heights) { vector<int> st; // 单调栈:存下标,对应的柱高从栈底到栈顶不减 int best = 0, n = heights.size(); for (int i = 0; i <= n; i++) { int h = i < n ? heights[i] : 0; // 在末尾放一根高度 0 的"哨兵"柱子,把栈里剩下的柱子全部弹出结算 while (!st.empty() && heights[st.back()] >= h) { // 当前柱子不比栈顶高:栈顶那根柱子往右最远只能延伸到 i-1 int height = heights[st.back()]; st.pop_back(); int left = st.empty() ? -1 : st.back(); // 弹出之后的栈顶就是往左第一个更矮的柱子 best = max(best, height * (i - left - 1)); } st.push_back(i); } return best; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[2,1,5,6,2,3]]  →  10
[[2,4]]  →  4
[[1]]  →  1
[[0]]  →  0
[[1,1]]  →  2
[[1,2,3,4,5]]  →  9
真实实测 · 本机执行输出
共 14 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 14/14
  Java        通过 14/14
  Go          通过 14/14
  JavaScript  通过 14/14
  C++         通过 14/14

耗时实测:线性对平方

柱子个数 n 分别为 1000、3000、1 万、100 万,每个规模取三次中最快的一次:

真实实测 · 本机执行输出
柱子个数 n | 暴力解 | 本文解法
      1000 |    37.652 ms | 0.138 ms
      3000 |   343.572 ms | 0.427 ms
     10000 |  3961.437 ms | 1.398 ms
   1000000 |      (未测,太慢) | 146.960 ms

暴力解从 1000 到 3000(3 倍)耗时增长约 9.1 倍,从 3000 到 1 万增长约 11.5 倍,是平方增长;单调栈从 1000 到 1 万(10 倍)增长约 10.1 倍,是线性,100 万根柱子只需要 147.0ms。n = 1 万时暴力解 3961ms,单调栈 1.4ms,约 2800 倍。暴力解在 n = 100 万时没有再测。

随机对拍

Python 版本另外和暴力解做了随机对拍:长度 1 到 12,高度 0 到 8:

真实实测 · 本机执行输出
随机对拍 30000 组(长度 1 到 12、取值 0 到 8 的随机柱高),solve 与 brute 的结果全部一致。

另外四种语言只在上面 14 组固定用例上验证过,没有做大规模随机对拍。

分步演示:官方示例

下面的演示用官方示例 [2, 1, 5, 6, 2, 3],答案 10(高度 5、宽度 2,覆盖下标 2 和 3)。可以看到:读到高度 1 时,弹出高度 2 的柱子;读到高度 2(下标 4)时,先弹出高度 6(宽 1,面积 6),再弹出高度 5(宽 2,面积 10,刷新最大值);最后的哨兵柱子把栈里剩下的都弹出结算。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

柱状图中最大的矩形:heights = [2, 1, 5, 6, 2, 3]
当前柱子 i当前最大矩形覆盖的柱子

    参考与说明

    • 官方题目页:LeetCode 84. 柱状图中最大的矩形。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:分别预处理每根柱子左右第一个更矮的柱子的两遍扫描写法;分治(按最矮柱子拆开)的写法。
    京ICP备2026067502号-1京公网安备11010502063487号