文章目录
- 单调栈
-
- [LeetCode 739.每日温度](#LeetCode 739.每日温度)
- [LeetCode 84.柱状图中最大的矩形](#LeetCode 84.柱状图中最大的矩形)
单调栈
单调栈:只保留**"有潜力成为答案"**的数据。
一旦出现一个"矮个子",它就会把前面所有"比它高的"全部杀掉,维护一个从矮到高的优良序列。
这样每个元素最多进一次、死一次,效率 O(N)。
所以,单调栈本质上不是为了"存储",而是为了**"维护一种有序性",从而实现O(1) 时间找到最近的极值**。
模式识别:一旦题目让你在数组里找"左边/右边第一个比我大/小的元素",立刻想到单调栈。
复杂度:每个元素最多进栈一次、出栈一次,所以时间复杂度是 O(N)。
LeetCode 739.每日温度

栈中记录还没算出下一个更大元素的那些数的下标。
相当于栈是一个 todolist,在循环的过程中,现在还不知道答案是多少,在后面的循环中会算出答案。
java
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
Deque<Integer> st = new ArrayDeque<>();
for(int i = 0; i < n; i++){
int t = temperatures[i];
while(!st.isEmpty() && t > temperatures[st.peek()]){
int j = st.pop();
ans[j] = i - j;
}
st.push(i);
}
return ans;
}
}
LeetCode 84.柱状图中最大的矩形

java
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// left[i] 存的是:第 i 根柱子左边第一个比它矮的柱子的下标
int[] left = new int[n];
// right[i] 存的是:第 i 根柱子右边第一个比它矮的柱子的下标
int[] right = new int[n];
//维护一个从栈底到栈顶高度递增的栈(因为每次都是踢掉高的,留下矮的)
Deque<Integer> st = new ArrayDeque<>();
// 第一步:从左往右遍历,给每个i找左边界 (Left Less Element)
//把比自己高和和自己一样高的踢掉
for(int i = 0; i < n; i++){
while(!st.isEmpty() && heights[st.peek()] >= heights[i]){
st.pop();
}
// 踢完高个子后:
// 1. 如果栈空了:说明左边没有任何柱子比我矮,我的左边界延伸到了无穷远(即下标 -1)。
// 2. 如果栈不空:栈顶剩下的那个就是"第一个比我矮"的家伙,它就是我的左边界。
left[i] = st.isEmpty()?-1:st.peek();
// 把当前柱子加入栈,作为后面柱子的潜在边界
st.push(i);
}
// 第二步:从右往左遍历,找右边界 (Right Less Element)
st.clear(); // 【重要】一定要记得清空栈,复用它
for(int i = n-1; i>=0; i--){//注意这里是--
while(!st.isEmpty() && heights[st.peek()] >= heights[i]){
st.pop();
}
// 1. 如果栈空了:说明右边没有比我矮的,边界延伸到数组最右端之外(即下标 n)。
// 2. 如果栈不空:栈顶就是右边第一个比我矮的。
right[i] = st.isEmpty() ? n: st.peek();
st.push(i);
}
// 第三步:遍历每根柱子,计算以它为高度的最大矩形面积
int ans = 0;
for(int i = 0; i < n; i++){
int h = heights[i];// 矩形高度:取决于当前柱子
// 矩形宽度:右边界 - 左边界 - 1
// 举例:左边界是下标 1,右边界是下标 5。
// 也就是中间夹着的下标是 2, 3, 4。
// 宽度 = 5 - 1 - 1 = 3。
int w = right[i] - left[i] -1;
ans = Math.max(ans,h*w);
}
return ans;
}
}
单调栈解法,维护一个从栈底到栈顶高度递增的栈(因为每次都是踢掉高的,留下矮的)。
踢掉无用数据,保持栈中有序

柱状图中最大的矩形不能直接用(接雨水那种)前后缀方法做,因为逻辑完全不同。
虽然这两道题都需要"往左看"和"往右看",也都可以用 单调栈 ,但在使用 前后缀数组 (Prefix/Suffix Array) 这个思路上,它们有本质的区别。
1. 接雨水 (Trapping Rain Water)
核心逻辑 :"我不关心旁边的人是谁,我只关心这一侧最高的那个墙有多高。"
- 问题 :当前位置
i能存多少水? - 公式 :
Water[i] = min(左边最高的墙, 右边最高的墙) - height[i] - 关键点 :这是一个累积 (Cumulative) 的概念。
- 只要左边有一个 100 米高的墙,不管它离我多远,它都能帮我挡水。
- 前后缀解法 :
leftMax[i]:从 0 到i的最大值。(简单的动态规划/一次遍历)rightMax[i]:从n-1到i的最大值。- 不需要栈,只需要比大小。
java
// 接雨水的前后缀解法(不需要栈)
leftMax[i] = Math.max(leftMax[i-1], height[i]); // 只关心最大的
2. 柱状图最大矩形 (Largest Rectangle)
核心逻辑 :"我非常关心旁边的人是谁,只要遇到一个比我矮的,我就完蛋了(断开了)。"
- 问题 :以当前高度
height[i]为矩形高度,能向两边延伸多远? - 关键点 :这是一个最近邻 (Nearest Neighbor) 的概念。
- 哪怕左边远方有一个 100 米高的墙,只要我紧挨着的左边是个 1 米的矮子,我就伸不过去,远处的 100 米对我毫无意义。
- 为什么前后缀(累积最大/最小)不管用?
- 如果你存"左边最大的",没用,挡不住我。
- 如果你存"左边最小的",也没用,那是最矮的板,但我可能被中间某个"次矮"的挡住。
- 解法 :必须找到离我最近的、比我小的 那个下标。
- 这就是 单调栈 的定义:专门用来找 Next/Previous Smaller/Greater Element。
3. 图解对比
假设数组:[5, 6, 2, 6, 5],我们看中间的 2。
接雨水视角(找靠山):
- 左边最高是 6 ,右边最高是 6。
- 所以我(2)上面能存
min(6, 6) - 2 = 4的水。 - 逻辑:找极值(Max/Min)。
柱状图视角(找边界):
- 想以 2 为高度做矩形。
- 左边是 6 (比我高,通过) -> 5 (比我高,通过) -> 边界。
- 右边是 6 (比我高,通过) -> 5 (比我高,通过) -> 边界。
- 逻辑 :找最近的坏人(第一个比我小的)。
- 总结:什么时候用什么?
| 特性 | 接雨水 (Rain Water) | 柱状图 (Histogram) |
|---|---|---|
| 关注点 | 这一侧最高的墙 (Cumulative Max) | 离我最近的比我矮的墙 (Nearest Smaller) |
| 受阻条件 | 只有比当前水位更高的墙才重要 | 只要比我矮,马上截断 |
| 前后缀数组 | 可以用 (简单遍历取Max) | 不能直接用 (逻辑不支持) |
| 单调栈 | 可以用 (找凹槽) | 必须用 (找左右边界) |
你的"错觉"来源
你刚才写的那个代码里,确实定义了 left[] 和 right[] 两个数组。
但这不是 传统意义上的"前后缀数组"(像接雨水那样 max(prev, curr) 算出来的)。
这两个数组是依靠单调栈算出来的"索引数组"。
结论:
- 如果面试问"这题能用前后缀做吗?" -> 回答:不能,必须用单调栈。
- 如果面试问"接雨水能用前后缀做吗?" -> 回答:可以,那是接雨水的标准解法之一(双指针其实就是前后缀的空间优化版)。