Skip to content

栈与队列基础与高频技巧

栈与队列 ⭐⭐ 中级 🔥 高频

💡 核心要点

栈处理"最近、配对、回退"关系,队列处理"按层、按序推进"。当需要在线维护最值时,引入单调栈(找下一个更大/更小元素)和单调队列(滑动窗口最值)。


识别信号

看到以下关键词,优先考虑栈或队列:

  • "括号匹配 / 嵌套结构 / 配对" → 栈
  • "下一个更大 / 下一个更小元素" → 单调栈
  • "柱状图面积 / 接雨水" → 单调栈(找左右边界)
  • "滑动窗口最大值 / 最小值" → 单调队列
  • "层序遍历 / BFS" → 队列
  • "表达式求值 / 计算器 / 逆波兰" → 栈

反例(不适用): 全局排序、二分查找、DP 状态转移——这些与"相邻关系"无关,不需要单调结构。


通用思考框架

第一步:判断核心关系是"最近回退"还是"先进先出"

关系特征数据结构典型场景
"最近的、配对的、嵌套的"括号匹配、DFS、表达式求值
"按层、按序、先到先处理"队列BFS、任务调度

判断技巧: 问自己"当我处理当前元素时,我需要回头看最近的那个,还是最早的那个?"最近的 → 栈;最早的 → 队列。

第二步:判断是否需要单调结构

普通栈  → 括号匹配、表达式求值、DFS 非递归
单调栈  → "下一个更大/更小元素"、柱状图面积(需要找左右边界)
普通队列 → BFS 层序遍历
单调队列 → 滑动窗口最大/最小值

单调结构的本质: 维护一个"有用候选者"不变量。以单调递减栈为例,栈中保留的都是"还没找到右边更大元素的候选者"——一旦当前元素比栈顶大,栈顶的答案就确定了,可以出栈。这个不变量保证了每个元素最多入栈出栈各一次,总体 O(n)。

第三步:确定栈/队列中存什么

  • 存下标(推荐):当需要计算距离(如天数差、宽度)时,或需要通过下标访问原数组值时。
  • 存值:仅当只需要比较大小、不需要位置信息时才考虑。

单调栈的不变量:

  • 单调递减栈(从底到顶):用于找"下一个更大元素",栈顶是当前最小候选者。
  • 单调递增栈(从底到顶):用于找"下一个更小元素",栈顶是当前最大候选者。

触发弹出的条件 / 弹出时做什么:

  • 弹出条件:当前元素 > 栈顶对应值(递减栈)
  • 弹出时:栈顶元素的答案就是当前元素(或当前下标减去栈顶下标)

第四步:确定遍历方向

目标遍历方向原因
找右边第一个更大元素从左到右当前元素是"右边的那个更大值"
找左边第一个更大元素从右到左当前元素是"左边的那个更大值"
环形数组遍历两遍(或取模)模拟"绕一圈"

代码基础:栈的用法

java
// Java 中推荐用 ArrayDeque,而不是 Stack(Stack 继承 Vector,方法有 synchronized,单线程下性能差)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(val);           // 入栈(压栈顶)
int top = stack.peek();    // 查看栈顶,不弹出
int val = stack.pop();     // 出栈
boolean empty = stack.isEmpty();

代码模板:单调栈(找下一个更大元素)

java
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++) {
        // 当前元素比栈顶大 → 栈顶的"下一个更大元素"就是 nums[i]
        while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
            result[stack.pop()] = nums[i];
        }
        stack.push(i);
    }
    // 循环结束后栈中剩余的下标:右边没有更大元素,result 保持 -1
    return result;
}

代码模板:单调队列(滑动窗口最大值)

java
public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    int[] result = new int[n - k + 1];
    Deque<Integer> deque = new ArrayDeque<>();  // 存下标,队列单调递减

    for (int i = 0; i < n; i++) {
        // 移除超出窗口左边界的元素(从队首)
        while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
            deque.pollFirst();
        }
        // 维护单调递减:移除所有比当前值小的元素(从队尾),它们不可能是窗口最大值
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
            deque.pollLast();
        }
        deque.offerLast(i);
        // 窗口形成后,队首就是当前窗口最大值的下标
        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    return result;
}

典型例题:每日温度(LeetCode 739)

题目链接

给定数组 temperatures,返回数组 answer,其中 answer[i] 是第 i 天之后需要等几天才能等到更暖的温度。如果之后没有更暖的日子,answer[i] = 0

1. 识别

"等到更暖的温度" = "找右边第一个比当前温度更大的元素" → 单调栈模板题。

2. 栈中存什么

需要计算天数差(下标差),所以存下标。栈维护单调递减的温度序列(从底到顶)。

3. 何时弹出

temperatures[i] > temperatures[stack.peek()]:当前温度比栈顶温度高,说明栈顶那天等到了更暖的日子。

4. 弹出时做什么

设栈顶下标为 j,则 answer[j] = i - j(等了 i - j 天)。

5. 完整代码

java
public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] answer = new int[n];
    Deque<Integer> stack = new ArrayDeque<>();  // 存下标

    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
            int j = stack.pop();
            answer[j] = i - j;  // 等了 i-j 天
        }
        stack.push(i);
    }
    // 栈中剩余的下标:之后没有更暖的天,answer 保持默认值 0
    return answer;
}

时间复杂度: O(n),每个元素最多入栈出栈各一次,均摊 O(1)。

空间复杂度: O(n)。


延伸题目

题号题名难度关键差异一句话提示
20有效的括号简单普通栈,配对匹配入栈对应的右括号,遇右括号直接 pop 比较
155最小栈中等辅助栈同步记录最小值两个栈:一个正常栈,一个记录当前最小值
232用栈实现队列简单两栈模拟队列输入栈 + 输出栈,均摊 O(1)
496下一个更大元素 I简单单调栈 + HashMap先对 nums2 建单调栈映射,再查询 nums1
84柱状图中最大的矩形困难单调栈找左右边界对每根柱,找左右第一个更矮的柱
239滑动窗口最大值困难单调队列,需移除过期元素双端队列维护单调递减,队首即窗口最大值
394字符串解码中等栈保存嵌套上下文[ 时将当前结果和倍数压栈,遇 ] 时弹出拼接
150逆波兰表达式求值中等栈处理后缀表达式遇数字入栈,遇运算符弹出两个操作数计算后入栈

常见陷阱与调试

  • 用了 Stack 而不是 ArrayDeque Java 的 Stack 继承自 Vector,方法都加了 synchronized,单线程下有不必要的性能开销,面试中直接用 ArrayDeque
  • 单调栈:搞混"下一个更大"和"上一个更大"的遍历方向: 找"右边"从左到右遍历;找"左边"从右到左遍历。方向搞反会导致答案完全错误。
  • 忘记处理循环结束后栈中的剩余元素: 循环结束后栈里还有元素,说明这些位置右边没有符合条件的答案,应保持默认值(通常是 -1 或 0)。初始化 Arrays.fill(result, -1) 可以避免额外处理。
  • 单调队列:忘记从队首移除过期元素: 滑动窗口问题中,每次新增元素前必须先检查队首是否超出窗口左边界,否则会把过期的最大值带入计算。

面试常问 & 怎么答

为什么用 ArrayDeque 而不是 Stack?

Java 的 Stack 继承自 Vector,所有方法都加了 synchronized,单线程场景下有不必要的性能开销。ArrayDeque 是非同步的,作为栈和队列都比 Stack/LinkedList 更快。

单调栈的时间复杂度为什么是 O(n)?

虽然有嵌套的 while 循环,但每个元素最多入栈一次、出栈一次,总操作次数不超过 2n,所以均摊 O(n)。

单调栈和单调队列的区别?

单调栈只在一端(栈顶)操作,适合找"下一个更大/更小元素"。单调队列两端都可以操作(双端队列),适合维护滑动窗口的最值,需要从队首移除过期元素。


看到什么就先想到这类

  • "括号匹配 / 嵌套结构" → 栈
  • "下一个更大/更小元素" → 单调栈
  • "柱状图面积 / 接雨水" → 单调栈
  • "滑动窗口最大值/最小值" → 单调队列
  • "表达式求值 / 计算器" → 栈
  • "用栈实现队列 / 用队列实现栈" → 两个栈/队列互相倒