单调栈模板:一眼识别“下一个更大元素”类问题#
很多同学第一次看到单调栈时会觉得它很神秘,其实它的本质非常朴素:
用一个“有序”的栈,帮我们快速找到某个元素左边或右边第一个更大 / 更小的位置。
只要题目出现“下一个更大元素”“下一个更小元素”“最近更大”“最近更小”“以当前元素为高/低点扩展区间”,单调栈基本就该上场了。
⚡ 速记版模板#
// 模板用途:在线性扫描中,快速确定元素的“最近更大/更小”位置
Deque<Integer> stack = new ArrayDeque<>(); // 单调栈:通常存下标,便于算距离和定位答案
for (int index = 0; index < nums.length; index++) { // 线性扫描每个元素
while (!stack.isEmpty() && nums[index] > nums[stack.peek()]) { // 当前值更大,说明栈顶元素“右侧第一个更大值”已出现
int prevIndex = stack.pop(); // 取出等待结算的下标
answer[prevIndex] = index; // 记录其答案(这里记录位置;有些题记录值或距离)
}
stack.push(index); // 当前下标入栈,等待未来更大元素来结算
}java🎯 什么时候想到单调栈?#
典型信号:
- 问右侧第一个更大 / 更小元素
- 问左侧第一个更大 / 更小元素
- 问每个元素作为最小值或最大值时的影响范围
- 需要把暴力的“向两边扩展”优化掉
Hot 100 里的典型题目:
💡 核心思路#
单调栈通常存的是 下标,不是值本身。这样可以同时拿到:
- 当前元素值:
nums[index] - 元素位置
- 两个位置之间的距离
常见两种栈:
-
单调递减栈
- 栈顶到栈底递减
- 常用于找“下一个更大元素”
-
单调递增栈
- 栈顶到栈底递增
- 常用于找“下一个更小元素”
🚀 模板一:找右侧第一个更大元素#
class Solution {
public int[] nextGreaterElement(int[] nums) {
int length = nums.length; // 数组长度
int[] answer = new int[length]; // answer[i]:nums[i] 右侧第一个更大元素值
Arrays.fill(answer, -1); // 默认不存在更大元素时返回 -1
Deque<Integer> stack = new ArrayDeque<>(); // 维护“值单调递减”的下标栈
for (int index = 0; index < length; index++) { // 从左到右扫描
while (!stack.isEmpty() && nums[index] > nums[stack.peek()]) { // 一旦当前值更大,可结算一批栈顶元素
int prevIndex = stack.pop(); // 被结算元素的下标
answer[prevIndex] = nums[index]; // 当前值就是它右侧第一个更大值
}
stack.push(index); // 当前下标入栈,后续等待更大元素
}
return answer; // 返回全部位置的结算结果
}
}java这个模板里:
- 栈里维护一个递减序列
- 当前元素一旦更大,就可以结算栈顶元素的答案
🚀 模板二:找左右边界#
很多题并不直接问“下一个更大元素是谁”,而是问当前元素能向左右扩多远。
class Solution {
public int[][] nearestSmaller(int[] nums) {
int length = nums.length; // 元素个数
int[] left = new int[length]; // left[i]:i 左侧最近更小元素下标,不存在为 -1
int[] right = new int[length]; // right[i]:i 右侧最近更小元素下标,不存在为 length
Arrays.fill(left, -1); // 初始化左边界哨兵
Arrays.fill(right, length); // 初始化右边界哨兵
Deque<Integer> stack = new ArrayDeque<>(); // 维护单调递增栈(按值)
for (int index = 0; index < length; index++) { // 扫描每个位置
while (!stack.isEmpty() && nums[index] < nums[stack.peek()]) { // 当前值更小,触发栈顶元素右边界结算
right[stack.pop()] = index; // 栈顶元素右侧最近更小下标就是当前 index
}
if (!stack.isEmpty()) { // 栈顶是当前元素左侧最近且更小的候选
left[index] = stack.peek(); // 记录左边界
}
stack.push(index); // 当前元素入栈,供后续元素结算
}
return new int[][]{left, right}; // 同时返回左右最近更小边界
}
}java适用场景:
- 柱状图最大矩形
- 子数组最小值 / 最大值贡献问题
- 需要知道某个元素的影响范围
🚀 模板三:单调栈 + 面积计算#
以柱状图最大矩形为代表:
class Solution {
public int largestRectangleArea(int[] heights) {
int length = heights.length; // 柱子数量
Deque<Integer> stack = new ArrayDeque<>(); // 维护“高度递增”的下标栈
int maxArea = 0; // 记录最大矩形面积
for (int index = 0; index <= length; index++) { // 多跑一步用于放置结尾哨兵
int currentHeight = index == length ? 0 : heights[index]; // 哨兵高度 0 强制清空栈并结算剩余柱子
while (!stack.isEmpty() && currentHeight < heights[stack.peek()]) { // 当前柱子更矮,说明栈顶柱子右边界确定
int height = heights[stack.pop()]; // 以被弹出柱子的高度作为矩形高
int leftIndex = stack.isEmpty() ? -1 : stack.peek(); // 弹出后新的栈顶是其左侧第一个更矮柱子
int width = index - leftIndex - 1; // 宽度 = 右边界(不含) - 左边界(不含)
maxArea = Math.max(maxArea, height * width); // 更新最大面积
}
stack.push(index); // 当前柱子下标入栈
}
return maxArea; // 返回最大矩形面积
}
}java这里额外加一个结尾高度 0,是为了把栈里剩下的柱子统一结算干净。
🧠 常见题目怎么套?#
1. 每日温度#
- 维护一个递减栈
- 当前温度更高时,栈顶那天就找到了答案
- 答案是下标差值
2. 柱状图最大矩形#
- 维护一个递增栈
- 当前高度更小时,说明某些柱子的右边界确定了
- 计算以出栈柱子为高的最大面积
3. 接雨水#
这题可用双指针,也可用单调栈:
- 当前柱子更高时,可以和栈里的“凹槽”形成积水区间
⚠️ 易错点#
-
栈里存值还是下标
- 绝大多数题都建议存下标
-
大小关系写错
- 是
>还是>= - 是
<还是<= - 取决于你想保留相等元素还是合并相等元素
- 是
-
最后一批元素没结算
- 柱状图类题常常要补一个哨兵
0
- 柱状图类题常常要补一个哨兵
-
宽度公式容易错
- 通常是
right - left - 1
- 通常是
🎨 面试时怎么说#
你可以这样说:
我用单调栈维护一组还没有确定答案的下标,一旦当前元素破坏了栈的单调性,就说明栈顶元素的右侧边界已经找到了,可以立刻结算答案。
📌 一句话总结#
单调栈的核心不是栈,而是:
- 帮你维护一个“还没结算答案”的候选集合
- 一旦出现更大 / 更小元素,就批量结算