专题9 · 数据结构1
栈与队列都没有发明新的存储,它们只是给"线性表"装上了不同的门:
栈只开一个门(先入后出),队列开前后两道门(先入先出)。
本讲按视频节奏:原理 → 手写实现 → STL → 对比选型 → 两道真题 → 两道拓展。
跟着视频走:五个段落,一条主线
视频 42′54″。主线是「先手写、再用 STL」——只有亲手推过指针怎么动,才知道 STL 那个函数到底替你做了什么。
🎬 视频节奏
- 0–12′ 栈:FILO 原理 → 数组模拟(栈指针)→ STL
stack - 12–25′ 队列:FIFO 原理 → 数组模拟 → 循环队列 → STL
queue - 25–30′ 对比:结构特性、操作逻辑、适用场景、怎么选型
- 30–42′ 例题:表达式括号匹配(栈)/ 猴子选大王(队列)
- 42′+ 拓展:后缀表达式求值 / Blah 数集
🎯 这一讲要拿下什么
- 能徒手用数组写出栈和队列,说清指针每一次怎么动。
- 能解释循环队列为什么要牺牲一个格子,以及判空 / 判满的三件套。
- 看到关键词(匹配、撤销、排队、轮转)能立刻选对结构。
- 括号匹配与约瑟夫环两道真题能独立 AC。
数据结构 = 存储方式 + 允许的操作
数组什么都能干(随机访问 O(1)),但自由也意味着容易写错。栈和队列主动把操作砍到只剩两三种,换来「不容易写错」。
📦 数组
任意位置读 / 写 / 插 / 删。
自由度最高,边界全靠自己。
🥞 栈 Stack
只能从一头进出。
后放进去的先拿出来 —— FILO。
🚶 队列 Queue
从一头进、另一头出。
先来的先走 —— FIFO。
🥞 生活原型:一摞盘子
洗好的盘子一个个往上摞,用的时候只能从最上面拿。最后摞上去的,第一个被拿走 —— 这就是栈。想拿最底下那个?必须先把上面的全拿掉。
🚶 生活原型:排队买票
先来的人站前面,先买到先走;新来的人只能站到队尾,不许插队 —— 这就是队列。处理顺序完全由"到达顺序"决定。
要"按顺序处理、讲先来后到"(排队、轮转、广度优先搜索)→ 队列。
栈:只有一个口的容器
FILO(First In Last Out,先入后出)。唯一能操作的那一端叫栈顶;封死的那一端叫栈底。
🥞 亲手摞一次:只能从上往下拿
📖 术语表(背下来,读题才不卡)
top 指向它。top + 1(top 从 −1 起)。top = -1 时 top+1 = 0 正好是个数,stk[top] 也不会被误读。栈的本质:一个数组 + 一个指针
不需要任何新语法。栈 = int stk[ ] + int top,"删除"只是让指针回退一格。
①
pop() 前不判空 → top 变成 −2 甚至更负,下次 push 写进 stk[-1],数组越界。② 数组开小了 → 一直 push 直到
top = N-1 还继续加,栈溢出。比赛里统一开大一点(如 1e5 + 5)。会手写之后,用 STL 才不会踩空
#include <stack> stack<int> st; ——接口只有 5 个,但有两个坑年年有人踩。
🔧 接口速查
| 接口 | 作用 | 复杂度 |
|---|---|---|
st.push(x) | 入栈 | O(1) |
st.pop() | 出栈 —— 不返回元素! | O(1) |
st.top() | 取栈顶(不删除) | O(1) |
st.size() | 元素个数 | O(1) |
st.empty() | 是否为空 | O(1) |
pop() 返回 void。想"取出并拿到值"必须两步:
int x = st.top(); st.pop();直接写
int x = st.pop(); 会编译报错。🕳️ 另外两个高频坑
top() / pop() 是未定义行为。STL 不会帮你报错,可能返回一个垃圾值或者直接崩。凡是取顶/出栈,先
if(!st.empty())。stack 故意不提供迭代器 —— 想看中间的就得一路 pop 出来再放回去。如果题目确实要遍历,说明你该用 vector 而不是 stack。队列:一头进,另一头出
FIFO(First In First Out,先入先出)。进的一端叫队尾(tail / rear),出的一端叫队首(head / front)——两个指针,各管一头。
🚶 排队演示:不许插队,也不许从队尾溜走
📖 术语 & 与栈的差别
head 指针指着。tail 指针指着(指向下一个空位)。tail - head(普通数组版)。朴素写法有个致命毛病:假溢出
入队 q[tail++] = x;,出队 head++;。看出问题了吗——head 只增不减,前面空出来的格子永远用不上了。
head 和 tail 一起往后爬,很快 tail 就顶到数组末尾(tail == 8)报"队满"——可数组前面明明空着一大片。这不是数组开小了的问题,是指针不回头的问题。解决办法就是下一页的循环队列。
让指针"转圈":取模回到开头
把数组首尾相接想成一个环,指针走到末尾就用 % M 绕回 0。这样 head 前面空出来的格子能被重新利用。
判空是
head == tail。如果允许把 M 个格子全填满,那么"满"也会变成 head == tail —— 空和满长得一模一样,无法区分。所以我们规定:永远留一个格子空着,于是判满 =
(tail + 1) % M == head。代价是容量只有 M − 1,换来的是判断唯一、绝不出错。判空、判满、求个数:三行公式
✍️ 完整模板(背这一段就够)
const int M = 100005; int q[M], head = 0, tail = 0; bool empty(){ return head == tail; } bool full() { return (tail + 1) % M == head; } int size() { return (tail - head + M) % M; } void push(int x){ if(full()) return; // 满了就别写,否则覆盖队首 q[tail] = x; tail = (tail + 1) % M; // ★ 取模回绕 } void pop(){ if(empty()) return; head = (head + 1) % M; // ★ 取模回绕 } int front(){ return q[head]; }
🧠 三个公式怎么来的
head == tail。(tail+1)%M == head。tail < head(绕回来了),所以必须先 +M 再 %M。(tail - head) % M。C++ 里负数取模结果还是负的(
(-1) % 4 == -1),所以必须写成 (tail - head + M) % M。这是复赛里真会 WA 的坑。cnt 变量记个数。判空 cnt==0、判满 cnt==M,就不会混淆了 —— 两种都可以,选一种练熟。queue:接口和 stack 像,但两头都能看
🔧 接口速查
| 接口 | 作用 |
|---|---|
q.push(x) | 入队(放到队尾) |
q.pop() | 出队(删队首,同样不返回) |
q.front() | 看队首(最早来的) |
q.back() | 看队尾(最新来的)★ stack 没有这个 |
q.size() / q.empty() | 个数 / 判空 |
int x=q.front(); q.pop();。🤔 那 STL 的 queue 会假溢出吗?
不会。 STL 的 queue 默认底层是 deque(双端队列),它内部就是分块的循环结构,会自动扩容,你不用管指针回绕。
queue;手写循环队列的价值在于——它让你真正理解 front/back/pop 背后的代价,以及为什么 deque 要那么设计。queue 也不支持遍历,不能 for 一遍。要看所有元素?只能一路 pop 出来 —— 那时请改用 deque 或 vector。同样的输入,出来的顺序完全不同
把 1、2、3、4 依次放进去,再依次取出来 —— 这是区分两者最直观的实验。
🥞 栈:只有右边一个口
🚶 队列:左出右进两个口
结构
栈:一头开口
队列:两头开口
指针
栈:1 个(top)
队列:2 个(head/tail)
顺序
栈:FILO(逆序)
队列:FIFO(保序)
典型场景
栈:括号匹配、撤销、DFS
队列:排队模拟、BFS
为什么括号匹配一定要用栈?
因为"后出现的左括号,要先被配对"——这正好是 FILO。最里面的那层括号,必须最先闭合。
① 表达式(当前字符高亮)
② 栈里存着"还没配对的左括号"
( → 入栈;② 遇到 ) → 若栈空则立刻判 NO,否则弹出一个;③ 扫完(遇到 @)→ 栈空才 YES,栈非空说明左括号多了。20 行:把三条规则直接翻译成代码
✍️ 标程(数组模拟栈)
char stk[300]; // 左括号 < 20,开 300 稳 int main(){ string s; cin >> s; int top = -1; // 空栈 for(int i=0;i<(int)s.size();i++){ char c = s[i]; if(c == '@') break; // 结束符 if(c == '('){ stk[++top] = c; // ① 入栈 } else if(c == ')'){ if(top < 0){ // ② 栈空 = 右括号多 cout << "NO "; return 0; } top--; // 弹出配对 } } cout << (top < 0 ? "YES" : "NO") << ' '; return 0; }
(,所以 top 本身就能当计数器,写成 int cnt 也完全正确。保留数组是为了让你看清栈的形状。🕳️ 这题的三个失分点
Yes 直接 0 分——题目怎么说就怎么输出。@ 之后的字符。本题 @ 是最后一个,不影响;但养成习惯:遇到结束符就 break。) ( 会在第一步就多出一个右括号 —— 必须边扫边判,否则会误判成 YES。)( 数量相等但顺序错。栈之所以必须,是因为它同时记住了数量和顺序——"最近的未配对左括号"就是栈顶。"围成一圈"翻译成队列:队首出、队尾进
报数没报到 m 的猴子出队再入队(等于走到队尾),报到 m 的出队不再回来(淘汰)。一圈就这么绕出来了。
🐒 围成一圈看(顶部 = 当前报数的猴子)
📥 队列的真实样子(左队首 → 右队尾)
while(q.size() > 1)。只剩 1 个时它就是大王,直接 cout << q.front()。⚠️ 注意:报数计数器
k 在淘汰后要重置为 1,在"回到队尾"后要 k++ —— 这两个分支别写反。12 行:队列模拟约瑟夫环
✍️ 标程(STL queue)
#include <queue> int n, m; cin >> n >> m; queue<int> q; for(int i=1;i<=n;i++) q.push(i); // 编号入队 int k = 1; // 当前报的数 while((int)q.size() > 1){ int x = q.front(); q.pop(); // 队首出队 if(k == m){ // 报到 m:淘汰 k = 1; // ★ 重新从 1 报 } else { // 没报到:回队尾 q.push(x); k++; // ★ 报数 +1 } } cout << q.front() << ' ';
🤔 为什么这题用队列、不用栈?
因为规则是"按顺序轮转"——这一轮没轮到的,要排到后面去等着,而不是立刻回头处理它。这正是 FIFO。
如果用栈,被压下去的猴子会最先被弹出来,顺序就反了。
① 把
k = 1 写成 k = 0:报数是从 1 开始的,重置也必须是 1。② 淘汰后忘了重置 k:会一路报下去,答案立刻错。
front/pop/push 换成你自己维护的 head/tail。f(1)=0; f(i)=(f(i-1)+m)%i,答案是 f(n)+1,复杂度 O(n)。但复赛建议先写队列模拟——它不容易写错,且 n 不大时一样过。递推解留作拓展。两道挑战题:方向给你,代码自己写
🧮 拓展 1:后缀表达式求值
后缀表达式(逆波兰式)把运算符放在操作数后面,如 3 4 + 5 * = 35,不需要括号。
b(先弹的是右操作数)、pop 出 a,算 a op b,push 回去;③ 最后栈里只剩一个数就是答案。🔢 拓展 2:Blah 数集
数集从 a 开始,每次产生 2x+1 和 3x+1,要求从小到大输出第 n 项。
2x+1 放一个队列 P、3x+1 放另一个队列 Q。两个队列各自都是递增的(因为 x 递增),所以每次只需比较两个队首取小的。题干出现"最近的 / 上一个 / 嵌套 / 回溯" → 栈;出现"依次 / 轮流 / 排队 / 按顺序最小" → 队列。
一道题 100 分是分档给的——不会正解也要先拿保底
复赛一道题的 100 分由十几到二十几个测试点组成, 前面几档专门为「没想出正解的人」准备。四道题各拿 30~40 分, 加起来往往就是一等奖和二等奖的分界。
① 特判档:先别想算法,看数据范围表
题目给的数据范围表就是出题人给你的送分清单。看到「n = 1」「只有一组数据」这类一行,
就先写个 if 直接输出答案。2 分钟换 5~10 分,全场最划算。
② 暴力档:按题意最直白地写一遍
多重循环、DFS 全枚举、朴素 O(n³)……不要优化,只要保证最小那档全对。 它既是保底分,又是后面对拍的标尺——没有它你无法证明正解是对的。
③ 特殊性质档:题目里那句「若……」
常见的送分性质:数据已经有序、所有值完全相同、规模小到可以 O(n²)、 只出现一种类型。题目写出来就是让你拿的,专门写一份即可。
④ 正解档:思路定了,就赢了八成
考场上的时间几乎都花在想思路上,不是写代码。写完先跑样例, 再用第 ② 档的暴力版造小数据对拍——这是唯一能证明你思路对的办法。
本讲 2 道题,各自的四档怎么走
表达式括号匹配 · BRACKET
第1档 · 2 分钟
输入只有 @ → Yes
第2档 · 8 分钟
反复扫描删除相邻的 (),删不动了看是否为空
第3档 · 10 分钟
只有一种括号 → 可以不记类型,单纯计数(前缀非负且总数相等)
第4档 · 15 分钟
栈:遇 ( 入栈,遇 ) 看栈顶,最后栈空
猴子选大王 · MONKEY
第1档 · 2 分钟n == 1 → 1
第2档 · 8 分钟
数组模拟,每次数 m 个删一个,n ≤ 10⁴ 稳过
第3档 · 10 分钟m == 2 → 有 O(1) 公式 2·(n−2^⌊log₂n⌋)+1
第4档 · 15 分钟
队列模拟:没数到 m 的出队再入队,数到 m 的出局,直到剩一个
0–5 分钟 读题 + 圈出数据范围表里最小的那几档 → 5–15 分钟 写完第 1、2 档并先交一次(保住 20~30 分)→ 15–40 分钟 冲第 3、4 档 → 40–50 分钟 造小数据对拍、检查
freopen 与文件名。本讲知识树
① 循环队列数组大小 M=8,最多能存几个元素?
② 括号匹配中,字符串 ) ( 用"只统计左右括号个数是否相等"来判断,结果是?
③ 用 STL 容器时,"取出并获得值"的正确写法是?