程序员面试宝典

一站式面试准备平台

返回分类
algorithms初级

栈和队列

深入理解栈和队列的原理、应用场景、以及如何回答面试官的追问

2026-04-15
阅读时间: 16分钟

栈和队列

栈和队列是两种"受限"的数据结构。面试官想知道你不只是会用,而是理解为什么需要这些限制。

面试考察点

面试官通过这道题想考察:
1. 你是否理解"受限"数据结构的意义
2. 你是否能举出实际的应用场景
3. 你是否能解释函数调用栈的原理
4. 你是否能理解"单调栈"这种扩展思想

一、栈(Stack)

1.1 什么是栈?(入门级解释)

现实类比:叠盘子

      ┌─────┐
      │盘子4│  ← 最后放上去的,最先被拿走
      ├─────┤
      │盘子3│
      ├─────┤
      │盘子2│
      ├─────┤
      │盘子1│  ← 最先放上去的,最后被拿走
      └─────┘

特点:后进先出(Last In First Out, LIFO)

专业解释

栈是一种只允许在"一端"进行插入和删除的线性表。

这一端叫"栈顶"(Top)
另一端叫"栈底"(Bottom)

操作:
- push(x):把 x 压入栈顶
- pop():弹出栈顶元素
- top():查看栈顶元素(不弹出)
- empty():判断是否为空

1.2 栈的核心原理(面试必问)

面试官追问:"栈为什么叫'后进先出'?这个限制有什么用?"

我来解释栈的核心思想:

栈就像一个桶:
- 只从上面放东西(push)
- 只从上面取东西(pop)
- 你永远动不到下面的东西

这有什么用?

1. 历史记录:撤销操作
   你写了很多字,现在 Ctrl+Z 撤销,
   栈会弹出最近的操作,逆序恢复。

2. 函数调用:保存调用现场
   main() 调用 func1(),func1() 调用 func2()
   调用顺序:main → func1 → func2
   返回顺序:func2 → func1 → main
   正好符合栈的 LIFO 特性!

3. 括号匹配:消除嵌套关系
   ( ( ( ) ) )  ← 括号是嵌套的,用栈来处理

1.3 函数调用栈的原理(面试高频追问)

面试官追问:"你能说说函数调用时栈里发生了什么?"

这是必须理解的知识点!

代码执行顺序:

function outer() {
    let a = 1;           // 第2步
    inner();              // 第3步
    let b = 2;           // 第5步
}

function inner() {
    let x = 10;           // 第4步
    console.log(x);
}

outer();                   // 第1步
调用栈的变化过程:

Step 1: outer() 被调用
┌─────────────────┐
│ outer()         │  ← 栈顶
│ 返回地址: main  │
│ 变量: a = ?     │
└─────────────────┘

Step 2: a = 1 执行
┌─────────────────┐
│ outer()         │
│ a = 1          │
└─────────────────┘

Step 3: inner() 被调用(outer 暂停)
┌─────────────────┐
│ inner()         │  ← 栈顶
│ x = ?          │
├─────────────────┤
│ outer() 暂停    │
│ a = 1          │
└─────────────────┘

Step 4: inner() 执行完毕,弹出
┌─────────────────┐
│ outer() 恢复    │  ← 栈顶
│ a = 1          │
└─────────────────┘

Step 5: outer() 继续执行完毕,弹出
┌─────────────────┐
│ (空)            │
└─────────────────┘

面试加分点:递归调用就是函数调用栈的应用!

递归调用(计算阶乘):

function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

factorial(4) 的调用栈:

factorial(4)
    ↓ 调用
factorial(3)
    ↓ 调用
factorial(2)
    ↓ 调用
factorial(1) → 返回 1
    ↑
factorial(2) → 返回 2*1 = 2
    ↑
factorial(3) → 返回 3*2 = 6
    ↑
factorial(4) → 返回 4*6 = 24

这就是为什么递归太深会"栈溢出"!
栈的深度 = 递归调用的层数

1.4 栈的实际应用场景

应用说明面试能说出来吗?
函数调用保存调用现场,递归就是栈的应用必须理解
括号匹配编译器用栈检查语法错误常见面试题
表达式求值中缀转后缀,后缀求值经典算法题
浏览器前进/后退后退就是弹出之前访问的页面实际应用
编辑器撤销Ctrl+Z 就是弹出操作历史实际应用

1.5 单调栈(进阶扩展)

面试追问:"你知道单调栈吗?什么时候用?"

普通栈:只管 LIFO
单调栈:栈内元素保持单调递增或递减

应用:找下一个更大/更小的元素

示例:求每个元素的"下一个更大元素"
nums = [2, 1, 2, 4, 3]

单调递减栈(栈顶最小):

遍历元素,维护栈:
2: 栈空,2 入栈          stack = [2]
1: 1 < 2,2 弹出,1 入栈  stack = [1]
  → 2 的下一个更大是 1
2: 2 > 1,2 入栈          stack = [1, 2]
4: 4 > 2,2 弹出          stack = [1, 4]
  → 2 的下一个更大是 4
  → 1 的下一个更大是 4
3: 3 < 4,3 入栈          stack = [1, 4, 3]

结果:2→1, 1→4, 2→4, 4→null, 3→null

二、队列(Queue)

2.1 什么是队列?(入门级解释)

现实类比:银行排队

      队首                      队尾
       ↑                         ↑
      [张三] [李四] [王五] [赵六] →
      ↑
    先来的先服务

特点:先进先出(First In First Out, FIFO)

和栈的区别

栈(一端操作):        队列(两端操作):
┌───────────┐          ┌───────────┐
│  只能从   │          │ 队尾进    │
│  栈顶操作 │          │ 队首出    │
└───────────┘          └───────────┘

2.2 队列的核心原理

队列就像排队买东西:
- 新来的人从队尾加入(enqueue)
- 服务员从队首服务(dequeue)

这就是为什么叫"先进先出"——先来的先被服务。

2.3 队列的实际应用

应用说明
任务调度操作系统用队列管理待执行的任务
广度优先搜索(BFS)图/树的层序遍历用队列
消息队列异步通信,解耦生产者和消费者
打印队列多个打印任务排队
滑动窗口单调队列实现 O(n) 滑动窗口

2.4 什么是循环队列?(面试追问)

面试官追问:"普通队列有什么问题?什么是循环队列?"

普通队列的问题:

假设队列大小是 5:

初始:[空] [空] [空] [空] [空]
 front                      rear
  ↑                          ↑
(队首)                   (队尾)

入队 3 个元素:
[A] [B] [C] [空] [空]
 front ↑
      rear

入队 2 个元素:
[A] [B] [C] [D] [E]
              ↑
             rear

出队 3 个:
[空] [空] [空] [D] [E]
                  ↑
                 front/rear

问题:队列没满,但队尾已经到头了!
      队首虽然有空位,但无法入队!

这就是"假溢出"问题。

循环队列的解决方案

循环队列:把队列想象成一个环

        rear
         ↓
    ┌──[ ]──┐
   /        \
  [ ]      [ ]
   \        /
    └──[ ]──┘
         ↑
        front

关键:rear 到达末尾后,绕回开头
     front 到达末尾后,也绕回开头

判断队满/队空:
- 队空:front == rear
- 队满:(rear + 1) % n == front

2.5 双端队列(Deque)

面试追问:"什么是双端队列?有什么用?"

Deque = Double Ended Queue

两端都可以入队和出队:

┌────────────────────────────────────┐
│ 队首 ←→ [元素] ←→ [元素] ←→ 队尾  │
└────────────────────────────────────┘

可以当:
1. 栈用(从一端进出一端)
2. 普通队列用(一段进另一端出)
3. 滑动窗口(两端都用到)

滑动窗口最大值:
用单调递减的双端队列,
每次窗口滑动时:
- 从队尾加入新元素
- 从队首移除超出窗口的元素
- 队首就是当前窗口最大值

三、面试真题讲解

题目1:有效的括号(LeetCode 20)

考察点:栈的应用 + 边界条件处理

问题:给一个字符串,判断括号是否匹配
      "({[]})" → true
      "([)]" → false

核心思想:
1. 遇到左括号,入栈
2. 遇到右括号,检查栈顶是否有匹配的左括号
3. 匹配则弹出,不匹配则 false
4. 最后栈空才是 true

面试官追问:为什么要用栈?

回答:因为括号是嵌套的!
     最近看到的左括号,应该最先被匹配。
     这就是栈的 LIFO 特性!

     "({[]})"
      ↑
     看到 {,期望后面有 } 来匹配它
     看到 (,期望后面有 ) 来匹配它
     最内层的 { } 先被匹配,这不就是栈吗?

题目2:最小栈(LeetCode 155)

考察点:设计数据结构,理解栈的变体

问题:设计一个栈,还要能 O(1) 获取最小值

普通栈只能 O(1) 获取栈顶
但最小值需要 O(n) 查找

如何 O(1) 获取最小值?

方案:用一个辅助栈存储"当前最小值"

stack: [5, 2, 3, 1, 4]
minStack: [5, 2, 2, 1, 1]
          ↑  ↑  ↑  ↑  ↑
         每一步的最小值

push(x):
  stack.push(x)
  minStack.push(min(x, minStack.top()))

pop():
  stack.pop()
  minStack.pop()

top():
  return stack.top()

getMin():
  return minStack.top()

面试追问:空间能优化吗?
回答:可以把最小值存为 (value, min) 的元组,
     或者用数学方法(存差值),
     但会增加代码复杂度。

题目3:滑动窗口最大值(LeetCode 239)

考察点:单调队列的高级应用

问题:求滑动窗口的最大值
nums = [1,3,-1,-3,5,3,6,7], k=3

滑动窗口:
[1,3,-1] → 最大是 3
[3,-1,-3] → 最大是 3
[-1,-3,5] → 最大是 5
[-3,5,3] → 最大是 5
[5,3,6] → 最大是 6
[3,6,7] → 最大是 7

答案:[3,3,5,5,6,7]

单调队列的原理

核心思想:维护一个单调递减的队列

遍历数组:
1. 从队尾加入新元素
2. 从队首移除超出窗口的元素
3. 队首就是当前窗口最大值

详细过程(k=3):

[1]     → 队列 [1],输出 null(窗口未形成)
[1,3]   → 3>1,1 弹出,队列 [3]
[1,3,-1]→ -1<3,入队,队列 [3,-1],输出 3
[3,-1,-3]→ -3<-1,-1弹出,-3入队,队列 [3,-3],输出 3
[-1,-3,5]→ 5>-3,-3弹出,5入队,队列 [5],输出 5
[-3,5,3] → 3<5,入队,队列 [5,3],输出 5
[5,3,6]  → 6>3,6>5,3和5弹出,6入队,队列 [6],输出 6
[3,6,7]  → 7>6,6弹出,7入队,队列 [7],输出 7

为什么这样做是对的?
因为新加入的元素比队列里所有元素都大,
那么队列里所有元素都不可能成为最大值了!
这就是"单调性剪枝"。

四、面试总结

栈和队列的核心区别

┌─────────────────────────────────────────────────┐
│                    线性表                        │
│                                                 │
│   ┌───────────────┐      ┌───────────────┐      │
│   │     栈        │      │     队列      │      │
│   │   (Stack)    │      │    (Queue)   │      │
│   ├───────────────┤      ├───────────────┤      │
│   │  只在一端操作  │      │  两端操作     │      │
│   │   LIFO       │      │   FIFO       │      │
│   │  撤销/返回   │      │  排队/调度   │      │
│   └───────────────┘      └───────────────┘      │
└─────────────────────────────────────────────────┘

常见追问汇总

追问考察点回答要点
函数调用栈是什么?操作系统基础保存调用现场,递归就是栈的应用
递归为什么可能栈溢出?调用栈深度栈空间有限,递归太深会耗尽
括号匹配为什么用栈?LIFO 特性最近看到的左括号最先匹配
什么是循环队列?队列优化解决假溢出,首尾相连
什么是单调栈/单调队列?进阶应用保持单调性,O(n) 优化

面试能加分的回答

1. 知道栈在函数调用中的应用
   "函数调用的返回地址、参数、局部变量都存在栈里"

2. 知道递归和栈的关系
   "递归本质上是函数调用栈的应用,循环是递归的迭代版本"

3. 知道滑动窗口最大值用单调队列
   "维护单调递减队列,队首是最大值,O(n) 时间"

4. 知道队列在 BFS 中的应用
   "树的层序遍历、图的最短路径都用队列"

相关题目

相关标签