Lesson 0011 · 栈、队列与优先队列

C++ 进阶 — 第 4 课

三句话速览
  1. stack 后进先出:最后放进来的先出去。
  2. queue 先进先出:先来的先处理,BFS 靠它。
  3. priority_queue 按值出,默认是大顶堆。
上节课:递归(0010)—— 函数自己调自己,把大问题缩成小问题,但必须有出口。
这节课:栈(后进先出)、队列(先进先出)、优先队列(按大小出)。三者都是操作受限的容器:不像 vector 那样什么都能干,只留下该有的动作,每种解决一类特定问题。

1. stack — 后进先出(LIFO)

像一摞盘子:最后放上去的,最先拿下来。

#include <stack>

stack<int> s;
s.push(1);           // 放入 1
s.push(2);           // 放入 2
s.push(3);           // 放入 3
cout << s.top();     // 3(只看栈顶,不删)
s.pop();             // 弹出栈顶(删掉 3)
cout << s.size();    // 2(还剩 1, 2)
操作代码说明
入栈s.push(x)放到栈顶
出栈s.pop()删栈顶,不返回值
看栈顶s.top()返回栈顶元素
大小s.size()现在还剩几个元素
是否空s.empty()空的时候返回 true

经典应用:括号匹配

bool check(string s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(') st.push(c);
        else if (c == ')') {
            if (st.empty()) return false;  // 右括号多了
            st.pop();
        }
    }
    return st.empty();                     // 栈空才算匹配
}

为什么用栈?因为一个右括号只能和「最近一个还没配对的左括号」配对 —— 最近的先出,正好是后进先出。最后那行 return st.empty(); 是在查「左括号多了」的情况,两个方向都要查。

2. queue — 先进先出(FIFO)

像排队:先来的先处理。

#include <queue>

queue<int> q;
q.push(1);           // 入队
q.push(2);
q.push(3);
cout << q.front();   // 1(队首,不删)
cout << q.back();    // 3(队尾,不删)
q.pop();             // 出队(删掉 1)
cout << q.front();   // 2
操作代码说明
入队q.push(x)放到队尾
出队q.pop()删队首,不返回值
队首q.front()返回队首元素
队尾q.back()返回队尾元素

注意和栈一样:pop() 只负责删,不负责把删掉的东西交给你。想要那个值,得先用 front() / top() 存一份。

3. priority_queue — 每次取最大(或最小)

不是按「先来后到」,而是按值的大小出队。

#include <queue>         // priority_queue 也在 queue 头文件里

priority_queue<int> pq;
pq.push(3);
pq.push(5);
pq.push(1);
cout << pq.top();       // 5(最大)
pq.pop();
cout << pq.top();       // 3

默认大顶堆(大的先出)。想要小的先出:

priority_queue<int, vector<int>, greater<int>> pq;  // 小顶堆

三个模板参数要写全:元素类型、底层容器、比较器。少写一个都编译不过 —— 这也是初学者最常卡住的一行。

操作代码复杂度
入队pq.push(x)O(log n)
出队pq.pop()O(log n)
看最大pq.top()O(1)

为什么 top() 是 O(1) 而 push / pop 是 O(log n)?因为堆只保证「堆顶最大」,一看就有答案;而进来一个、出去一个,都要沿着树往上或往下挪几层,层数是 log n。

4. 三者对比

stackqueuepriority_queue
出队规则后进先出先进先出按大小
日常类比摞盘子排队急诊(重的先看)
经典题括号匹配BFS 广搜Top K、Dijkstra
只暴露栈顶一头队首、队尾两头堆顶一头

5. 用 vector 模拟 stack

stack 底层就是 vector,只是把 push_back 改名 push,把 pop_back 改名 pop,然后禁掉中间插入:

vector<int> v;
v.push_back(1);   v.push_back(2);    // 代替 push
cout << v.back();                    // 代替 top()
v.pop_back();                         // 代替 pop()
理解本质:栈和队列不是新数据结构,只是限制了 vector 的操作方式。限制 = 防错 = 代码意图更清晰。

6. 坑点清单

7. 小测

Q1. stack: push(1) push(2) push(3) pop() pop() top() 结果?
Q2. BFS 广搜通常用哪个容器?
Q3. priority_queue 默认是什么堆?

8. 动手练习

题目1:洛谷 P1739 表达式括号匹配

输入含 () 的表达式,判断括号是否匹配。
用 stack 搞定:遇 ( 入栈,遇 ) 出栈。
题目2:洛谷 P1090 合并果子

每次选最小的两堆合并,用 小顶堆 priority_queue 一行搞定。
提示:priority_queue<int, vector<int>, greater<int>> pq;

选做:把第 5 节的 vector 写法包成一个 struct MyStack,只公开 push / pop / top / size / empty,内部用 vector 存。写完你就明白「限制操作」是什么意思了。

卡住时的自查顺序

  1. 括号题先跑 ()、)(、(() 三组样例,看三个答案是不是都对的。
  2. 每次 pop() / top() / front() 之前问一句:这时候容器会不会是空的?
  3. 最后有没有 return st.empty();?漏了它,左括号多的串会被判成匹配。
  4. 优先队列要小顶堆,三个模板参数写全了吗?编译错误多半在这一行。
  5. 答案对但顺序不对:是不是把队列当栈用了(或者反过来)?把操作序列抄在纸上画一遍。
  6. 都不是:把输入、期望输出、你的输出三样一起贴过来。

本课小结

下一课预告
0012 复杂度分析入门 —— 这一课一直在说 O(log n)、O(1),下一课把这个记号讲透:怎么数一段循环是几阶、常见复杂度对照表、以及「n 多大时哪种阶会超时」的洛谷经验法则。
别小看它:后面每一课选算法,都是先估一阶再动手。

← 上一课:递归 · 课程总览

头文件 <stack>(栈)· <queue>(队列和优先队列)