栈和队列不是两个新容器,是两种顺序约定:栈规定“最后进来的第一个出去”(LIFO),队列规定“最先进来的第一个出去”(FIFO)。前面几节其实一直在用它们 ——BFS 用队列按层扩散,递归用系统栈保存现场 —— 但都是自动的。这一节把它们拿到手里,用一行 st.push(x) 自己决定什么时候存、什么时候取。
具体做三件事:①后缀表达式求值,看清“栈顶就是最近的两个操作数”;②括号匹配,看清“最近未匹配的左括号就是栈顶”;③把递归改写成显式栈,把队列当成一个循环的圈。
| 容器 | 从哪进 / 从哪出 | 看一眼拿到谁 | 典型场景 |
|---|---|---|---|
| stack<int> | 都从栈顶 | 最近压入的 | 括号匹配、后缀表达式、递归改迭代、单调栈 |
| queue<int> | 队尾进、队头出 | 最早进来的 | BFS 按层扩散、约瑟夫环、任务排队 |
| deque<int> | 两端都能进出 | 最早或最近的 | 0-1 BFS、滑动窗口最值(第 28 节) |
三种容器的接口几乎一样(push / pop / empty / size),区别只在 pop() 删的是哪一个,以及取元素用的是 top() 还是 front()。把这三个名字对错位置,是本节第一个坎。
| 组成 | 含义 / 格式要求 |
|---|---|
stack<int> | 头文件 <stack>(bits 已包含)。只有“栈顶”一个出入口;底层默认用 deque 装,所以不用预先开大小。 |
st.push(x) | 压栈,O(1)。x 成为新的栈顶,原来的栈顶被压在它下面。 |
st.top() | 读栈顶,O(1),不删除。返回引用,可以赋值(st.top() += 3 合法)。栈空时调用是未定义行为,通常 RE。 |
st.pop() | 弹栈,O(1),删除栈顶,返回 void。想用被删的值,必须先 top() 存下来。 |
st.empty() / st.size() | 判空 / 元素个数。size() 的返回类型是 size_t(无符号),空栈时算 size() - 1 会得到 18446744073709551615。 |
没有迭代器、没有 clear() | stack 故意不给你遍历和下标 —— 只能看栈顶,这就是它的语义。清空只能循环 pop,或者 st = stack<int>(); 重新赋值。 |
stack<int> st;
st.push(1); // 栈底 → [1]
st.push(2); // [1, 2]
st.push(3); // [1, 2, 3] ← 栈顶
cout << st.top(); // 3
st.pop(); // 弹出 3,剩下 [1, 2]
cout << st.top() << st.size(); // 22
// 清空的标准写法
while (!st.empty()) st.pop();int x = st.pop(); 编译不过:pop() 返回 void。这是初学者最高频的一行错。
top() 返回的是引用,char c = st.top(); st.pop(); 的顺序不能反 —— 先 pop 再 top 拿到的是下一个元素。
| 组成 | 含义 / 格式要求 |
|---|---|
queue<int> | 头文件 <queue>。队尾进、队头出;先进先出,所以最早进来的最先被处理。 |
q.push(x) | 入队,O(1)。只能从队尾进。 |
q.front() | 读队头,O(1),不删除。相当于 stack 的 top(),但拿到的是最旧的元素。 |
q.back() | 读队尾,O(1)。拿到的是最新入队的元素。 |
q.pop() | 删除队头,O(1),同样返回 void。 |
与 stack 的对照 | 取元素:stack 用 top(),queue 用 front()。pop() 删的都是“下一个该被处理的”,但 stack 处理最新的,queue 处理最旧的 —— 这一句就是 LIFO 与 FIFO 的全部差别。 |
queue<int> q;
for (int i = 1; i <= 5; ++i) q.push(i); // [1, 2, 3, 4, 5]
cout << q.front() << q.back(); // 15
// 把队头搬到队尾:整圈轮转一格
q.push(q.front());
q.pop();
cout << q.front(); // 2第 26 节已经用 queue 写过 BFS,这里只做对照:把 top 和 front、把“最新”和“最旧”彻底分清。
queue 不能随机访问,也不能倒着走。需要两端都能动时用 deque(第 28 节)。
把队头搬到队尾只要两行:q.push(q.front()); q.pop(); —— 记住这个动作,约瑟夫环就是它。
后缀表达式(逆波兰式)把运算符写在两个操作数之后。例如 3.5.2.-*7.+@ 对应中缀写法 3×(5−2)+7,值为 16。
输入一行后缀表达式:操作数是十进制非负整数,每个数后面跟一个 '.' 表示这个数结束;运算符只有 +、−、×、÷;整行以 '@' 结尾。
输出格式:一行一个整数,即表达式的值。
题目保证表达式合法;除法按 C++ 的整数除法计算(向零取整)。
中缀表达式难算,是因为读到 + 的时候你还不知道后面有没有 ×:优先级和括号都要“往后看”。后缀表达式把这些决定提前做完了,读到运算符时,它要的两个操作数一定刚好是最近的两个。
“最近的”就是栈顶。于是算法只有三行:数字入栈;运算符弹出两个算一次再压回去;扫完栈里剩一个数。
顺序是关键。先弹出的是右操作数 b,后弹出的是左操作数 a,计算的是 a op b。对 + 和 × 无所谓,对 − 和 ÷ 写反就得到相反数或倒数 —— 这是本节最高频的 WA。
为什么这样是对的。用归纳看不变量:扫过某个前缀之后,栈里从底到顶正好是“这个前缀算出的、还没被合并的中间结果”,并且它们的相对顺序与表达式剩余部分一致。读到一个数:它是新的中间结果,压栈,不变量成立。读到一个运算符:它作用在最近的两个中间结果上,弹两个压一个,正好是它们的合并,不变量仍成立。扫完整个表达式只剩一个中间结果,就是答案。
复杂度 O(n):每个字符进栈一次或做一次常数运算。长度 10⁵ 的表达式,几百微秒的事。
// 骨架:一个栈包打天下(st 是 stack<long long>,num 是正在攒的数)
while (cin >> c) {
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(); // 后弹出的是左操作数
st.push(c == '+' ? a + b : c == '-' ? a - b : c == '*' ? a * b : a / b);
}
}#include <bits/stdc++.h>
using namespace std;
int main() {
stack<long long> st; // 中间结果可能很大,用 long long 不用 int
long long num = 0; // 正在读的那个数
char c;
while (cin >> c) { // 逐字符读,cin >> char 会自动跳过空白
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; // 不能写成 b - a
else if (c == '*') r = a * b;
else r = a / b; // 同上,顺序错了结果就反了
st.push(r); // 结果必须压回栈,后面的运算符要用它
}
}
cout << st.top() << '\n'; // 扫完栈里恰好剩一个数
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
stack<long long> st; | 栈里存的是“中间结果”,不是字符。用 long long 是因为 7×8×9×… 这类连乘很快超出 int,本题数据小,但换个数据就爆。 |
while (cin >> c) | 逐字符读。cin >> char 会自动跳过空格和换行,所以表达式里有没有空格都不影响。另一种写法是 getline 整行再遍历,那样要自己处理 '\r'。 |
num = num * 10 + (c - '0'); | 多位数拼装。342 是 3→34→342:每次先把已有的数乘 10 进位,再加上新的一位。(c - '0') 把字符 '7' 转成整数 7,靠的是 ASCII 码连续。 |
else if (c == '.') | 本题的 '.' 是数字结束符,不是小数点。看到它就说明前面攒的 num 是一个完整的操作数。 |
long long b = st.top(); st.pop(); | 先弹出的是右操作数。后缀表达式 3.5.- 的含义是 3 − 5,栈里是 [3, 5],先弹出 5 才是被减数那一侧的 b。 |
long long a = st.top(); st.pop(); | 后弹出的是左操作数。两个 pop 的先后顺序就是本题唯一需要动脑的地方。 |
r = a - b; r = a / b; | 减法和除法不满足交换律,写成 b − a 后 5.3.- 会输出 −2 而不是 2。加法和乘法写反了看不出来,所以这个错常被样例放过。 |
st.push(r); | 结果必须压回栈顶,它是后续运算的操作数。漏了这一行,下一个运算符弹到的是更早的数据,整片错。 |
cout << st.top() | 合法表达式扫完后,所有中间结果都合并成了一个。若栈里还剩两个,说明表达式不合法或漏算了。 |
给一行字符串(可能含其他字符,忽略),判断其中 ()、[]、{} 三种括号是否配对且嵌套正确。
栈顶就是“最近的、还没被匹配的左括号”。遇到左括号压栈;遇到右括号,它必须和栈顶配对 ——因为括号的配对是后开的先闭,这正是 LIFO。扫描结束时栈必须为空,否则有左括号没闭上。
#include <bits/stdc++.h>
using namespace std;
bool check(const string &s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
st.push(c); // 左括号:记住它,等待配对
} else if (c == ')' || c == ']' || c == '}') {
if (st.empty()) return false; // 右括号多了:没有左括号可以配
char t = st.top();
if ((c == ')' && t != '(') ||
(c == ']' && t != '[') ||
(c == '}' && t != '{')) return false; // 类型不对,嵌套关系被破坏
st.pop(); // 配对成功,弹出这一个
}
}
return st.empty(); // 左括号多了时这里返回 false
}
int main() {
string s;
getline(cin, s);
cout << (check(s) ? "YES" : "NO") << '\n';
return 0;
}三个返回点对应三种失败:①栈空却来了右括号(右括号多);②类型不匹配(嵌套错);③扫完栈非空(左括号多)。漏掉任何一个,都会把非法串判成合法。
只有一种括号时,其实一个计数器就够了:遇 ( 加一、遇 ) 减一,中途不能为负、最后必须为 0。这是同一个不变式的退化版本。一旦有第二种括号,计数器就不够了 —— 它记不住“该闭的是哪一种”,例如 ([)] 的计数完全合法,但嵌套是错的。
按 1, 2, …, n 的顺序压栈,任意时刻都可以弹栈。给定一个出栈序列,问它是否合法。判断规则只有一条:按入栈顺序逐个压栈,每压一个就检查栈顶是不是出栈序列当前要的元素,是就一直弹;压完全部之后若栈为空,就合法。
为什么这一条规则够:出栈序列的第一个元素是 k,意味着 1…k 都已经压进来过(不压进去弹不出来),而 1…k−1 还压在 k 下面。此时“能弹就弹”不会损失任何选择 —— 早弹掉一个已经可以弹的元素,只是把它从栈顶移走,不改变剩下元素之间的压弹关系。
// 入栈序列固定为 1..n,判断 out[1..n] 是不是合法的出栈序列
bool ok(int n, const vector<int> &out) {
stack<int> st;
int p = 1; // 下一个还没压进去的数
for (int i = 1; i <= n; ++i) {
// 栈顶不是 out[i],就只能继续压,直到压出它或压完
while (p <= n && (st.empty() || st.top() != out[i])) st.push(p++);
if (st.empty() || st.top() != out[i]) return false; // 压完了还是没有
st.pop(); // 栈顶正好是 out[i],弹掉它
}
return true;
}第 21 节说过:递归靠系统栈,每层压入参数、局部变量和返回地址,深度到 10⁶ 就爆栈 RE。既然栈是显式的概念,就可以自己开一个栈放到堆上,把递归改写成循环 —— 求连通块这种“只关心访问过哪些点、不关心访问顺序”的问题,改完和递归完全等价。
// 网格连通块:从 (sx, sy) 出发,把能走到的 '.' 全部标记
const int N = 1005;
int n, m;
char g[N][N];
bool vis[N][N];
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
void floodStack(int sx, int sy) {
stack<pair<int, int>> st;
st.push({sx, sy});
vis[sx][sy] = true; // ★ 入栈的同时就标记
while (!st.empty()) {
auto [x, y] = st.top(); // C++17 结构化绑定
st.pop();
for (int k = 0; k < 4; ++k) {
int nx = x + dx[k], ny = y + dy[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (g[nx][ny] != '.' || vis[nx][ny]) continue;
vis[nx][ny] = true; // ★ 这里也一样:push 之前标记
st.push({nx, ny});
}
}
}标记时机是唯一容易错的地方。递归版是“进函数就标记”,显式栈必须在 push 之前标记。如果等 pop 的时候才标记,同一个点会被它的多个邻居各压一次,在 10⁶ 的网格上栈会膨胀到千万级 → MLE 或 TLE。
两者的差别只在访问顺序:递归深度优先地一条路走到黑,显式栈的弹出顺序受入栈顺序影响,遍历次序不同但访问集合相同。所以连通性、可达性这类问题可以随意替换;要求“按字典序输出第一条路径”时不能随便换。
n 个人围成圈编号 1…n,从 1 号开始报数,报到 m 的人出列,下一个人重新从 1 报,求出列顺序。
用队列,队头就是“当前该报数的人”。报 1 到 m−1 的人安全,把他从队头搬到队尾(相当于绕了一圈);轮到第 m 个人就出队并输出。“搬队头到队尾”这一个动作,就把线性队列变成了环。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
queue<int> q;
for (int i = 1; i <= n; ++i) q.push(i);
while (!q.empty()) {
// 前 m-1 个人安全:逐个搬到队尾,报数指针自然前移
for (int k = 1; k < m; ++k) {
q.push(q.front());
q.pop();
}
cout << q.front() << ' '; // 第 m 个人出列
q.pop();
}
cout << '\n';
return 0;
}n = 10⁴、m = 10⁴ 时最坏要搬 10⁸ 次,勉强能过;n 到 10⁵ 就必须换算法。一个立刻见效的常数优化:当前还剩 s 个人时,搬 s 次就回到原点,所以只搬 (m - 1) % s 次。彻底的做法是把 m 从一开始就模掉,见下面的数学补给。
空栈上调用 top() 或 pop():未定义行为,几乎必然 RE。后缀表达式里运算符比操作数多一个就会触发;括号匹配里 )abc 这种以右括号开头的串,不先判 empty() 就是 RE。
后缀表达式两个操作数顺序搞反(a - b 写成 b - a):样例 3.5.2.-*7.+@ 会输出 −2 而不是 16,属于“样例就能查出来”的错;但很多人只测了全是 + 的数据 → WA。
运算符算完忘记 st.push(r):下一个运算符弹出的是更早的数据或空栈 → WA 或 RE。
把 '.' 当成小数点,用 stod 读它:本题的 '.' 是数字结束符,3.5. 是“数 3、数 5”两个操作数,不是小数三点五 → WA。
括号匹配只用计数法(遇左加一、遇右减一):([)] 会被判成合法 → WA。计数法只对单一类型括号成立。
括号匹配最后忘了 return st.empty():(() 被判成合法 → WA。
约瑟夫轮转次数差一:for (int k = 1; k < m; ++k) 转 m−1 次是对的,写成 k <= m 会多转一个人,n = 5、m = 2 时第一个出列的人从 2 变成 3,整串输出全错 → WA。
显式栈 DFS 在 pop 之后才标记 visited:同一点被多个邻居重复压栈,10⁶ 网格直接 MLE / TLE。
关掉页面,把 P1449 的代码从零敲一遍并提交,确认样例输出 16。
改一改①:把 r = a - b; 改成 r = b - a;,用表达式 5.3.-@ 测,正确结果是 2,改完变成 −2。亲手确认一次“先弹出的是右操作数”。
改一改②:括号匹配把最后的 return st.empty() 改成 return true,用 (() 测,答案从 NO 变成 YES —— 一个左括号没闭上就被放过了。
改一改③:约瑟夫把 for (int k = 1; k < m; ++k) 改成 k <= m,跑 n = 5、m = 2:正确顺序是 2 4 1 5 3,改完第一个数就变成 3。
队列模拟每次出列要搬 m 次,n 个人总共 O(nm)。n = m = 10⁵ 时是 10¹⁰ 次操作,必然超时。但这个环有一个只看“人数”的递推结构。
把编号改成 0 起:n 个人编号 0…n−1,报到 m 的人出列(下标是 (m−1) mod n)。出列后剩下 n−1 人,从原来编号 m mod n 的那个人开始重新报 1。把这 n−1 个人按新的起点重新编号 0…n−2,则新编号 k 对应原编号 (k + m) mod n。
于是得到递推:J(1) = 0,J(n) = (J(n−1) + m) mod n,其中 J 是胜者在新编号下的下标。答案就是 J(n) + 1(换回 1 起)。因为每次只在“少一个人”的同一个问题上加一步,复杂度 O(n)、空间 O(1)。
| n | J(n)(0 起,m = 2) | 场上还剩的人(1 起) |
|---|---|---|
| 1 | 0 | 1 |
| 2 | 0 | 1 |
| 3 | 2 | 3 |
| 4 | 0 | 1 |
| 5 | 2 | 3 |
用模拟验证 n = 5、m = 2:出列顺序 2 4 1 5 3,最后剩 3 ✓,与递推 J(5) = 2 → 3 一致。手算一遍就能确认公式没有差一:J 用的是 0 起,输出要加 1。
注意这个公式只能求最后的胜者。要输出完整的出列顺序(如 P1996),递推帮不上忙,只能模拟,或者用树状数组/线段树在 O(n log n) 内定位每次出列的人 —— 那是后面的内容。
顺手记一串数:合法括号序列的个数是卡特兰数 Catalan(n) = C(2n, n)/(n+1)(第 22 节算过)。n = 3 时是 5 种:((()))、(()())、(())()、()(())、()()()。同一串数还在数别的东西 ——n 个数进出栈的合法出栈序列也正好是 Catalan(n) 个:n = 3 时 5 个(312 不合法),n = 4 时 14 个,穷举一遍就能验证。括号匹配的栈深度最大值,就等于序列的最大嵌套层数。
-3.4.+@),现在的代码会怎样?要支持它,需要在读数字时额外维护什么状态?pop() 不返回值意味着什么。给定一个只含加法和乘法、不含括号的表达式,运算符按通常的优先级计算(先乘后加),求表达式的值,输出它除以 10000 的余数。
输入格式:一行字符串,为所求表达式。数字是非负整数、不含前导零,只出现 '+' 和 '*'。
输出格式:一行一个整数,表示表达式的值 mod 10000。
数据范围:表达式长度在 10⁵ 量级,单个数字可能有多位。
从左到右直接算是不行的:1+1*3 从左结合会得 6,正确是 4。必须先算乘法。
整个表达式一定能唯一拆成若干个“加项”之和,每个加项是若干数字的连乘。想办法用一个栈保存“已经定型的加项”,再想乘法该作用在谁身上。
读到 '+' 就结算当前项、开一个新项;读到 '*' 就把当前的数直接乘到栈顶那一项上。结尾别忘了还有最后一个数没有结算。
结构。任何只含 + 和 * 的表达式都能写成 a₁ + a₂ + … + a_k,其中每个 aᵢ 是若干数的连乘(先乘后加,加法是最后一步)。
用一个栈存加项。变量 num 表示正在读的数,op 表示上一个读到的运算符(初始设为 '+',这样第一个数会以一个加项的形式入栈)。
读到运算符 c 时:先按上一个运算符 op 结算 num —— op 是 '+' 就把 num 作为新加项 push;op 是 '*' 就把它乘进栈顶那一项(乘法优先级高,它和栈顶属于同一个加项)。然后把 op 换成 c、num 清零。先结算、再换状态,顺序不能反。
结尾:字符串末尾没有运算符,循环里不会结算最后一个 num,必须再按 op 结算一次 ——漏掉这一步会少加最后一项。
取模。只要后四位,所以每次结算都 mod 10000;加法和乘法对取模是同余的,边算边模与最后取模结果相同,但避免了天文数字溢出。
正确性。栈顶始终是“正在构造的那个加项”,遇到 '+' 就封口并开新的,遇到 '*' 就往里乘。这正是“先乘后加”的栈化,等价于中缀转后缀后再求值。复杂度 O(长度)。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 10000;
int main() {
string s;
getline(cin, s);
stack<long long> st; // 存已经定型的加项,栈顶 = 正在构造的那一项
long long num = 0; // 正在读的数(只保留后四位)
char op = '+'; // 上一个运算符;初值 '+' 让第一个数以加项身份入栈
for (size_t i = 0; i < s.size(); ++i) {
char c = s[i];
if (c >= '0' && c <= '9') {
num = (num * 10 + (c - '0')) % MOD; // 只保留后四位,等价于最后取模
} else if (c == '+' || c == '*') {
// 先按上一个运算符结算 num,再把 op 换成当前字符
if (op == '+') st.push(num); // 新的加项
else st.top() = st.top() * num % MOD; // 上一个运算符是乘:并进栈顶那一项
op = c;
num = 0;
}
// 其他字符(空格、'\r')直接忽略:不让它们被当成运算符
}
// 结尾的最后一个数还没有结算
if (op == '+') st.push(num);
else st.top() = st.top() * num % MOD;
long long ans = 0;
while (!st.empty()) { // 把所有加项加起来
ans = (ans + st.top()) % MOD;
st.pop();
}
cout << ans % MOD << '\n';
return 0;
}| 片段 | 作用 |
|---|---|
char op = '+'; | 虚拟一个“上一个运算符是加号”,于是第一个数会被当成一个新加项 push。少了这个初值,第一个数要么丢失、要么要单独写一段特判。 |
num = (num * 10 + (c - '0')) % MOD; | 多位数拼装 + 随时取模。因为只要后四位,(a × 10 + d) mod 10000 = ((a mod 10000) × 10 + d) mod 10000,所以可以边读边模,不必担心数字有几百位。 |
if (op == '+') st.push(num); | 上一个运算符是加号,说明 num 是一个独立的加项,压栈。 |
else st.top() = st.top() * num % MOD; | 上一个运算符是乘号,说明 num 属于栈顶那一项,直接乘进去。top() 返回引用,所以可以这样赋值;换成 long long t = st.top(); 再 push 就错了 ——那会多出一个加项。 |
op = c; num = 0; | 结算完才更新状态,顺序不能反。先清零再结算,num 就永远是 0。 |
else if (c == '+' || c == '*') | 只认这两个字符。用 else 兜底会把行末的 '\r' 或空格当成运算符,在某些评测环境下直接算错。 |
if (op == '+') st.push(num); // 循环之外 | 字符串末尾没有运算符触发结算,最后一个 num 必须手工处理。这是本题最容易漏的一块,样例 1+1*3+4 漏了会输出 4 而不是 8。 |
ans = (ans + st.top()) % MOD; | 最后求和也要取模。栈里每项都小于 10000、项数最多 10⁵ 量级,总和不超过 10⁹,int 也勉强装得下;但写成 long long 并且边加边模是零成本的保险。 |
else 兜底处理运算符,把行末的 '\r' 或空格当成了运算符:轻则多出一次结算,重则 num 被错误压栈 → WA。