栈与队列:从底层原理到实际应用
接触数据结构时,栈和队列是绕不开的基础。刚开始总觉得它们操作简单、概念直白,直到实际写代码踩了坑,才发现这两个 "简单结构" 里藏着不少设计巧思。结合学习资料和自己的实践经历,整理了这篇偏复习向的笔记,把核心知识点和避坑点都理清楚。
一、先搞懂核心:本质与特性
栈和队列最核心的区别在于数据访问顺序,这直接决定了它们的应用场景。用生活里的例子类比最容易记:栈像家里摞起来的盘子,只能从最顶上拿放;队列像排队买奶茶,先来的人先拿到饮品。
1. 栈(Stack):后进先出(LIFO)
- 核心原则:最后插入的元素最先被移除,只有 "栈顶" 一个操作口。
- 必记操作:
push
(入栈,加在栈顶)、pop
(出栈,删栈顶元素)、top
(查栈顶元素)、empty
(判空)、size
(查元素数)。 - 关键提醒:
pop
操作只删元素不返回值,要拿元素得先⽤top
,这是刚开始写代码常搞混的点。
2. 队列(Queue):先进先出(FIFO)
- 核心原则:最先插入的元素最先被移除,有 "队头"(删)和 "队尾"(加)两个操作口。
- 必记操作:
push
(入队,加在队尾)、pop
(出队,删队头元素)、front
(查队头)、back
(查队尾),empty
和size
与栈一致。 - 关键提醒:和栈一样,
pop
不返回值,且队列没有top
接口,别记混成栈的操作名。
二、深入底层:不是容器,是 "适配器"
这是我学习时的第一个认知误区:一直以为栈和队列是和vector
、list
并列的容器,后来才发现它们是容器适配器—— 基于其他容器封装的接口,自己本身不存数据。
1. 底层容器的选择逻辑
C++ 里栈和队列默认用deque
(双端队列)做底层,因为deque
的头尾操作都很高效。但也能自定义底层容器,不过有严格限制:
操作需求 | 栈(Stack)要求 | 队列(Queue)要求 |
---|---|---|
尾部插入 | ✅ push_back | ✅ push_back |
尾部删除 | ✅ pop_back | ❌ 不需要 |
头部删除 | ❌ 不需要 | ✅ pop_front |
访问尾部 | ✅ back | ✅ back |
访问头部 | ❌ 不需要 | ✅ front |
2. 实战里的选择建议
- 栈的底层选择:可以用
vector
或list
。我自己写题时更爱用vector
,因为内存连续,缓存命中率高,速度更快;但如果数据量波动大、频繁扩容,list
的按需分配内存更有优势。 - 队列的底层选择:千万别用
vector
!vector
没有pop_front
接口,强行模拟头删要移动所有元素,时间复杂度 O (n),完全违背队列的高效需求。实际开发里要么用默认的deque
,要么用list
,两者头删尾插都是 O (1)。
3. 代码示例:自定义底层容器
// 用vector做底层的栈
stack<int, vector<int>> s_vec;
// 用list做底层的队列
queue<int, list<int>> q_list;
// 默认都是deque,等价于下面这样
stack<int> s; // 等价于stack<int, deque<int>>
queue<int> q; // 等价于queue<int, deque<int>>
三、避坑指南:这些错误别再犯
刚开始写栈和队列的代码时,踩过不少 "低级错误",后来发现都是没吃透特性导致的,整理几个高频坑:
1. 空容器操作:直接崩溃的重灾区
栈和队列的pop
、top
、front
、back
操作都不能在空容器上执行,会触发未定义行为(大概率崩溃)。必须先判空再操作,这是铁律。
// 错误写法
stack<int> s;
s.pop(); // 空栈pop,直接崩
cout << s.top(); // 同样危险// 正确写法
if (!s.empty()) {cout << s.top();s.pop();
}
2. 遍历误区:没有迭代器,别硬遍历
栈和队列设计的核心是 "限制访问顺序",所以 STL 里根本没给它们迭代器,没法用for
循环遍历所有元素。如果非要遍历,只能通过top
/front
+pop
的方式,把元素一个个取出来处理,但这样会清空容器,记得提前备份。
3. 优先级队列:别和普通队列搞混
priority_queue
虽然在queue
头文件里,但本质是堆结构,不是普通队列。它的 "出队" 顺序是按优先级(默认大的优先),不是插入顺序。比如插入 3、1、5,出队顺序是 5、3、1,这点刚开始很容易记混。
四、实用主义:记牢应用场景
数据结构的价值体现在应用上,记住栈和队列的典型场景,做题和开发时能快速选对工具。
1. 栈的用武之地
- 回溯与撤销:浏览器的后退功能、编辑器的 Ctrl+Z,本质都是把操作记录压栈,撤销时弹栈恢复。
- 括号匹配:经典算法题,遇到左括号压栈,遇到右括号就弹栈比对,最后栈空则匹配成功。
- 递归实现:递归函数调用时,系统会自动用栈保存上下文,递归太深栈溢出就是这个原因。
- 表达式求值:比如计算 "3+4*2",用栈能处理运算符的优先级。
2. 队列的用武之地
- 任务调度:打印机的任务队列、多线程的任务池,都要保证先提交的任务先执行。
- BFS 遍历:图和树的广度优先搜索必须用队列,比如求二叉树的层序遍历、迷宫的最短路径。
- 消息队列:应用间通信的消息传递,用队列能解耦生产者和消费者,保证消息顺序。
我自己在做 "二叉树层序遍历" 时,刚开始用了栈,结果出来的是深度优先的顺序,后来才反应过来 BFS 必须用队列,这就是没把场景和结构对应好的教训。
五、复习小结:一张表理清核心区别
最后整理成对比表,复习时看一眼就能回忆起关键差异:
特性 | 栈(Stack) | 队列(Queue) |
---|---|---|
核心原则 | 后进先出(LIFO) | 先进先出(FIFO) |
操作口 | 仅栈顶 | 队头(删)、队尾(加) |
关键操作 | top () 访问栈顶 | front ()/back () 访队头 / 尾 |
默认底层容器 | deque | deque |
能否用 vector 底层 | 可以 | 不建议(无 pop_front) |
典型应用 | 括号匹配、递归、撤销 | BFS、任务调度、消息队列 |
时间复杂度(核心操作) | O(1) | O(1) |
其实栈和队列的知识点不算复杂,但细节决定成败。比如忘记判空就操作、选错底层容器、把优先级队列当普通队列用,这些小错误都可能导致程序崩溃。复习时重点抓 "特性 - 底层 - 应用" 的逻辑链,再把避坑点记牢,基本就不会出问题了