从零开始写算法-栈-柱状图中最大的矩形
一、题目描述
题目来源:LeetCode 84. Largest Rectangle in Histogram
给定 n 个非负整数,表示直方图的柱状高度。
每个柱子的宽度为 1,求能形成的最大矩形面积。
示例:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大矩形面积是由高度 5 和 6 构成的矩形(宽度为 2)。
二、思路分析
这个题看起来像简单的“找最大面积”,但其实是典型的 单调栈应用题。
我们需要为每个柱子找到:
它左边第一个比它矮的柱子;
它右边第一个比它矮的柱子。
这样,它所能扩展的最大矩形就确定了。
直观解释
假设我们有一组高度:[2, 1, 5, 6, 2, 3]
我们从左往右遍历,用一个 单调递增栈(存下标) 维护柱子的高度顺序:
当当前柱子比栈顶“高”时,说明还可以往右延伸,入栈。
当当前柱子比栈顶“矮”时,说明栈顶那根柱子的“右边界”已经确定——此时就可以计算它的面积。
换句话说:
入栈延迟计算,出栈触发计算。
三、核心逻辑讲解
我们维护一个递增栈 st,存放的是柱子的下标:
当
heights[i] >= heights[st.top()]时 —— 入栈;当
heights[i] < heights[st.top()]时 ——
说明栈顶柱子的右边界被确定了,此时:弹出栈顶下标
idx;此柱子的高度是
heights[idx];弹出后的栈顶是“左边第一个比它矮的柱子”;
当前
i是“右边第一个比它矮的柱子”;所以矩形的宽度 =
i - st.top() - 1;面积 =
heights[idx] * width。
遍历结束后,为了清空栈中的剩余柱子,我们可以在数组末尾添加一个哨兵
-1。
四、代码实现(cpp)
class Solution {
public:int largestRectangleArea(vector<int>& heights) {// 思路:// 暴力解法:对每个柱子,向左右扫描找到第一个比它矮的柱子,确定宽度后计算面积。时间复杂度 O(n^2)。// 优化1:可以用两个数组或栈,分别预处理出每个柱子左边第一个更矮的位置和右边第一个更矮的位置,从而在 O(n) 时间内计算出所有面积。// 优化2(最终方案):使用单调递增栈,只遍历一次,通过“出栈时计算面积”的方式同时确定左右边界,时间复杂度降为 O(n),空间 O(n)。/*• idx:是 当前出栈的柱子的下标。• heights[idx]:就是该柱子的高度(矩形高度)。• i:是右边第一个比它矮的柱子的下标(右边界)。• left:是左边第一个比它矮的柱子的下标(左边界)。• (i - left - 1):就是该矩形的宽度。• 栈中存的是下标(index)• 栈中对应的高度是单调递增的(从栈底到栈顶)• 每次遇到“比栈顶矮”的柱子时,就说明我们找到了栈顶柱子的「右边第一个比它矮」的位置。*/stack<int> st;int n = heights.size();heights.push_back(-1); // 目的是为了最后清空栈,执行所有的遍历,因为只有在“当前柱子比栈顶柱子矮”时,才会触发出栈计算面积。int ans = 0;for (int i = 0; i <= n; ++i) {while (!st.empty() && heights[i] < heights[st.top()]){ // 确保栈是递增的,且栈顶要比我当前这个高度小int idx = st.top();st.pop();int left = st.empty()? -1 : st.top();ans = max(ans, (i - left - 1) * heights[idx]); // 注意:我们在这里不是在计算「当前柱子 i」的面积,而是在计算「被弹出的柱子 idx」能延伸的最大矩形面积。}st.push(i);}return ans;}
};五、举例说明(关键理解点)
以 heights = [2,1,5,6,2,3] 为例:
| 步骤 | 当前 i,h | 栈内容 | 弹出 | 面积计算 | 当前最大面积 |
|---|---|---|---|---|---|
| i=0,h=2 | 入栈 | [0] | - | - | 0 |
| i=1,h=1 | 1 < 2 → 弹出0 | [ ] | idx=0 | (i - (-1) -1)*2=2 | 2 |
| i=1,h=1 | 入栈 | [1] | - | - | 2 |
| i=2,h=5 | 入栈 | [1,2] | - | - | 2 |
| i=3,h=6 | 入栈 | [1,2,3] | - | - | 2 |
| i=4,h=2 | 2<6 弹出3 | [1,2] | idx=3 | (4-2-1)*6=6 | 6 |
| i=4,h=2 | 2<5 弹出2 | [1] | idx=2 | (4-1-1)*5=10 | 10 ✅ |
| i=4,h=2 | 入栈 | [1,4] | - | - | 10 |
| i=5,h=3 | 入栈 | [1,4,5] | - | - | 10 |
| i=6,h=-1 | 触发清栈 | 弹出5,4,1... | - | ... | 10 |
最终最大面积 = 10
六、关键点总结
✅ 为什么要加一个 -1?
为了强制让栈里的柱子全部弹出,计算剩余面积。
✅ 为什么要乘的是 heights[idx] 而不是当前的?
因为当前柱子是“右边界”,被弹出的才是“矩形高度的主体”。
✅ 为什么在弹出时计算面积?
弹出时说明边界确定了,矩形的左右范围都清楚,正是计算的最佳时机。
七、单调栈通用思维模型
单调栈思想:维持有序,弹出计算。
常见应用:
| 题目类型 | 栈顺序 | 计算时机 |
|---|---|---|
| 柱状图最大矩形 | 单调递增 | 弹出时计算面积 |
| 每日温度 | 单调递减 | 弹出时计算间隔 |
| 下一个更大元素 | 单调递减 | 弹出时确定答案 |
八、总结一句话记忆:
单调栈的精髓是:
「入栈延迟计算,出栈触发计算」。
只要掌握这个节奏,柱状图类题目再也不怕!
