深度学习C++中的数据结构:栈和队列
一、栈(Stack):先进后出的"叠盘子"
栈的核心特点是先进后出(LIFO,Last In First Out),就像叠盘子一样:最后放上去的盘子,要先拿下来;最底下的盘子,只能等上面的都拿完才能取。

image.png
1. 栈的定义方式
栈有两种常见定义方式,核心是指定存储的数据类型和底层容器(默认用deque)。
| 定义方式 | 代码示例 | 说明 |
|---|---|---|
| 普通定义(默认底层容器) |
| 存储int类型,底层默认用deque容器 |
| 指定底层容器 |
| 分别指定vector、list作为底层容器 |
注意:如果不指定底层容器,C++标准库会默认用deque——因为deque兼顾了vector的随机访问和list的高效插入删除,适合栈的操作场景。
2. 栈的常用操作
栈的操作很简洁,核心围绕"栈顶"(毕竟只能从栈顶进出),常用成员函数如下:
| 成员函数 | 功能 | 示例 |
|---|---|---|
|
| 判断栈是否为空,返回bool值 |
|
|
| 获取栈中有效元素的个数,返回整数 |
|
|
| 获取栈顶元素(不删除),注意栈空时调用会报错 |
|
|
| 将元素x压入栈顶 |
|
|
| 删除栈顶元素(不返回),栈空时调用会报错 |
|
|
| 交换两个栈的所有元素 |
|
3. 栈的使用示例
下面代码演示了栈的基本操作:压入4个元素,然后从栈顶依次弹出并打印。
代码语言:cpp
代码运行次数:0
运行
AI代码解释
#include <iostream>
#include <stack> // 包含stack头文件
#include <vector> // 若指定vector为底层容器,需包含此头文件using namespace std;int main() {// 定义一个以vector为底层容器的栈,存储int类型stack<int, vector<int>> st;// 压入元素(栈顶依次变成1→2→3→4)st.push(1);st.push(2);st.push(3);st.push(4);// 打印栈的大小(此时有4个元素)cout << "栈的大小:" << st.size() << endl; // 输出:4// 循环弹出栈顶元素(直到栈空)while (!st.empty()) {// 先获取栈顶元素并打印cout << st.top() << " "; // 再删除栈顶元素(注意:pop()不返回元素,必须先top()再pop())st.pop(); }cout << endl; // 输出:4 3 2 1(符合先进后出)return 0;
}
二、队列(Queue):先进先出的"排队"
队列的核心特点是先进先出(FIFO,First In First Out),就像日常生活中排队买东西:先排队的人先结账,后排队的人只能等前面的人结束才能轮到。

image.png
1. 队列的定义方式
和栈类似,队列也需要指定数据类型和底层容器(默认同样用deque)。
| 定义方式 | 代码示例 | 说明 |
|---|---|---|
| 普通定义(默认底层容器) |
| 存储int类型,底层默认用deque |
| 指定底层容器 |
| 分别指定vector、list作为底层容器 |
注意:虽然可以用vector作为底层容器,但队列的pop()操作需要删除队头元素——vector删除队头元素效率很低(要移动所有元素),所以实际中更推荐用list或默认的deque。
2. 队列的常用操作
队列的操作围绕"队头"(出队)和"队尾"(入队),常用成员函数如下:
| 成员函数 | 功能 | 示例 |
|---|---|---|
|
| 判断队列是否为空,返回bool值 |
|
|
| 获取队列中有效元素的个数 |
|
|
| 获取队头元素(不删除),队列空时调用报错 |
|
|
| 获取队尾元素(不删除),队列空时调用报错 |
|
|
| 将元素x插入队尾 |
|
|
| 删除队头元素(不返回),队列空时调用报错 |
|
|
| 交换两个队列的所有元素 |
|
3. 队列的使用示例
下面代码演示了队列的基本操作:插入4个元素,然后从队头依次弹出并打印。
代码语言:cpp
代码运行次数:0
运行
AI代码解释
#include <iostream>
#include <queue> // 包含queue头文件
#include <list> // 若指定list为底层容器,需包含此头文件using namespace std;int main() {// 定义一个以list为底层容器的队列,存储int类型queue<int, list<int>> q;// 插入元素(队尾依次加入1→2→3→4,队头始终是1)q.push(1);q.push(2);q.push(3);q.push(4);// 打印队列的大小(此时有4个元素)cout << "队列的大小:" << q.size() << endl; // 输出:4// 循环弹出队头元素(直到队空)while (!q.empty()) {// 先获取队头元素并打印cout << q.front() << " "; // 再删除队头元素q.pop(); }cout << endl; // 输出:1 2 3 4(符合先进先出)return 0;
}
三、实战:用栈解决逆波兰表达式求值
栈的一个经典应用是计算逆波兰表达式(也叫后缀表达式)。先搞懂什么是中缀、后缀表达式:
1. 中缀表达式 vs 后缀表达式
- 中缀表达式:我们平时写的
1 + 2 * 3,操作符在两个操作数中间,需要考虑优先级(先乘后加); - 后缀表达式:把操作符放在两个操作数后面,比如
1 2 3 * +,不需要考虑优先级,按顺序计算即可。

image.png
后缀表达式的计算规则很简单:遇到操作数就入栈,遇到操作符就弹出栈顶两个元素,计算后将结果再入栈,最终栈里剩下的就是结果。
比如计算1 2 3 * +:
- 1、2、3依次入栈 → 栈:1, 2, 3;
- 遇到
*,弹出3(右操作数)和2(左操作数),计算2*3=6,6入栈 → 栈:1, 6; - 遇到
+,弹出6和1,计算1+6=7,7入栈 → 栈:7; - 最终结果就是7。

image.png
2. 力扣例题:逆波兰表达式求值
题目链接:150. 逆波兰表达式求值 - 力扣(LeetCode)
题目描述
给你一个字符串数组tokens,表示一个逆波兰表达式,返回该表达式的计算结果。操作符只有+、-、*、/,且所有操作数都是整数。
解题思路
完全遵循后缀表达式的计算规则,用栈实现:
- 遍历字符串数组
tokens; - 若当前字符串是操作数(不是
+、-、*、/),转成整数后入栈; - 若当前字符串是操作符,弹出栈顶两个元素(注意:先弹的是右操作数,后弹的是左操作数);
- 计算两个元素的结果,将结果入栈;
- 遍历结束后,栈中仅剩的一个元素就是最终结果。

image.png
代码实现
代码语言:cpp
代码运行次数:0
运行
AI代码解释
#include <iostream>
#include <vector>
#include <stack>
#include <string>using namespace std;class Solution {
public:int evalRPN(vector<string>& tokens) {// 1. 定义一个栈,存储操作数和中间结果stack<int> st;// 2. 遍历字符串数组for (auto& str : tokens) {// 2.1 判断当前字符串是否为操作符if (str == "+" || str == "-" || str == "*" || str == "/") {// 弹出栈顶两个元素(注意顺序:先弹右操作数,后弹左操作数)int right = st.top(); // 右操作数(后入栈的)st.pop();int left = st.top(); // 左操作数(先入栈的)st.pop();// 根据操作符计算结果,结果入栈switch (str[0]) { // str是单个字符的字符串,取第一个字符判断case '+':st.push(left + right);break;case '-':st.push(left - right); // 注意:左减右,不是右减左break;case '*':st.push(left * right);break;case '/':st.push(left / right); // 注意:左除右,且题目保证是整数除法break;}} else {// 2.2 是操作数,转成整数后入栈(stoi:string to int)st.push(stoi(str));}}// 3. 遍历结束,栈中仅剩的元素就是结果return st.top();}
};// 测试代码
int main() {Solution sol;// 示例:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]vector<string> tokens = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"};cout << "逆波兰表达式结果:" << sol.evalRPN(tokens) << endl; // 输出:22return 0;
}
四、底层原理:为什么栈和队列是"容器适配器"?
前面我们反复提到"容器适配器",到底什么是适配器?
简单说:适配器是一种"包装器",它基于已有的容器(如vector、list、deque),通过限制部分操作、封装特定接口,实现新的数据结构功能。
栈和队列本身不存储数据,而是"借用"底层容器的存储空间,只暴露符合自身逻辑的接口(比如栈只暴露栈顶操作,队列只暴露队头/队尾操作)。

image.png
1. 栈的底层实现逻辑
栈的核心是"先进后出",所以无论用哪种底层容器,只需确保所有操作都针对容器的"一端"(比如vector的尾端、list的尾端):
push():调用底层容器的push_back()(在尾端插入);pop():调用底层容器的pop_back()(删除尾端元素);top():调用底层容器的back()(获取尾端元素)。

image.png

image.png
2. 队列的底层实现逻辑
队列的核心是"先进先出",所以需要确保入队在一端,出队在另一端:
push():调用底层容器的push_back()(队尾插入);pop():调用底层容器的pop_front()(队头删除);front():调用底层容器的front()(获取队头);back():调用底层容器的back()(获取队尾)。
这也是为什么不推荐用vector作为队列的底层容器——vector没有pop_front()接口(删除队头元素需要移动所有元素,效率低),而list和deque都有高效的pop_front()。

image.png

image.png
3. 容器适配器的设计思想
容器适配器的设计体现了C++的"复用"思想:不需要重新开发存储逻辑,而是基于已有的高效容器,通过封装接口实现新的数据结构。
除了栈和队列,C++中还有priority_queue(优先队列)也是容器适配器,底层默认用vector,通过堆排序实现"每次出队最大/最小元素"的逻辑。

image.png

image.png

image.png

image.png

image.png
总结
- 栈:先进后出,操作围绕栈顶,默认底层容器是deque;
- 队列:先进先出,操作围绕队头/队尾,默认底层容器是deque;
- 核心应用:栈适合解决"后入先出"场景(如逆波兰表达式、括号匹配),队列适合解决"先入先出"场景(如任务调度、广度优先搜索);
- 底层本质:容器适配器,基于现有容器封装接口,复用存储逻辑。
掌握栈和队列的核心逻辑后,很多算法题都会变得简单——比如括号匹配、滑动窗口最大值、二叉树的层序遍历等,后续可以继续深入练习这些场景~
