【Hot100】5.栈/队列
这一类的核心思想是:利用栈的“后进先出”特性,维护一个有序序列(单调栈)或处理嵌套结构。
- 单调栈:用于解决 “下一个更大/更小元素” (Next Greater/Smaller Element) 问题。通过维护一个单调递增或递减的栈,可以在 $O(N)$ 时间内找到每个元素左右两侧第一个比它大/小的元素。
- 辅助栈:用于处理括号匹配、表达式计算或需要回溯的场景。
- 单调队列:专门解决 滑动窗口最值 问题(如 239 题),队头始终是当前窗口的最值。
📝 本类题型总结
| 题目 | 栈/队列类型 | 关键差异点 (vs 每日温度) |
|---|---|---|
| 739. 每日温度 | 单调递减栈 | 基准:找下一个更大,存索引,弹栈时计算距离。 |
| 84. 最大矩形 | 单调递增栈 | 找左右第一个更小的。弹栈时计算 h * (右 - 左 - 1)。需补 0。 |
| 42. 接雨水 | 单调递减栈 | 找凹槽。弹栈底后,若栈不空,计算 min(左, 右) - 底 的水量。 |
| 20. 有效括号 | 普通辅助栈 | 不单调。左入右配,不匹配即 False,最后判空。 |
| 155. 最小栈 | 双栈同步 | 一个存数据,一个存当前最小值。Push/Pop 需同步。 |
| 239. 滑动窗口 | 单调递减队列 | 双端队列。队头存最大值。需处理窗口滑出(队头过期)逻辑。 |
🏆 母题:LeetCode 739. 每日温度 (Daily Temperatures)
题目内容:给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
核心逻辑:维护一个单调递减栈(栈底到栈顶温度递减),栈中存索引。
- 遍历温度,若当前温度
T[i]> 栈顶索引对应的温度T[stack.peek()]:- 说明找到了栈顶元素的“下一个更高温度”。
- 弹出栈顶
idx,计算天数i - idx,填入结果。 - 重复此过程直到栈空或当前温度不大于栈顶。
- 将当前索引
i入栈。
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
// ⚠️ [核心数据结构] 栈中存储的是【索引】,而非温度值
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// ⚠️ [核心逻辑] 维护单调递减栈
// 当当前温度 > 栈顶索引对应的温度时,说明找到了栈顶元素的“下一个更高温度”
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int idx = stack.pop();
res[idx] = i - idx; // 计算天数差
}
stack.push(i);
}
// 栈中剩余的元素表示后面没有更高的温度,res 默认为 0,无需处理
return res;
}
}
🔄 变体 1:LeetCode 84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)
题目内容:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
相比母题的变化:
- 栈的类型:母题是单调递减;本题需维护单调递增栈。
- 目的:找到每个柱子左边和右边第一个比它矮的柱子,从而确定以该柱子为高度的最大宽度。
- 计算时机:当遇到一个比栈顶矮的柱子
h[i]时,栈顶元素top的右边界确定了(即i),左边界是新的栈顶stack.peek()。- 宽度
w = i - stack.peek() - 1。 - 面积
area = heights[top] * w。
- 宽度
- 边界处理:为了处理最后栈中剩余的元素,通常在数组首尾各补一个 0。
- 头部补 0:避免栈空判断。
- 尾部补 0:强制弹出所有剩余元素进行计算。
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int largestRectangleArea(int[] heights) {
// ⚠️ [变化点 1] 构造新数组,首尾补 0
int[] newHeights = new int[heights.length + 2];
System.arraycopy(heights, 0, newHeights, 1, heights.length);
// newHeights[0] = 0; // 默认就是 0
// newHeights[newHeights.length - 1] = 0;
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0;
for (int i = 0; i < newHeights.length; i++) {
// ⚠️ [变化点 2] 维护单调递增栈
// 当当前高度 < 栈顶高度时,说明栈顶元素的右边界确定了
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
int h = newHeights[stack.pop()];
// ⚠️ [变化点 3] 计算宽度
// 右边界是 i (第一个比 h 小的),左边界是 stack.peek() (弹出后新的栈顶,也是比 h 小的)
// 宽度 = 右边界 - 左边界 - 1
int w = i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}
}
🔄 变体 2:LeetCode 42. 接雨水 (Trapping Rain Water) - 单调栈解法
题目内容:同前(双指针解法已讲)。这里展示单调栈解法。
相比母题的变化:
- 栈的类型:维护单调递减栈(存索引)。
- 目的:寻找“凹槽”。栈底到栈顶递减,说明在积累“左壁”。
- 计算逻辑:
- 当遇到一个比栈顶高的柱子
h[i]时,说明形成了一个凹槽。 - 弹出栈顶
bottom(凹槽底部)。 - 如果栈不空,新的栈顶
left是左壁,当前i是右壁。 - 高度
H = min(h[left], h[i]) - h[bottom]。 - 宽度
W = i - left - 1。 - 累加水量
H * W。
- 当遇到一个比栈顶高的柱子
- 区别:双指针是按列算水(关注左右最大值),单调栈是按层/按凹槽算水(关注局部低洼)。
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int trap(int[] height) {
Deque<Integer> stack = new ArrayDeque<>();
int water = 0;
for (int i = 0; i < height.length; i++) {
// ⚠️ [变化点 1] 维护单调递减栈
// 当当前高度 > 栈顶高度,说明可能形成凹槽
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop(); // 凹槽底部
// ⚠️ [变化点 2] 如果栈空,说明没有左壁,无法形成凹槽,直接跳过
if (stack.isEmpty()) break;
int left = stack.peek(); // 左壁索引
int right = i; // 右壁索引
// ⚠️ [变化点 3] 计算凹槽的水量
int h = Math.min(height[left], height[right]) - height[bottom];
int w = right - left - 1;
water += h * w;
}
stack.push(i);
}
return water;
}
}
🔄 变体 3:LeetCode 20. 有效的括号 (Valid Parentheses)
题目内容:给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串 s ,判断字符串是否有效。
相比母题的变化:
- 栈的用途:非单调栈。纯粹利用 LIFO 特性进行匹配抵消。
- 逻辑:
- 遇左括号:入栈。
- 遇右括号:检查栈顶是否为对应的左括号。
- 是:弹出栈顶(匹配成功)。
- 否:或栈空:直接返回
false。
- 结果:遍历结束后,栈必须为空。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
class Solution {
public boolean isValid(String s) {
// ⚠️ [变化点 1] 栈用于存储左括号,进行匹配
Deque<Character> stack = new ArrayDeque<>();
// 建立右括号到左括号的映射,方便检查
Map<Character, Character> map = new HashMap<>();
map.put(')', '(');
map.put(']', '[');
map.put('}', '{');
for (char c : s.toCharArray()) {
if (map.containsKey(c)) {
// ⚠️ [变化点 2] 遇到右括号
// 如果栈空 或 栈顶不匹配,则无效
if (stack.isEmpty() || stack.peek() != map.get(c)) {
return false;
}
stack.pop(); // 匹配成功,弹出
} else {
// 遇到左括号,入栈
stack.push(c);
}
}
// ⚠️ [变化点 3] 最终栈必须为空才有效
return stack.isEmpty();
}
}
🔄 变体 4:LeetCode 155. 最小栈 (Min Stack)
题目内容:设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
相比母题的变化:
- 数据结构:使用双栈。
dataStack:存储所有数据。minStack:同步存储当前状态下的最小值。
- Push 逻辑:
dataStack正常压入。minStack压入min(x, minStack.peek())(如果minStack为空则直接压入x)。
- Pop 逻辑:两个栈同时弹出。
import java.util.ArrayDeque;
import java.util.Deque;
class MinStack {
private Deque<Integer> dataStack;
private Deque<Integer> minStack;
public MinStack() {
dataStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
dataStack.push(val);
// ⚠️ [变化点 1] 维护最小值栈
// 如果 minStack 为空,或者当前值 <= 栈顶最小值,则压入当前值
// 注意:这里必须用 <=,保证如果有多个相同的最小值,pop 时能正确对应
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
} else {
// 否则,重复压入当前的最小值,保持两个栈大小一致
minStack.push(minStack.peek());
}
}
public void pop() {
// ⚠️ [变化点 2] 双栈同时弹出
if (!dataStack.isEmpty()) {
dataStack.pop();
minStack.pop();
}
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
🔄 变体 4:LeetCode 239. 滑动窗口最大值 (Sliding Window Maximum)
题目内容:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
核心逻辑(单调队列):
- 数据结构:使用
Deque(双端队列),而非普通栈。 - 单调性:维护一个单调递减队列。队头始终是当前窗口的最大值。
- 操作:
- 入队:新元素进入窗口前,从队尾开始,把所有比它小的元素都弹出(因为它们不可能再成为最大值了),然后新元素入队尾。
- 出队:检查队头元素是否已滑出窗口(索引
<= i - k),如果是,则从队头弹出。 - 取值:队头即为当前窗口最大值。
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) return new int[0];
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
// ⚠️ [核心数据结构] 双端队列,存储【索引】
// 队列保持单调递减:队头是最大的元素索引
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 1. 维护单调性:移除队尾所有小于当前元素的索引
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
// 2. 移除过期元素:如果队头索引已经滑出窗口 (i - k),则移除
if (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 3. 记录结果:当窗口形成 (i >= k - 1) 时,队头即为最大值
if (i >= k - 1) {
res[idx++] = nums[deque.peekFirst()];
}
}
return res;
}
}

浙公网安备 33010602011771号