栈是「一摞盘子」,队列是「排队买饭」—— 两种最简单的规则,却能解决括号匹配、表达式求值这类经典问题
| 0 - 30 min | 理解 LIFO / FIFO 两种规则,背下接口(第 1、2 节) |
| 30 - 45 min | 三个容器的接口对照表(第 3 节)—— 考试前扫一眼 |
| 45 - 75 min | 括号匹配 + 后缀表达式两个模板(第 4、5 节) |
| 75 - 150 min | 做 4 道题,每题限时 20 分钟 |
| 150 - 180 min | 对照本文题解补题 + 做自测 |
从 Day 1 到今天,你用的容器只有「数组」。数组的特点是:想访问哪里就访问哪里(a[7] 随便读),
但你也得自己管下标。
今天要学的两个容器刚好相反:你只能碰「一头」。听起来像是限制,但正是这种限制让某些问题变得特别简单 —— 比如「最近出现的那个还没配对的左括号是哪个」,答案显然就是「最后一个」,也就是栈顶。
食堂里有一摞干净盘子。阿姨洗完一个就往最上面放一个;你要拿盘子,也只能从最上面拿。 最后放上去的那个,最先被拿走。
这就是栈:Last In First Out(后进先出),简称 LIFO。 能操作的那一端叫栈顶(top),另一端叫栈底。
| 操作 | 写法 | 作用 |
|---|---|---|
| 入栈 | s.push(x) | 把 x 放到栈顶 |
| 出栈 | s.pop() | 把栈顶元素扔掉(不返回它!) |
| 看栈顶 | s.top() | 返回栈顶元素的值(不删除) |
| 判空 | s.empty() | 栈空返回 true |
| 大小 | s.size() | 栈内元素个数 |
坑 1:pop() 不会返回被删掉的元素。
这跟很多语言(比如 Python 的 list.pop())不一样。你要先 top() 看一眼,
再 pop() 删掉。这两步必须分开写:
int x = s.top(); // 先拿到值 s.pop(); // 再删掉
坑 2:栈空的时候不能 top() 也不能 pop()。
对空栈调用这两个函数是未定义行为(可能崩溃、可能读到垃圾值,也可能这次碰巧没事下次就 RE)。
铁律:动手之前先 if(!s.empty())。
下面这段代码我实际编译运行过,输出是真实的:
stack<int> s;
cout << "empty? " << (s.empty() ? "是" : "否") << "\n";
s.push(1); s.push(2); s.push(3);
cout << "size=" << s.size() << " top=" << s.top() << "\n";
s.pop();
cout << "pop 后 size=" << s.size() << " top=" << s.top() << "\n";
while(!s.empty()){ cout << "弹出 " << s.top() << "\n"; s.pop(); }
cout << "清空后 empty? " << (s.empty() ? "是" : "否") << "\n";
真实输出:
empty? 是 size=3 top=3 pop 后 size=2 top=2 弹出 2 弹出 1 清空后 empty? 是
依次 push 了 1、2、3,栈顶是 3(最后进的)。
pop 一次以后栈顶变成 2,size 从 3 变成 2。
清空时弹出的顺序是 2、1 —— 和进来的顺序(1、2、3)完全相反。这就是「后进先出」。
顺便注意那个 while(!s.empty()):这是清空栈的标准写法。
C++ 的 stack 没有 clear() 函数(这是个常见的抱怨),只能这样一个个弹掉。
把栈竖着画:push 是往上叠,pop 是从上面拿走,top 是看最上面那个。
想不清楚的时候就在草稿纸上竖着写一列数字,画个箭头,比死看代码有用得多。
食堂打饭的队伍。新来的人站在队尾,打饭的阿姨从队首叫下一个人。 先来的先走。
这就是队列:First In First Out(先进先出),简称 FIFO。
| 操作 | 写法 | 作用 |
|---|---|---|
| 入队 | q.push(x) | 把 x 放到队尾 |
| 出队 | q.pop() | 把队首元素扔掉(同样不返回值) |
| 看队首 | q.front() | 返回队首元素(下一个轮到谁) |
| 看队尾 | q.back() | 返回队尾元素(最后进来的那个) |
| 判空 | q.empty() | 队列空返回 true |
| 大小 | q.size() | 队列内元素个数 |
队列是两头都能看,但只有一头能进、另一头能出:
· push 永远加在 back(队尾)
· pop 永远删掉 front(队首)
所以「front 和 back 是两个不同的元素」(除非队列里只有一个)。
刚学的时候很容易以为 push 进去马上就能 front 出来。
queue<int> q;
q.push(10); q.push(20); q.push(30);
cout << "size=" << q.size()
<< " front=" << q.front() << " back=" << q.back() << "\n";
q.pop();
cout << "pop 后 front=" << q.front()
<< " back=" << q.back() << " size=" << q.size() << "\n";
真实输出:
size=3 front=10 back=30 pop 后 front=20 back=30 size=2
依次 push 了 10、20、30。队首是 10(先进来的),队尾是 30(最后进来的)。
pop 一次以后,10 出去了,队首变成 20,队尾还是 30。先进先出,顺序和进来时一样。
今天你只是拿队列做「模拟排队」这种直白的题。但队列真正的威力在 BFS(广度优先搜索): BFS 要「一层一层往外扩」,而「先发现的先处理」正好就是队列的规则。
今天把接口练熟,到时候写 BFS 就只管想逻辑,不用再纠结语法。
今天还要认识第三个容器 priority_queue(优先队列)。
它不在今天的 4 道题里,但接口必须现在背下来 —— 后面贪心、最短路(Dijkstra)都会用到它。
| 想做的事 | stack | queue | priority_queue |
|---|---|---|---|
| 头文件 | <stack> | <queue> | <queue> |
| 加入元素 | push(x) | push(x) | push(x) |
| 删除元素 | pop() | pop() | pop() |
| 看「下一个是谁」 | top() | front() | top() |
| 看另一端 | —(没有) | back() | —(没有) |
| 判空 | empty() | empty() | empty() |
| 元素个数 | size() | size() | size() |
| 规则 | 后进先出 LIFO | 先进先出 FIFO | 每次出最大的 |
stack 和 priority_queue 用 top();
只有 queue 用 front()。
记法:栈和优先队列都是「一堆东西里取出最该拿的那个」→ top;队列是「排队」→ 看队首 front。
写错了编译器会直接报错('class std::queue<int>' has no member named 'top'),
不算致命,但考场上白白浪费时间。
它的规则是:不管谁先进来,出队时永远是当前最大的那个先出。 我用一段代码实测过:
// 默认写法:大根堆,top() 是最大值 priority_queue<int> pq; for(int x : {5, 1, 9, 3, 7}) pq.push(x); cout << "top=" << pq.top() << " size=" << pq.size() << " -> 依次弹出:"; while(!pq.empty()){ cout << " " << pq.top(); pq.pop(); } cout << "\n"; // 想要小根堆:加上 vector<int> 和 greater<int> 两个参数 priority_queue<int, vector<int>, greater<int>> minpq; for(int x : {5, 1, 9, 3, 7}) minpq.push(x); cout << "top=" << minpq.top() << " -> 依次弹出:"; while(!minpq.empty()){ cout << " " << minpq.top(); minpq.pop(); } cout << "\n";
真实输出:
top=9 size=5 -> 依次弹出: 9 7 5 3 1 top=1 -> 依次弹出: 1 3 5 7 9
priority_queue<int, vector<int>, greater<int>> minpq;
· 第 1 个参数:元素类型(int)
· 第 2 个参数:底层用哪个容器装,一般就写 vector<int>
· 第 3 个参数:怎么比较。greater<int> 表示「小的优先」→ 小根堆
注意最后那个 >> 在现代 C++ 里可以直接连着写(老编译器要求写成 > > 加空格,
C++11 之后不需要了)。
三个容器都是「只能在一头或两头操作」,所以都不支持下标访问(不能写 s[0]),
也都不能用 for 循环遍历。想知道里面有什么,只能一个个 pop 出来。
这是栈最著名的用途,也是今天 P1739 的做法。
给一个只含 ( 和 ) 的表达式,判断括号是否配对。
从左往右读,每遇到一个 ),它要和左边最近的那个还没配对的 ( 配对。
「最近的那个还没配对的」—— 这不就是栈顶吗?
· push 一个左括号 = 「记下来,我还欠一个右括号」
· pop 一个 = 「刚才那个左括号被配掉了,不欠了」
· 栈顶 = 「最近一个还没被配掉的左括号」
· 最后栈空 = 所有左括号都被配掉了
( → 入栈。) → 先检查栈是不是空的。如果空,说明这个右括号没有左括号配它,
立刻判定不匹配(不用再往后看了,直接输出 NO)。如果不空,就 pop 一个。① 遇到右括号时栈为空 —— 这是「右括号多了」。例子:)x+y(
② 扫完之后栈不为空 —— 这是「左括号多了」。例子:((x+y)
很多人只写了第 ② 条,遇到 )( 这种「右括号在前」的情况就会判成 YES,其实应该是 NO。
第 ① 条必须在循环里当场判掉,不能等到最后。
((x+y))*| 读到的字符 | 动作 | 栈内容(底 → 顶) |
|---|---|---|
| ( | 入栈 | ( |
| ( | 入栈 | ( ( |
| x | 不管 | ( ( |
| + | 不管 | ( ( |
| y | 不管 | ( ( |
| ) | 栈不空 → 弹一个 | ( |
| ) | 栈不空 → 弹一个 | (空) |
| * | 不管 | (空) |
| 结束 | 栈空 → 匹配 | → YES |
如果括号只有一种(全是大括号或全是小括号),那么用一个计数器 cnt 就够了:
遇 ( 就 cnt++,遇 ) 就 cnt--,
中途 cnt 不能变成负数,最后 cnt 必须为 0。
但一旦出现多种括号(比如同时有 () 和 []),计数器就不够用了 ——
因为 ([)] 里括号个数是配平的,可它是错的。那时必须用栈(栈里存左括号的类型,遇到右括号时检查栈顶类型是否对应)。
今天先用圆的练手,但心里要知道栈才是通用解法。
我们平时写的是中缀表达式:3*(5-2)+7。运算符在中间,还要考虑优先级和括号。
后缀表达式(又叫逆波兰式)把运算符写在两个操作数后面,而且不需要括号、不需要考虑优先级,
从左往右按顺序算就行。3*(5-2)+7 的后缀写法是:
3.5.2.-*7.+@
其中 . 是「一个操作数结束了」的标记,@ 是整个表达式的结束标记。
最后栈里只剩一个数,那就是答案。
3.5.2.-*7.+@| 读到的内容 | 动作 | 栈内容(底 → 顶) |
|---|---|---|
| 3. | 数字入栈 | 3 |
| 5. | 数字入栈 | 3 5 |
| 2. | 数字入栈 | 3 5 2 |
| - | 弹出 2 和 5,算 5−2=3,入栈 | 3 3 |
| * | 弹出 3 和 3,算 3×3=9,入栈 | 9 |
| 7. | 数字入栈 | 9 7 |
| + | 弹出 7 和 9,算 9+7=16,入栈 | 16 |
| @ | 结束 | 答案 = 16 |
走完之后栈里只剩 16,和样例输出一致。(顺便也能看出为什么不需要括号:
5−2 先算,是因为 - 写在 5 和 2 后面、排在 * 前面。)
遇到运算符时,我们要弹出两个数。假设先弹出来的是 b、后弹出来的是 a,那么:
a = 先压进去的(后弹出的) // 左操作数 b = 后压进去的(先弹出的) // 右操作数 结果 = a - b // 一定要写成 a-b,不是 b-a! 结果 = a / b // 除法同理
为什么? 因为栈是后进先出。在后缀表达式 5.2.- 里,5 先进栈、2 后进栈,
所以 2 在栈顶、先被弹出。而我们要算的是 5−2,也就是「先弹出的当右操作数」。
加法和乘法无所谓(交换律),但减法和除法写反了就是错的。这是 P1449 最常见的 WA 原因。
题目里说「数字可能是多位的」。像 10.28.30./*7.-@ 里的 10、28、30
都是两位数。所以我们不能读一个字符就当做一个数字。
.
long long num = 0; // 正在拼的数 if(c 是数字) num = num * 10 + (c - '0'); // 拼上去 else if(c == '.') { 入栈(num); num = 0; } // 一个数读完了
比如读 1、0、.:num 先变 1,再变成 1×10+0 = 10,遇到 . 时入栈 10。
这跟 Day 6 学的「数字拆分」是同一个套路,只是方向反过来 —— 那边是往外拆,这边是往里拼。
题目特别说明:/ 的结果要向 0 取整,并且「与 C++ / 运算的规则一致」。
这是好消息:C++ 的整数除法天生就是向 0 取整(−7/2 = −3,7/2 = 3),所以你直接用 / 就行,
不需要任何额外处理。(这一点和其他一些语言不一样,那些语言里负数除法是向下取整。)
下面每道题的代码都用 g++ -std=c++14 -O2 -Wall
实际编译通过(零警告)并跑过官方样例,输出是你看到的真实结果。
题意:实现一个栈,支持四种操作:
push(x):把 x 入栈。pop():弹出栈顶。如果栈是空的,不做任何操作,输出 Empty。query():输出栈顶元素。如果栈是空的,输出 Anguei!。size():输出栈内元素个数。本题单测试点内有多组数据。 1 ≤ T, n ≤ 10⁶,且 Σn ≤ 10⁶; 0 ≤ x < 2⁶⁴。
两个关键点:
① Σn ≤ 10⁶ —— 操作总数一百万,必须用快读/关同步,否则可能 TLE;
② x < 2⁶⁴ —— x 最大接近 1.8×10¹⁹,超过 unsigned long long 就装不下了,
必须用 unsigned long long(4 字节的 int、8 字节的 long long 里,
只有 unsigned long long 能表示到 2⁶⁴−1)。
题目自己也提醒:「请注意大量数据读入对程序效率造成的影响」。
用 std::stack 直接做。要点有三个:
1. 数据类型用 unsigned long long,因为 x 可以到 2⁶⁴−1。
2. 每次 pop / query 之前都要判空,空的时候输出对应的字符串而不是去动栈。
3. 六种操作怎么区分。 这里有个陷阱:
我一开始想偷懒,用第二个字符来区分:if(op[1]=='u') 认为是 push,
if(op[1]=='o') 认为是 pop。
这是错的! 因为 "query" 的第二个字符也是 'u'
(q-u-e-r-y)!于是所有的 query 都被当成了 push,
程序去读一个根本不存在的整数 x,cin 进入失败状态,后面的所有读入全部失效 ——
结果就是程序陷入死循环、疯狂输出 0。
正确做法:先看第一个字符。 'p' 开头是 push 或 pop(这两个才用第二个字符区分),
'q' 开头是 query,'s' 开头是 size。
2 5 push 2 query size pop query 3 pop query size
2 1 Anguei! Empty Anguei! 0
第一组:push 2 → query 输出 2 → size 输出 1 → pop(成功,静默)→ query 时栈已空,输出 Anguei!
第二组:栈一直是空的,pop 输出 Empty,query 输出 Anguei!,size 输出 0。
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false); // 数据量大,关掉同步更快
cin.tie(nullptr);
int T;
cin >> T;
while(T--){ // ★ 多组数据
int n;
cin >> n;
stack<unsigned long long> st; // ★ x 可达 2^64-1,必须 ull
while(n--){
string op;
cin >> op;
// 注意:push / pop 都以 'p' 开头,query 缩写成 q,size 缩写成 s
// 千万不能用 op[1] 区分 push 和 query —— "query"[1] 也是 'u'!
if(op[0] == 'p'){
if(op[1] == 'u'){ // push
unsigned long long x;
cin >> x;
st.push(x);
}else{ // pop
if(st.empty()) cout << "Empty\n";
else st.pop();
}
}else if(op[0] == 'q'){ // query
if(st.empty()) cout << "Anguei!\n";
else cout << st.top() << "\n";
}else{ // size
cout << st.size() << "\n";
}
}
}
return 0;
}
官方样例 → 输出 2 / 1 / Anguei! / Empty / Anguei! / 0 完全一致
极值测试:push 18446744073709551615(就是 2⁶⁴−1,unsigned long long 能存的最大值)
然后 query → 输出 18446744073709551615,size → 1。
这验证了 unsigned long long 的选择是对的:换成长度不够的类型这里就已经错了。
int 或 long long 存 x → x 可以到 2⁶⁴−1,存不下。必须 unsigned long long。op[1] 区分操作 → 会把 query 误判成 push(我实际踩过,会死循环)。要先看 op[0]。top() 是未定义行为,可能 RE。Empty 没有感叹号,Anguei! 有感叹号。题目专门提醒过。stack 定义在 while(T--) 循环里面,
否则上一组残留的数据会污染下一组。ios::sync_with_stdio 的话,10⁶ 次读入可能超时。题意:实现一个队列。操作不是用字符串给的,而是用数字编号:
| 输入 | 含义 | 空队列时的行为 |
|---|---|---|
1 x | 把 x 加入队尾 | — |
2 | 弹出队首 | 输出 ERR_CANNOT_POP |
3 | 查询队首 | 输出 ERR_CANNOT_QUERY |
4 | 查询元素个数 | 输出 0(正常输出) |
对于 100% 的测试数据,满足 n ≤ 10000,且被插入队列的所有元素值都是 [1, 1000000] 以内的正整数。
数据非常小,int 存元素完全够用,也不需要考虑读入效率。这题的重点是把接口用对。
纯模拟题,读一个操作号,分支处理即可。三个要点:
1. 操作 1 后面还跟着一个数 x,要接着读进来。
2. 操作 2 和 3 都要先判空,空的时候输出错误提示字符串。
3. 输出格式。 题目文字里有一句「每条输出之间应当用空行隔开」, 但官方样例输出里并没有空行。
以样例为准。评测用的就是标准输出文件,而样例就是标准输出的一部分。
我的做法是按样例输出(不加空行),实测通过样例。如果评测真的要求空行,样例不可能长这样。 但你要知道这个坑存在 —— 这是我这次唯一不太确定的点,已如实说明。
13 1 2 3 4 1 233 3 2 3 2 4 3 2 1 144 3
2 1 2 233 0 ERR_CANNOT_QUERY ERR_CANNOT_POP 144
逐步走一遍(题目提示里的解释):
插入 2 → 队首是 2、个数 1 → 插入 233 → 队首还是 2(先进先出!)→ 弹出 → 队首变成 233 →
再弹出 → 队列空、个数 0 → 查询队首(空)报错 → 弹出(空)报错 → 插入 144 → 队首 144。
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
queue<int> q;
while(n--){
int op;
cin >> op;
if(op == 1){
int x; cin >> x; // ★ 操作 1 后面还有个数
q.push(x);
}else if(op == 2){
if(q.empty()) cout << "ERR_CANNOT_POP\n";
else q.pop();
}else if(op == 3){
if(q.empty()) cout << "ERR_CANNOT_QUERY\n";
else cout << q.front() << "\n"; // ★ 队列用 front(),不是 top()
}else{
cout << q.size() << "\n"; // 操作 4:空队列也正常输出 0
}
}
return 0;
}
官方样例 → 输出 2 / 1 / 2 / 233 / 0 / ERR_CANNOT_QUERY / ERR_CANNOT_POP / 144
(共 8 行),与官方样例逐行完全一致。
注意这 8 行输出里没有空行,这印证了上面那条判断:以样例为准。
front() 写成 top() → 编译报错(queue 没有 top)。front() / pop() → 未定义行为。必须先判空。ERR_CANNOT_POP 和 ERR_CANNOT_QUERY(都是大写 + 下划线)。题意:给一个后缀表达式(逆波兰式),求它的值。运算符只有 + - * /。
操作数后面跟 . 表示这个数结束,整个表达式用 @ 结尾。
保证除法除数不为 0,且除法结果向 0 取整。
数据保证 1 ≤ |s| ≤ 50,答案和计算过程中的每一个值的绝对值不超过 10⁹。
字符串长度只有 50,所以时间、空间都毫无压力。难点完全在于解析和运算顺序。
注意「过程中每个值的绝对值不超过 10⁹」这句话 —— 用 int 其实也放得下,
但用 long long 更保险(反正内存不差这点)。
一个栈搞定,从左往右扫字符串:
num = num * 10 + (c - '0')。. → 一个数读完了,把 num 入栈,然后 num = 0 重新开始。@ → 表达式结束,跳出循环。循环结束后,栈顶就是答案。
3.5.2.-*7.+@
16
10.28.30./*7.-@
-7
样例 2 手推:10、28、30 依次入栈 → 遇 / 弹出 30 和 28,
算 28 / 30 = 0(整数除法)→ 入栈得到 [10, 0] → 遇 * 弹出 0 和 10,
算 10 × 0 = 0 → 入栈得到 [0] → 7 入栈得到 [0, 7] → 遇 - 弹出 7 和 0,
算 0 − 7 = −7。注意这里就是「先弹出的当右操作数」:0 − 7,不是 7 − 0。
#include <bits/stdc++.h>
using namespace std;
int main(){
string s;
cin >> s;
stack<long long> st;
long long num = 0; // 正在拼的数字
for(size_t i = 0; i < s.size(); i++){
char c = s[i];
if(c >= '0' && c <= '9'){
num = num * 10 + (c - '0'); // ★ 多位数:一位一位拼起来
}else if(c == '.'){
st.push(num); // 遇到 '.' 说明一个数读完了
num = 0;
}else if(c == '@'){
break; // 表达式结束
}else{ // 运算符
long long b = st.top(); st.pop(); // ★ 先弹出的是右操作数
long long a = st.top(); st.pop();
long long r = 0;
if(c == '+') r = a + b;
else if(c == '-') r = a - b;
else if(c == '*') r = a * b;
else r = a / b; // C++ 的 / 本身就是向 0 取整
st.push(r);
}
}
cout << st.top() << "\n";
return 0;
}
官方样例 1 3.5.2.-*7.+@ → 16 完全一致
官方样例 2 10.28.30./*7.-@ → -7 完全一致
多位数专项测试(这题的评分点就在这儿):
· 123.456.+@ → 579(123 + 456)
· 1000.1000.*2.-@ → 999998(1000×1000 − 2)
· 42.@ → 42(表达式里只有一个数)
10. 时只识别成 1,后面全乱。必须用 num * 10 + digit 累积。. 忘了把 num 清零 → 下一个数字会接在旧的后面(比如 12 之后读 3 会变成 123)。cin >> s 读入是可行的(表达式里没有空格),但如果题目说「一行可能带空格」就得用 getline。i < s.size() 但忘了处理 @ → @ 会被当成运算符处理。要显式 break。c >= '0' && c <= '9',不要用字符的 ASCII 码硬编码。题意:表达式由数字、小写字母、运算符 + - * / 和左右小括号组成,以 @ 结尾。
判断圆括号是否匹配:匹配输出 YES,否则输出 NO。
只有圆括号要管,方括号、大括号题面里没有。
表达式长度小于 255,左圆括号少于 20 个。
数据极小,用栈还是用计数器都毫无压力。这题的重点是把两种情况都想全。
用栈做(这是通用解法):遇 ( 入栈;遇 ) 时先判空 ——
空说明右括号多了,立刻判 NO;不空就弹一个。扫完后再判一次栈是否为空,不空说明左括号多了。
因为只有一种括号,用一个计数器就够了:遇 ( 加一、遇 ) 减一。
但为了跟 Day 28 的主题呼应、也为了以后遇到「多种括号」时能直接套用, 我的代码还是用栈写的。栈的写法通用性更强,建议你练熟这一版。
2*(x+y)/(1-x)@
YES
(25+x)*(a*(a+b+b)@
NO
样例 1:两个 ( 两个 ),且顺序正确 → YES。
样例 2:有两个 ( 但只有一个 ),结束时栈里还剩一个 → NO。
#include <bits/stdc++.h>
using namespace std;
int main(){
string s;
cin >> s;
stack<char> st;
bool ok = true;
for(size_t i = 0; i < s.size(); i++){
char c = s[i];
if(c == '@') break; // 表达式结束
if(c == '('){
st.push(c); // 左括号入栈
}else if(c == ')'){
if(st.empty()){ // ★ 右括号多了,当场判错
ok = false;
break;
}
st.pop(); // 和栈顶的左括号配对
}
}
if(!st.empty()) ok = false; // ★ 左括号多了
cout << (ok ? "YES" : "NO") << "\n";
return 0;
}
官方样例 1 2*(x+y)/(1-x)@ → YES 完全一致
官方样例 2 (25+x)*(a*(a+b+b)@ → NO 完全一致
边界测试:
· ((x+y))*@(嵌套 + 并列)→ YES
· )x+y(@(右括号在前)→ NO ← 如果只判「最后栈是否为空」,这里会错判成 YES
· x+y@(一个括号都没有)→ YES(空表达式也算匹配)
)( 这种会错判成 YES。必须在循环里对空栈判错。top() / pop() → 未定义行为。要先判空。@ 处停下 → 后面的字符会被当成表达式内容。YES / NO,全大写,别写 Yes 或 yes。| # | 错误写法 | 后果 | 正确写法 |
|---|---|---|---|
| 1 | 对空栈直接 top() 或 pop() |
未定义行为,可能 RE、可能读到垃圾值 | if(!s.empty()) 之后再动手 |
| 2 | 以为 pop() 会返回被删的值int x = s.pop(); |
编译报错(pop 返回 void) | int x = s.top(); s.pop(); |
| 3 | 队列用 q.top() |
编译报错:queue 没有 top | 队列是 q.front(),栈和优先队列才是 top() |
| 4 | 后缀表达式里r = b - a |
减法和除法结果全反 → WA | r = a - b;(先弹出的是右操作数) |
| 5 | 后缀表达式一次只读一位数字 | 多位数(10、28、30)被拆散 → WA | num = num*10 + (c-'0'),遇 . 才入栈 |
| 6 | 括号匹配只在最后判一次空栈 | )(x+y( 这类会错判成 YES |
遇到右括号时若栈空 → 立刻判 NO |
| 7 | B3614 用 op[1] 区分操作 |
"query"[1] 也是 'u',被当成 push→ 读入失败 → 死循环 |
先看 op[0],再决定要不要看 op[1] |
| 8 | B3614 用 int / long long 存 x |
x < 2⁶⁴ 超出范围 → WA | unsigned long long |
| 9 | 多组数据时把 stack 定义在循环外 |
上一组残留元素污染下一组 | 定义在 while(T--) 循环体内 |
| 10 | 输出字符串拼错Empty! / Anguei |
答案错误 | Empty(无叹号)Anguei!(有叹号) |
stack<int> s; int x = s.pop();
答:两个错。
① pop() 的返回值是 void,不能赋值给任何变量,这行直接编译不过。
想拿值要先 top()。
② 就算写成 int x = s.top();,如果此时 s 是空的,同样是未定义行为。
必须先 if(!s.empty())。
push 和 pop,那怎么区分「下一个出来的是谁」?答:
· stack:push 和 pop 都在同一头(栈顶),用 top() 看 →
后进先出。
· queue:push 进队尾、pop 从队首出,用 front() 看下一个、
back() 看最后进来的 → 先进先出。
一句话:栈是一头进出,队列是两头分工。
5.3.-2.*@ 的值,并写出每一步的栈内容。答:
| 读到 | 动作 | 栈 |
|---|---|---|
| 5. | 入栈 | 5 |
| 3. | 入栈 | 5 3 |
| - | 弹 3(右)、弹 5(左),算 5−3=2 | 2 |
| 2. | 入栈 | 2 2 |
| * | 弹 2(右)、弹 2(左),算 2×2=4 | 4 |
| @ | 结束 | 答案 4 |
它对应的中缀表达式是 (5−3)*2,确实是 4。
答:因为那只能发现「左括号多了」,发现不了「右括号多了」。
反例:)x+y(@。第一个字符就是 ),此时栈是空的 —— 它没有左括号可配。
但如果只在最后判空,扫完后栈恰好也是空的(因为中间又补了一个 ( 和一个 )),
程序就会错误地输出 YES。正确答案是 NO。
所以必须在循环里遇到 ) 时先判空,一发现空就立刻判 NO 并 break。
string rev(string s){
stack<char> st;
for(char c : s) st.push(c); // 全部压进去
string r;
while(!st.empty()){ // 再一个个弹出来
r += st.top();
st.pop();
}
return r;
}
★ 因为栈是后进先出,最后一个字符最先被弹出,所以弹出来的顺序正好和压进去的顺序相反。 「反转」是栈最本质的用途 —— 括号匹配里「找最近的左括号」本质上也是在利用这个性质。
答:当你需要「每次取出的不是最早进来的,而是当前最大/最小的那个」时。
比如「每次从一堆任务里挑优先级最高的执行」「Dijkstra 最短路里每次取当前距离最小的点」。 这些场景的顺序和「进来时间」无关,只和「值的大小」有关,所以要用优先队列。
默认的 priority_queue<int> 是大根堆(每次出最大值);
要小根堆得写 priority_queue<int, vector<int>, greater<int>>。
【stack · 后进先出 LIFO】 #include <stack> stack<int> s; s.push(x); // 入栈 s.top(); // 看栈顶(★ 不删除) s.pop(); // 删栈顶(★ 不返回) s.empty(); s.size(); // ★ 先 top 再 pop;动手前先 if(!s.empty()) 【queue · 先进先出 FIFO】 #include <queue> queue<int> q; q.push(x); // 进队尾 q.front(); // 看队首(★ 下一个出队的) q.back(); // 看队尾 q.pop(); // 删队首 q.empty(); q.size(); 【priority_queue · 每次出最大 / 最小】 priority_queue<int> pq; // 大根堆 priority_queue<int, vector<int>, greater<int>> mp; // 小根堆 pq.push(x); pq.top(); pq.pop(); pq.empty(); pq.size(); 【三个容器的「看下一个」】 stack → top() queue → front() priority_queue → top() // 都不要用下标访问,都不能 for 遍历 【括号匹配】 for(char c : s){ if(c == '@') break; if(c == '(') st.push(c); else if(c == ')'){ if(st.empty()){ ok = false; break; } // ★ 右括号多了 st.pop(); } } if(!st.empty()) ok = false; // ★ 左括号多了 【后缀表达式求值】 long long num = 0; for(char c : s){ if(isdigit(c)) num = num*10 + (c-'0'); // ★ 多位数 else if(c == '.') { st.push(num); num = 0; } else if(c == '@') break; else { long long b = st.top(); st.pop(); // ★ 先弹出的是右操作数 long long a = st.top(); st.pop(); if(c=='+') st.push(a+b); if(c=='-') st.push(a-b); if(c=='*') st.push(a*b); if(c=='/') st.push(a/b); // 向 0 取整,C++ 默认行为 } } cout << st.top();
限时模拟赛(第一次完整比赛体验)
前面 28 天你都在按知识点一天一题地练。明天要换一种练法:给你 4 道题、3 个小时, 自己判断难度、自己分配时间。
题目是 P1067 多项式输出、P1328 生活大爆炸版石头剪刀布、P1981 表达式求值、P1996 约瑟夫问题 —— 都不难,但在压力下把简单的题写对,本身就是一项需要专门练的能力。
今天学的栈和队列明天不会直接用上,但「用容器管理数据」的思路会一直跟着你。 另外 Day 20 的 BFS 就是靠队列撑起来的 —— 今天的接口就是那时候的地基。