stack栈容器
困难7后进先出,一摞盘子的秘密——C++ stack 栈容器详解
什么是栈?—— 后进先出的数据结构
想象一下,你面前有一摞干净的盘子。你只能从最上面拿盘子,也只能在最上面放新盘子。后放上去的盘子,总是最先被拿走——这个规则就叫 “后进先出”(Last In First Out,简称 LIFO)。
在计算机里,有很多场景都用到这个“后进先出”的规矩:
- 浏览器里的“后退”按钮:你依次访问页面 A → B → C,它们就像被压进一个栈里。点“后退”时,最先回到的是最近访问的 C,然后是 B,最后是 A。
- 编辑器的“撤销”操作:你做的每一步操作(打字、删除、改颜色)都依次入栈。撤销时,先撤销最近一步。
- 还有食堂里堆叠的餐盘、薯片罐里的薯片(最后放进去的薯片在最上面,你先吃到它)……
C++ 标准库中的 stack 容器,就是帮我们实现这个“一摞盘子”的神奇工具。使用它之前,需要加上头文件:
#include <stack>
stack 本质上是一个 容器适配器(container adapter)。什么意思呢?它不自己存储数据,而是套在另一个容器(比如 deque、vector、list)外面,只允许你从“栈顶”进行操作,从而保证 LIFO 顺序。默认情况下,stack 底层用的是 deque(双端队列),但你也可以指定用 vector 或 list,比如:
stack<int, vector<int>> my_stack; // 底层用 vector 实现的栈
stack<int, list<int>> my_stack; // 底层用 list 实现的栈
不过我们一般直接用默认的 stack<int> 就足够了,不用操心底层细节。
栈的核心操作 —— 就像管理一摞盘子
stack 提供的操作很少,但每个都很关键。我们来逐一看看。
1. push(x) — 放盘子(入栈)
把数据 x 放到栈的最上面。
stack<int> s; // 创建一个空栈,存放整数
s.push(10); // 放入第一个盘子:10
s.push(20); // 再放一个:20
s.push(30); // 再放一个:30
// 现在栈从底到顶是:10, 20, 30
2. top() — 看一眼最上面的盘子(查看栈顶元素)
返回栈顶元素的值,但 不删除 它。如果栈是空的,调用 top() 会导致程序崩溃(未定义行为),所以使用前最好先检查 empty()。
int val = s.top(); // val 等于 30
cout << val << endl; // 输出 30
3. pop() — 拿走最上面的盘子(移除栈顶元素)
删除栈顶元素,不返回任何值。很多新手会以为 pop() 能返回被删的值,其实不能!需要先用 top() 拿到值,再 pop()。
s.pop(); // 移除栈顶的 30,现在栈里剩 10, 20
int next = s.top(); // 拿到现在的栈顶 20
s.pop(); // 再移除 20
4. empty() — 判断栈是不是空的
如果栈中没有元素,返回 true(真);否则返回 false(假)。常用在循环里逐个取出所有元素。
if (s.empty()) {
cout << "栈是空的" << endl;
}
5. size() — 数一数栈里有多少元素
返回栈中元素个数,类型是 size_t(无符号整数)。
cout << "栈中有 " << s.size() << " 个元素" << endl;
常见错误与注意事项
-
在空栈上调用
top()或pop()
❌ 错误示例:stack<int> s; // s 是空的 s.pop(); // 崩溃!空栈不能 pop s.top(); // 崩溃!空栈没有 top✅ 正确做法:每次操作前先检查
if (!s.empty())。 -
忘记
pop()不返回被删的值
❌ 错误:int x = s.pop();
✅ 正确:先int x = s.top();再s.pop(); -
循环遍历时不更新栈状态
❌ 错误:无限循环while (!s.empty()) { cout << s.top(); // 只输出,不 pop,栈永远不空 }✅ 正确:每次输出后记得
s.pop(); -
混淆顺序:
pop()后原来的top()值会失效
如果你先取top()保存到变量里,再pop(),原来的变量依然有效,因为那是一个副本。但如果你用引用(int &ref = s.top();),pop()后引用就无效了,小心使用。
完整示例:反转单词(小练习的完整实现)
题目要求:输入一个单词(比如 hello),把每个字母依次入栈,然后全部弹出,看看输出是什么。这正是利用栈“后进先出”的特点来反转字符串。
#include <iostream>
#include <stack>
#include <string> // 使用 std::string
using namespace std;
int main() {
string word; // 定义字符串,用于存储用户输入的单词
cout << "请输入一个单词:";
cin >> word; // 读取输入
stack<char> letters; // 创建一个字符栈,用来存放每个字母
// 把单词的每个字母依次入栈
for (int i = 0; i < word.size(); i++) {
letters.push(word[i]); // 将第 i 个字母放到栈顶
}
cout << "反转后的结果:";
// 只要栈不为空,就取出栈顶并输出,然后弹出
while (!letters.empty()) {
char ch = letters.top(); // 拿到栈顶字母
cout << ch; // 输出它
letters.pop(); // 移除栈顶字母
}
cout << endl;
return 0;
}
运行演示:
请输入一个单词:hello
反转后的结果:olleh
你看,输入 hello,输出变成了 olleh。这正是后进先出(LIFO)的奇妙效果:h 第一个进去,最后一个出来;o 最后一个进去,第一个出来。
生活中的更多栈应用
除了之前说的浏览器后退、撤销操作,栈在程序世界里无处不在:
-
函数调用栈:你写的程序里,一个函数调用另一个函数,再调用第三个函数……每次调用都会把当前函数的返回地址、局部变量等信息“压”进一个栈中。当最里面的函数执行完,就“弹”出,回到上一级函数。这就是为什么函数调用能正确地一层层返回。
-
括号匹配:写数学表达式
(1 + (2 * 3))时,可以用栈检查左右括号是否匹配——遇到左括号入栈,遇到右括号出栈,最后栈为空说明匹配成功。 -
表达式求值:计算机计算
3 + 4 * 2时,会先把数字和运算符分别压入两个栈,再按运算优先级依次弹出处理。
相关指引
stack 属于 C++ STL(标准模板库)中的容器适配器之一。如果你已经掌握了栈,可以继续学习:
- 队列(
queue):先进先出(FIFO),就像排队买饭,先来的人先得到服务。 - 双端队列(
deque):两端都可以插入和删除,是stack和queue的默认底层容器。 - 优先队列(
priority_queue):每次取出的都是当前最大(或最小)的元素,常用于任务调度、哈夫曼编码等。 - vector / list:栈的底层容器,了解它们的区别有助于理解为什么栈选择
deque作为默认底层。
另外,在手写递归算法时,有时会遇到栈溢出(Stack Overflow)——递归层级太深,系统调用栈爆了。这时可以考虑用自己写的 stack 把递归改成迭代,避免溢出。这也是栈在算法竞赛(CSP-J/S 等)中的重要应用之一。
现在,快去试试用栈解决更多有趣的问题吧!比如用栈检查括号是否匹配、模拟火车车厢调度、或者实现一个“最近打开的文件”历史记录……栈虽小,力量大!
例题精讲
在C++中,stack容器默认使用的底层容器是?
调用stack的top()函数时,如果栈为空,则会导致未定义行为。
补充代码,使得在栈非空时安全地弹出栈顶元素:\nstack<int> st;\n// 向栈中压入若干元素后\nif( ___ ) {\n st.pop();\n}stack容器中size()成员函数的返回值类型是?
stack容器允许通过迭代器或下标直接访问栈底元素,例如st[0]或*st.begin()。