📌 题目链接:84. 柱状图中最大的矩形 - 力扣(LeetCode)
🔍 难度:困难 | 🏷️ 标签:栈、数组、单调栈
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)
🧠 题目分析
给定一个非负整数数组 heights,每个元素表示柱状图中一个宽度为 1 的柱子的高度。要求找出能勾勒出的最大矩形面积。
这个问题看似简单,但暴力解法的时间复杂度高达 O(n²) ,在 LeetCode 的约束(n ≤ 1e5)下会超时。因此,我们需要一种更高效的算法。
核心观察点是:对于每一个柱子,以其高度为矩形的高,向左右扩展直到遇到比它矮的柱子,此时形成的矩形面积即为该柱子所能贡献的最大面积。
于是问题转化为:对每个位置 i,快速找到其左侧第一个小于它的位置 left[i] 和右侧第一个小于它的位置 right[i] 。
这正是**单调栈(Monotonic Stack)**的经典应用场景!
🧱 核心算法及代码讲解
🔁 什么是单调栈?
单调栈是一种特殊的栈结构,其中元素按照单调递增或单调递减的顺序排列。在本题中,我们使用单调递增栈(从栈底到栈顶,对应的高度严格递增)。
✅ 关键性质:当新元素入栈时,若破坏了单调性(即当前高度 ≤ 栈顶高度),则不断弹出栈顶,直到满足单调性为止。每次弹出时,可以确定被弹出元素的右边界!
🎯 为什么单调栈适用于本题?
-
对于每个柱子 i,我们希望知道:
- left[i] :i 左边第一个高度 < heights[i] 的位置;
- right[i] :i 右边第一个高度 < heights[i] 的位置。
-
单调栈可以在一次遍历中高效维护这些信息。
📜 算法步骤(优化版:单次遍历)
-
初始化
left数组(记录左边界),right数组(初始化为 n,即右哨兵); -
使用一个栈
mono_stack存储索引; -
从左到右遍历
heights:- 若栈非空且
heights[栈顶] >= heights[i],说明栈顶元素的右边界就是i,将其弹出并设置right[栈顶] = i; - 此时栈顶(若存在)就是
i的左边界; - 将
i入栈;
- 若栈非空且
-
遍历结束后,未弹出的元素右边界仍为
n(已在初始化时设定); -
最后遍历每个 i,计算
(right[i] - left[i] - 1) * heights[i],取最大值。
💻 C++ 核心代码(带详细行注释)
vector<int> left(n), right(n, n); // right 初始化为 n(右哨兵)
stack<int> mono_stack;
for (int i = 0; i < n; ++i) {
// 当前高度 <= 栈顶高度 → 栈顶的右边界就是 i
while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
right[mono_stack.top()] = i; // 设置右边界
mono_stack.pop(); // 弹出栈顶
}
// 栈空?说明左边没有更小的 → 左边界为 -1(左哨兵)
// 否则,栈顶就是左边第一个更小的位置
left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
mono_stack.push(i); // 当前索引入栈
}
⚠️ 注意:这里使用
>=而不是>是为了处理高度相等的情况。虽然会导致某些柱子的右边界“提前”,但最右边的那个同高柱子仍能正确计算出最大面积,因此不影响最终结果。
🧩 解题思路(分步详解)
Step 1️⃣:理解“以每个柱子为高”的思想
- 枚举每个柱子作为矩形的高;
- 向左右扩展,直到遇到比它矮的柱子(不能继续扩展);
- 宽度 = 右边界 - 左边界 - 1;
- 面积 = 高度 × 宽度。
Step 2️⃣:暴力法为何不行?
- 双重循环找左右边界 → O(n²);
- n = 1e5 时,操作次数 ≈ 1e10,必然超时。
Step 3️⃣:引入单调栈优化
- 利用栈的 LIFO 特性和单调性,在 O(1) 均摊时间内找到每个元素的左右边界;
- 总体时间复杂度降为 O(n) 。
Step 4️⃣:哨兵技巧简化边界处理
- 左哨兵:位置 -1,高度视为 -∞;
- 右哨兵:位置 n,高度视为 -∞;
- 避免额外判断边界条件。
Step 5️⃣:单次遍历 vs 两次遍历
- 方法一(官方):两次遍历分别求 left 和 right;
- 方法二(优化):一次遍历同时确定 left 和 right(出栈时确定右边界);
- 推荐使用方法二,代码更简洁,效率更高。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n) :每个元素入栈和出栈各一次,均摊 O(1) |
| 空间复杂度 | O(n) :需要 left、right 数组和栈,均为 O(n) |
| 是否原地 | ❌ 需要额外空间 |
| 面试高频点 | ✅ 单调栈模板、哨兵思想、边界处理、面积计算逻辑 |
💡 面试加分项:
- 能清晰解释“为什么用 >= 而不是 >”;
- 能手写单调栈模板;
- 能对比暴力法与优化法的复杂度差异;
- 能举一反三(如接雨水、最大矩形等类似题)。
💻 代码
✅ C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
vector<int> left(n), right(n, n); // right 默认为 n(右哨兵)
stack<int> mono_stack;
for (int i = 0; i < n; ++i) {
// 弹出所有高度 >= 当前高度的柱子,它们的右边界就是 i
while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
right[mono_stack.top()] = i;
mono_stack.pop();
}
// 栈空则左边界为 -1,否则为栈顶
left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
// 宽度 = right[i] - left[i] - 1
ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> h1 = {2,1,5,6,2,3};
cout << sol.largestRectangleArea(h1) << "\n"; // 输出: 10
vector<int> h2 = {2,4};
cout << sol.largestRectangleArea(h2) << "\n"; // 输出: 4
return 0;
}
✅ JavaScript 完整代码
/**
* @param {number[]} heights
* @return {number}
*/
var largestRectangleArea = function(heights) {
const n = heights.length;
const left = new Array(n);
const right = new Array(n).fill(n); // 右哨兵为 n
const stack = [];
for (let i = 0; i < n; i++) {
// 弹出所有高度 >= 当前高度的索引
while (stack.length > 0 && heights[stack[stack.length - 1]] >= heights[i]) {
const idx = stack.pop();
right[idx] = i;
}
// 左边界:栈空则为 -1,否则为栈顶
left[i] = stack.length === 0 ? -1 : stack[stack.length - 1];
stack.push(i);
}
let ans = 0;
for (let i = 0; i < n; i++) {
const width = right[i] - left[i] - 1;
ans = Math.max(ans, width * heights[i]);
}
return ans;
};
// 测试
console.log(largestRectangleArea([2,1,5,6,2,3])); // 10
console.log(largestRectangleArea([2,4])); // 4
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!