1. 栈(stack)是什么?
在 C++ 标准库中,std::stack
是一个容器适配器(container adapter),它并不是独立的数据结构,而是基于其他顺序容器(如 deque
或 vector
)实现的。
特点:
- 遵循 先进后出(FILO / LIFO) 原则。
- 只能在栈顶进行插入和删除操作。
- 常用于表达式求值、括号匹配、函数调用栈模拟等。
2. 头文件与命名空间
#include <stack>
#include <iostream>
using namespace std;
3. 常用 API
方法 | 作用 |
---|---|
push(value) | 向栈顶压入一个元素 |
pop() | 移除栈顶元素(不会返回值) |
top() | 获取栈顶元素的引用 |
empty() | 判断栈是否为空 |
size() | 返回栈中元素个数 |
swap(other) | 交换两个栈的内容 |
⚠️ 注意:pop()
只是删除,不会返回元素值。如果想取值,应先用 top()
,再 pop()
。
4. 基本用法示例
#include <stack>
#include <iostream>
using namespace std;
int main() {
stack<int> s; // 定义一个空栈,存放 int 类型
// 入栈
s.push(10);
s.push(20);
s.push(30);
cout << "当前栈顶元素: " << s.top() << endl; // 输出 30
// 出栈
s.pop();
cout << "出栈后栈顶: " << s.top() << endl; // 输出 20
cout << "栈中元素数量: " << s.size() << endl;
// 遍历栈(需循环 pop)
cout << "栈中所有元素: ";
while (!s.empty()) {
cout << s.top() << " ";
s.pop();
}
cout << endl;
return 0;
}
运行结果:
5. 自定义类型
stack
可以存储结构体或类对象。
struct Point {
int x, y;
};
int main() {
stack<Point> st;
st.push({1, 2});
st.push({3, 4});
cout << "Top Point: (" << st.top().x << ", " << st.top().y << ")" << endl;
return 0;
}
6. 栈的应用场景
一句话来总结就是:最近的事先处理,过去的事后处理
1. 函数调用栈(最近调用的函数先返回)
-
场景:在 C++ 或其他语言里,每次调用函数时,系统会把当前执行环境压入栈(函数参数、局部变量、返回地址)。
-
特点:最近调用的函数,一定是第一个完成并返回的。
-
例子:
void A() { B(); } void B() { C(); } void C() { cout << "C Done\n"; } int main() { A(); return 0; }
执行顺序就是:
main -> A -> B -> C
,返回时是 C → B → A → main,完全符合 最近的事先处理。这里的 “函数调用 → 返回顺序” 背后依赖的就是 调用栈(Call Stack) 机制。
1. 每次调用函数 → 压栈(Push)
当一个函数被调用时,系统会把“当前的执行现场”压入栈里,通常包含:
- 返回地址(调用完要跳回的地方)
- 参数
- 局部变量
- 寄存器保存状态
这些组成了一个 栈帧(Stack Frame)。
2. 函数执行完 → 出栈(Pop)
函数运行结束后,这个栈帧会被弹出(Pop),系统恢复到上一个函数的上下文。
3. 栈的特征体现
以上面的代码为例:
#include <iostream>
void C() { std::cout << "C Done\n"; }
void B() { C(); }
void A() { B(); }
int main() {
A();
return 0;
}
执行过程(调用时 压栈):
main()
进栈- 调用
A()
→A
的栈帧压栈 - 调用
B()
→B
的栈帧压栈 - 调用
C()
→C
的栈帧压栈
执行结束(返回时 出栈):
C
返回 →C
栈帧出栈B
返回 →B
栈帧出栈A
返回 →A
栈帧出栈- 最后回到
main
这就是 “最近调用的函数,最先返回”,完全符合 栈的 LIFO 原则。
4. 实际中的“调用堆栈”
在调试器(如 Visual Studio、gdb)里,能直接看到这个调用栈:
- 在 VS 里是 Call Stack 窗口。
- 在 gdb 里用
bt
(backtrace)命令。
例如,当代码执行到 C
的时候,调用栈显示:
这就是函数调用的堆栈链路。
函数调用过程天然体现了 栈结构的使用,它不需要手动写 std::stack
,而是由编译器和操作系统自动管理的“调用堆栈(Call Stack)”。
2. 表达式计算(最近遇到的操作符,先处理)
-
场景:中缀表达式
3 + (2 * 5)
转换/计算时,括号里的操作需要先完成。 -
特点:括号表达式里,最后遇到的左括号
(
,先被处理并匹配。 -
例子:
- 输入:
(1+(2*3))
- 栈执行过程:
(
入栈 →2*3
先计算 → 再算1+结果
。
- 输入:
-
实际应用:计算器程序、编译器语法解析。
表达式求值(逆波兰表达式 RPN)
int evalRPN(vector<string>& tokens) {
stack<int> s;
for (auto& t : tokens) {
if (t == "+" || t == "-" || t == "*" || t == "/") {
int b = s.top(); s.pop();
int a = s.top(); s.pop();
if (t == "+") s.push(a+b);
if (t == "-") s.push(a-b);
if (t == "*") s.push(a*b);
if (t == "/") s.push(a/b);
} else {
s.push(stoi(t));
}
}
return s.top();
}
3. 回溯算法(最近的一步先撤销)
-
场景:在迷宫寻路中,如果走到死胡同,要回退到最近的一步重新尝试。
-
特点:最近走的一步,最先被撤销。
-
例子:
- 走迷宫路径
A → B → C → D
,发现 D 不通 → 弹出 D → 回到 C。
- 走迷宫路径
-
实际应用:数独求解、八皇后问题、DFS 搜索。
4. 撤销 / 恢复操作(最近的操作先撤销)
-
场景:文本编辑器(如 Word、VS Code)里,撤销(Undo)功能常用栈实现。
-
特点:最后输入的字符,最先被撤销。
-
例子:
- 操作顺序:输入
ABC
- 栈状态:
A → B → C
- 撤销:先撤销
C
→ 再撤销B
→ 再撤销A
。
- 操作顺序:输入
-
实际应用:编辑器、IDE、绘图软件。
5. 浏览器前进/后退(最近访问的页面先返回)
-
场景:浏览器的历史记录。
-
特点:
- 后退:最近访问的页面先返回。
- 前进:另一个栈保存“未来”页面,形成 双栈模型。
-
例子:
- 访问顺序:
A → B → C
- 后退:栈顶
C
弹出 → 回到B
。
- 访问顺序:
-
实际应用:浏览器、APP 页面导航。
6. 数据反转(最近存进去的数据,先取出来)
-
场景:需要把一个序列翻转过来。
-
特点:栈的 LIFO 特性天然可以完成反转。
-
例子:
- 数制转换:10 进制转 2 进制时,每次
%2
的余数压栈,最后从栈中依次弹出就得到正确顺序。 - 字符串反转:把
hello
压栈 → 依次弹出得到olleh
。
- 数制转换:10 进制转 2 进制时,每次
-
实际应用:字符串处理、逆序输出。
数制转换(十进制 → 二进制)
void toBinary(int n) {
stack<int> s;
while (n > 0) {
s.push(n % 2);
n /= 2;
}
while (!s.empty()) {
cout << s.top();
s.pop();
}
cout << endl;
}
7. 小结
“最近的事先处理,过去的事后处理” 在实际场景中的体现:
场景 | 为什么用栈 |
---|---|
函数调用 | 最近调用的函数必须先返回 |
表达式求值 | 最近的括号先匹配,最近的运算符先执行 |
回溯算法 | 最近的一步必须先撤销 |
撤销操作 | 最近的编辑必须先撤销 |
浏览器导航 | 最近访问的页面先返回 |
数据反转 | 最近压入的数据先取出,正好实现逆序 |
7. 总结
- 栈是一种只允许在一端操作的数据结构,遵循 LIFO(后进先出) 原则。
- 在 C++ 中可通过 std::stack 使用。
- 典型应用:函数调用栈、表达式求值、回溯算法、撤销恢复、浏览器历史、数据反转。
- 关键记忆点:最近的事先处理,过去的事后处理。