一、单调栈是什么?用排队来理解
先看一个生活场景:
你站在一排人中,从前往后看,只能看到比你矮的人。
身高:6 3 1 4 5
位置:0 1 2 3 4
从右往左看(站在最右边往左看):
站在位置4,身高5 → 往左看,能看到 1, 3, 4
但看不到 6(被5挡住了)
单调栈就是维护这样一个"从某个方向看,高度递增/递减"的结构。
核心思想:
遍历数组时,维护一个栈,栈中的元素始终保持单调递增(或递减)。当新元素不满足单调性时,弹出栈顶元素,处理它与新元素的关系。
二、核心原理
2.1 为什么叫"单调"?
栈中元素始终保持单调递增 或单调递减:
单调递增栈(从栈底到栈顶递增):
栈底 → [1, 3, 5, 7] ← 栈顶
新元素 4 来了 → 7 和 5 比 4 大,弹出 7 和 5
栈变成 → [1, 3, 4] ← 满足单调递增 ✅
单调递减栈(从栈底到栈顶递减):
栈底 → [7, 5, 3, 1] ← 栈顶
新元素 4 来了 → 1 比 4 小,弹出 1
栈变成 → [7, 5, 3, 4] ← 满足单调递减 ✅
2.2 求什么决定了用哪种栈
┌───────────────────────────────────────────────────────────┐
│ 问题类型 │ 栈的类型 │ 弹出时的含义 │
├────────────────────────┼────────────────┼───────────────────┤
│ 找下一个更大元素 │ 单调递减栈 │ 弹出=找到了更大值 │
│ 找下一个更小元素 │ 单调递增栈 │ 弹出=找到了更小值 │
│ 找左边第一个更大/小 │ 从左往右遍历 │ 弹出=当前元素是答案 │
│ 找右边第一个更大/小 │ 从右往左遍历 │ 弹出=当前元素是答案 │
└────────────────────────┴────────────────┴───────────────────┘
三、通用模板代码
java
/**
* 单调栈模板:找每个元素右边第一个更大的元素
* 如果要找更小的,把 while 里的 > 改成 <
*/
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1); // 默认没有更大元素
// 单调递减栈:存储元素的下标
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 当前元素比栈顶大 → 栈顶找到了"下一个更大元素"
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
int idx = stack.pop(); // 弹出栈顶
result[idx] = nums[i]; // 当前元素就是栈顶的下一个更大元素
}
stack.push(i); // 当前元素入栈
}
return result;
}
四、经典题1:下一个更大元素I
LeetCode 496 --- 下一个更大元素 I
给定 nums1 和 nums2,对于 nums1 中的每个元素,找出在 nums2 中对应位置的下一个更大元素。
4.1 思路
经典的"下一个更大元素"问题。用单调递减栈,从左到右遍历 nums2。
4.2 图解过程
nums2 = [2, 1, 2, 4, 3], nums1 = [2, 4]
用 HashMap 先记录 nums2 中每个元素的「下一个更大值」
最后根据 nums1 查 HashMap
遍历 nums2:
i=0, nums2[0]=2
栈: [2] (存下标)
i=1, nums2[1]=1
1 < 2,不弹栈
栈: [2, 1]
i=2, nums2[2]=2
2 > 1 → 弹出1,result[1]=2 ← 元素1的下一个更大是2
2 不大于 2,停止
栈: [2, 2]
i=3, nums2[3]=4
4 > 2 → 弹出2(idx=2),result[2]=4
4 > 2 → 弹出2(idx=0),result[0]=4
栈: [4]
i=4, nums2[4]=3
3 < 4,不弹栈
栈: [4, 3]
最终 map: {2→4, 1→2}
nums1[0]=2 → 4
nums1[1]=4 → -1(没有更大元素)
4.3 完整代码
java
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 第一步:遍历 nums2,用单调栈找出每个元素的下一个更大值
Map<Integer, Integer> nextGreater = new HashMap<>();
Deque<Integer> stack = new ArrayDeque<>();
for (int num : nums2) {
while (!stack.isEmpty() && num > stack.peek()) {
nextGreater.put(stack.pop(), num);
}
stack.push(num);
}
// 剩余栈中元素没有下一个更大值(默认-1)
// 第二步:根据 nums1 查询
int[] result = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
result[i] = nextGreater.getOrDefault(nums1[i], -1);
}
return result;
}
}
五、经典题2:柱状图中最大矩形
LeetCode 84 --- 柱状图中最大矩形
给定 n 个非负整数表示柱状图中各个柱子的高度,求最大矩形面积。
5.1 思路
关键思路:对于每根柱子,找到它能向左右延伸的最大宽度(即找到左边和右边第一个比它矮的柱子)。
这正好是"找下一个更小元素",用单调递增栈。
5.2 图解过程
heights = [2, 1, 5, 6, 2, 3]
柱状图:
┌──┐
┌──┤ │
┌─┤ │ │ ┌──┐
│ │ │ │ │ │
┌─┤ │ │ │ │ │
│ │ │ │ │ │ │
2 1 5 6 2 3
关键:每根柱子的矩形面积 = 高度 × 能延伸的最大宽度
柱子0(高2): 左边界=-1, 右边界=1 → 宽=1-(-1)-1=1 → 面积=2×1=2
柱子1(高1): 左边界=-1, 右边界=6 → 宽=6-(-1)-1=6 → 面积=1×6=6
柱子2(高5): 左边界=1, 右边界=4 → 宽=4-1-1=2 → 面积=5×2=10
柱子3(高6): 左边界=2, 右边界=4 → 宽=4-2-1=1 → 面积=6×1=6
柱子4(高2): 左边界=1, 右边界=6 → 宽=6-1-1=4 → 面积=2×4=8
柱子5(高3): 左边界=4, 右边界=6 → 宽=6-4-1=1 → 面积=3×1=3
最大面积 = 10(柱子2)
5.3 完整代码
java
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int maxArea = 0;
// 单调递增栈,存下标
Deque<Integer> stack = new ArrayDeque<>();
// 在数组前后各加一个高度为0的哨兵,保证所有柱子都能被弹出
int[] newHeights = new int[n + 2];
System.arraycopy(heights, 0, newHeights, 1, n);
// newHeights[0] 和 newHeights[n+1] 默认为0
for (int i = 0; i < newHeights.length; i++) {
// 当前柱子比栈顶矮 → 栈顶找到了右边界
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
int height = newHeights[stack.pop()]; // 弹出的柱子高度
int left = stack.peek(); // 左边界(栈中上一个元素)
int right = i; // 右边界(当前元素)
int width = right - left - 1; // 宽度
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
}
六、经典题3:接雨水(单调栈解法)
LeetCode 42 --- 接雨水
给定 n 个非负整数表示柱子高度,计算能接多少雨水。
6.1 与双指针解法对比
之前的文章用双指针解过这道题。双指针是按列计算 ,单调栈是按行计算:
双指针:看每根柱子上方能装多少水(纵向计算)
单调栈:看每层能装多少水(横向计算)
↓ 这一层的水
┌─────┐
│ 水 │
┌───┤ 水 ├───┐
│ │ 水 │ │
│ └─────┘ │
└───────────────┘
6.2 单调栈思路
用单调递减栈存储柱子下标。当遇到比栈顶高的柱子时:
- 弹出栈顶(凹槽底部)
- 新的栈顶是左边界
- 当前元素是右边界
- 水的宽度 = right - left - 1
- 水的高度 = min(heightsleft, heightsright) - heights凹槽
6.3 图解
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
|
| | | |
| | | | | |
__|_|_|_|___|_|_|_|__
关键过程:
当 i=3, height=2 时:
弹出 i=2(高0), 左边界=1(高1), 右边界=3(高2)
水 = min(1,2) - 0 = 1, 宽 = 3-1-1 = 1
接水 = 1 × 1 = 1
当 i=7, height=3 时:
弹出 i=6(高1), 左边界=5(高0)→继续弹出
弹出 i=5(高0), 左边界=3(高2), 右边界=7(高3)
水 = min(2,3) - 0 = 2, 宽 = 7-3-1 = 3
接水 = 2 × 3 = 6
6.4 完整代码
java
class Solution {
public int trap(int[] height) {
int totalWater = 0;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < height.length; i++) {
// 单调递减栈:遇到更高的柱子就开始计算
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop(); // 凹槽底部
if (stack.isEmpty()) break; // 没有左边界,无法形成凹槽
int left = stack.peek(); // 左边界
int right = i; // 右边界
int waterHeight = Math.min(height[left], height[right]) - height[bottom];
int waterWidth = right - left - 1;
totalWater += waterHeight * waterWidth;
}
stack.push(i);
}
return totalWater;
}
}
6.5 两种解法对比
| 对比项 | 双指针解法 | 单调栈解法 |
|---|---|---|
| 思路 | 按列纵向算 | 按行横向算 |
| 时间 | O(n) | O(n) |
| 空间 | O(1) | O(n) |
| 理解难度 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 适用性 | 简单场景 | 更通用 |
七、单调栈 vs 单调队列
┌──────────────────────────────────────────────────────┐
│ 单调栈 vs 单调队列 │
├───────────┬────────────────┬─────────────────────────┤
│ 对比项 │ 单调栈 │ 单调队列 │
├───────────┼────────────────┼─────────────────────────┤
│ 数据结构 │ Stack(后进先出)│ Deque(先进先出) │
│ 典型场景 │ 找下一个更大/小 │ 滑动窗口最大/最小值 │
│ 遍历方向 │ 通常单向 │ 窗口两端 │
│ 代表题目 │ LeetCode 84/42 │ LeetCode 239 │
│ 核心操作 │ 弹出不满足条件的 │ 弹出过期的 + 不满足的 │
│ Java实现 │ ArrayDeque │ ArrayDeque │
└───────────┴────────────────┴─────────────────────────┘
💡 总结 :单调栈的核心就一句话------维护一个有序的结构,弹出的那一刻就是找到答案的时刻。记住模板,找到"找下一个XX"的关键词,直接套。
如果觉得有帮助,点赞 + 收藏支持一下!有问题欢迎评论区讨论 💬