栈(stack)、队列(queue)和优先级队列(priority_queue)是C++ STL中非常经典的容器适配器,也是数据结构与算法面试、笔试中的高频考点。它们底层依赖其他容器实现,对外提供统一、简洁的接口,分别满足后进先出、先进先出、优先级优先三种不同的数据访问需求。
本文将从原理介绍、常用接口、模拟实现、经典面试题四个维度,详细梳理 stack、queue、priority_queue 的核心知识点,并附上可直接运行的代码实现,帮助大家彻底掌握这三种容器适配器。
1. stack(栈)的介绍与使用
1.1 stack 的本质
• 后进先出(LIFO):只能在栈顶(top)进行插入(push)和删除(pop)操作。
• 是一种容器适配器,而非独立容器,STL 默认使用 deque 作为底层容器。
1.2 stack 核心接口
|-----------|--------------|
| 函数声明 | 接口说明 |
| stack() | 构造空栈 |
| empty() | 检测栈是否为空 |
| size() | 返回栈中元素个数 |
| top() | 返回栈顶元素的引用 |
| push(val) | 将元素 val 压入栈顶 |
| pop() | 将栈顶元素弹出 |
代码示例:
cpp
#include <iostream>
#include <stack> // 必须包含此头文件
using namespace std;
int main() {
// 1. stack() - 构造空栈
stack<int> st;
// 2. empty() - 检测栈是否为空
if (st.empty()) {
cout << "初始状态:栈为空" << endl;
}
// 3. push(val) - 将元素压入栈顶
cout << "依次压入 10, 20, 30..." << endl;
st.push(10);
st.push(20);
st.push(30);
// 4. size() - 返回栈中元素个数
cout << "当前栈的大小:" << st.size() << endl;
// 5. top() - 返回栈顶元素的引用
// 注意:top() 只获取不删除,栈不为空时才能调用
cout << "栈顶元素是:" << st.top() << endl;
// 修改变量(演示引用特性)
st.top() = 999;
cout << "修改栈顶元素后,新的栈顶是:" << st.top() << endl;
// 6. pop() - 将栈顶元素弹出
// 注意:pop() 只删除不返回,栈不为空时才能调用
cout << "执行弹出操作..." << endl;
st.pop();
cout << "弹出后,新的栈顶是:" << st.top() << endl;
cout << "弹出后,栈的大小:" << st.size() << endl;
return 0;
}
运行结果:
cpp
初始状态:栈为空
依次压入 10, 20, 30...
当前栈的大小:3
栈顶元素是:30
修改栈顶元素后,新的栈顶是:999
执行弹出操作...
弹出后,新的栈顶是:20
弹出后,栈的大小:2
⚠️注意:
-
top() 与 pop() 的分工:STL 的设计理念是"职责单一"。top() 只负责读取栈顶,pop() 只负责删除栈顶。如果想"取出并删除",需要先 top() 再 pop()。
-
空栈调用风险:在调用 top() 和 pop() 之前,必须先用 empty() 判断栈是否为空,否则会导致程序崩溃(未定义行为)。
-
引用特性:top() 返回的是引用,因此可以直接修改栈顶元素的值(如示例中将 30 改为 999)。
1.3 经典例题:最小栈(MinStack)
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack()初始化堆栈对象。void push(int val)将元素val推入堆栈。void pop()删除堆栈顶部的元素。int top()获取堆栈顶部的元素。int getMin()获取堆栈中的最小元素。
思路:
最小栈的要求是:在 O(1) 时间内获取栈中最小值,同时支持正常的 push、pop、top 操作。
普通栈只能在栈顶操作,无法直接拿到最小值,因此需要用两个栈配合来实现。
- 整体设计:主栈 _st:存所有真实数据,保证正常的栈功能。辅助栈 _min:专门存当前状态下的最小值,栈顶永远是整个栈的最小值。
- push 思路:新元素先正常压入主栈。如果辅助栈为空,直接压入。如果新元素 ≤ 辅助栈栈顶,也压入辅助栈。用 ≤ 而不是 <,是为了处理重复最小值的情况,保证 pop 时不会出错。
- pop 思路:先看主栈栈顶是否等于辅助栈栈顶。如果相等,说明要弹出的是当前最小值,辅助栈也要一起弹出。最后主栈正常弹出。
- top / getMin 思路:top():直接返回主栈栈顶。getMin():直接返回辅助栈栈顶。两个操作都是 O(1)。
- 核心思想总结:用空间换时间:用一个额外的栈,专门维护每一步的最小值,让获取最小元素从遍历 O(n) 变成直接取栈顶 O(1)。
cpp
#include <stack>
using namespace std;
/**
* @brief 最小栈:支持 push、pop、top 操作,并能在常数时间 O(1) 内检索到最小元素。
* @note 核心思路:使用两个栈,一个存储数据,一个存储当前的最小值序列。
*/
class MinStack {
public:
/**
* @brief 构造函数:默认构造即可,栈的初始化由其自身的构造函数完成。
*/
MinStack() {}
/**
* @brief 入栈操作
* @param val 要压入栈的数值
*/
void push(int val) {
// 1. 无论如何,先将数据压入主数据栈
_st.push(val);
// 2. 关键逻辑:维护最小值栈
// 条件1:_min为空(首次入栈),必须压入
// 条件2:新值小于等于当前最小值,也需要压入(小于等于处理重复最小值的情况)
if (_min.empty() || val <= _min.top()) {
_min.push(val);
}
}
/**
* @brief 出栈操作
* @note 必须同步维护最小值栈,防止最小值被弹出后,_min栈顶失效。
*/
void pop() {
// 1. 关键判断:如果要弹出的元素是当前的最小值
if (_st.top() == _min.top()) {
// 则最小值栈也需要同步弹出栈顶
_min.pop();
}
// 2. 主数据栈执行出栈
_st.pop();
}
/**
* @brief 获取栈顶元素
* @return 栈顶元素的值
* @note 直接返回主数据栈的栈顶
*/
int top() {
return _st.top();
}
/**
* @brief 检索栈中的最小元素
* @return 栈中的最小值
* @note 时间复杂度 O(1),直接返回最小值栈的栈顶
*/
int getMin() {
return _min.top();
}
private:
stack<int> _st; // 主栈:存储所有入栈的元素
stack<int> _min; // 辅助栈:存储对应主栈状态下的最小值,栈顶始终是当前的最小值
};
1.4 经典例题:栈的压入弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
-
0<=pushV.length == popV.length <=1000
-
-1000<=pushV[i]<=1000
-
pushV 的所有数字均不相同
思路:
• 用一个辅助栈 st 模拟真实栈的操作。
• 遍历入栈序列 pushV,逐个将元素压入栈。
• 每次压入后,检查栈顶是否与当前需要弹出的元素(popV[popi])一致:
◦ 如果一致,就弹出栈顶,并移动弹出序列的指针 popi。
◦ 重复这个过程,直到栈顶不匹配或栈为空。
• 最后,如果辅助栈 st 为空,说明所有元素都按顺序弹出了,返回 true;否则返回 false。
cpp
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pushV int整型vector
* @param popV int整型vector
* @return bool布尔型
*/
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
size_t popi = 0; // 指向弹出序列 popV 的当前位置
stack<int> st; // 辅助栈,模拟真实栈的操作
for (auto& e : pushV) {
// 入栈
st.push(e);
// 跟出栈序列匹配
while (!st.empty() && st.top() == popV[popi]) {
st.pop();
popi++;
}
}
return st.empty();
// 如果 st.empty() 为 true,说明所有入栈元素都被成功弹出,弹出序列合法。
// 如果 st 不为空,说明还有元素无法按弹出序列顺序弹出,序列不合法。
}
};
• 模拟法:严格按照栈的"后进先出"规则,模拟每一步操作,用结果验证序列的合法性。
• 时间复杂度:每个元素最多入栈和出栈一次,因此时间复杂度为 O(n)。
• 空间复杂度:最坏情况下(如入栈序列和弹出序列完全相反),需要 O(n) 的辅助栈空间。
1.5 经典例题:逆波兰表达式求值
利用栈计算后缀表达式(逆波兰式)的值。
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for (size_t i = 0; i < tokens.size(); ++i) {
string& str = tokens[i];
// 如果是数字,入栈
if (str != "+" && str != "-" && str != "*" && str != "/") {
s.push(atoi(str.c_str()));
} else {
// 如果是运算符,弹出两个数计算
int right = s.top(); s.pop();
int left = s.top(); s.pop();
switch (str[0]) {
case '+': s.push(left + right); break;
case '-': s.push(left - right); break;
case '*': s.push(left * right); break;
case '/': s.push(left / right); break; // 题目保证除数不为0
}
}
}
return s.top();
}
};
1.6 二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路:用队列 + 记录每一层节点个数。
-
队列:用来按顺序存下一层要遍历的节点。
-
levelSize:记录当前这一层一共有多少个节点,保证一层遍历完再遍历下一层。
-
每处理完一层,就把这一层的结果放进二维数组,最终返回。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x, left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
// 存放最终结果:每一层对应一个vector<int>
vector<vector<int>> vv;
// 队列:用于层序遍历,存储节点指针
queue<TreeNode*> q;
// 记录当前层有多少个节点
int levelSize = 0;
// 如果根节点不为空,先把根入队,第一层节点数为1
if(root)
{
q.push(root);
levelSize = 1;
}
// 队列不为空就继续遍历
while(!q.empty())
{
// 用来存当前这一层的节点值
vector<int> v;
// 遍历当前层的所有节点(levelSize个)
while(levelSize--)
{
// 取队头节点
TreeNode* front = q.front();
q.pop();
// 把当前节点的值放入当前层数组
v.push_back(front->val);
// 左孩子不为空,入队(下一层)
if(front->left)
q.push(front->left);
// 右孩子不为空,入队(下一层)
if(front->right)
q.push(front->right);
}
// 当前层遍历完,把这一层结果放进最终二维数组
vv.push_back(v);
// 更新下一层的节点个数 = 现在队列里的元素个数
levelSize = q.size();
}
return vv;
}
};
举例:
cpp
3
/ \
9 20
/ \
15 7
我们要得到的层序遍历结果是:
[
[3],
[9,20],
[15,7]
]
对着例子走一遍流程
1)第 0 层:[3] 队列:[3]; levelSize = 1
处理 3:存入 v → [3];左 9、右 20 入队; 队列变成:[9,20]; levelSize 更新为 2;vv 变成:[[3]]
2)第 1 层:[9,20] levelSize = 2
处理 9: 存入 v → [9];无孩子
处理 20:存入 v → [9,20];左 15、右 7 入队;队列变成:[15,7];levelSize 更新为 2;vv 变成:[[3], [9,20]]
3)第 2 层:[15,7] levelSize = 2
处理 15 → 存入 处理 7 → 存入 ;都没孩子,队列空; vv 变成:
cpp
[
[3],
[9,20],
[15,7]
]
• 数据结构:队列 + 二维数组
• 关键点:用 levelSize 控制每一层的遍历范围
• 时间复杂度:O(n),每个节点只访问一次
• 空间复杂度:O(n),队列最多存一层节点
1.7 stack的模拟实现
用 vector 模拟栈(仅尾插尾删,符合栈的特性):
cpp
#include <vector>
namespace gxy {
template<class T>
class stack {
public:
stack() {}
void push(const T& x) { _c.push_back(x); }
void pop() { _c.pop_back(); }
T& top() { return _c.back(); }
const T& top() const { return _c.back(); }
size_t size() const { return _c.size(); }
bool empty() const { return _c.empty(); }
private:
std::vector<T> _c;
};
}
stack(容器适配器版)
cpp
#pragma once // 防止头文件被重复包含
#include <deque> // 默认底层容器使用 deque
namespace gxy
{
// 栈的模拟实现 ------ 容器适配器
// 第二个模板参数 Container:底层存储的容器
// 默认使用 deque<T>,也可以传 vector、list
template<class T, class Container = deque<T>>
class stack
{
public:
// 入栈:尾插
void push(const T& x)
{
// 底层容器调用尾插,适配成栈的push
_con.push_back(x);
}
// 出栈:尾删
void pop()
{
// 底层容器调用尾删,适配成栈的pop
_con.pop_back();
}
// 获取栈顶元素(只读)
const T& top() const
{
// 栈顶 = 底层容器的最后一个元素
return _con.back();
}
// 获取栈中元素个数
size_t size() const
{
return _con.size();
}
// 判断栈是否为空
bool empty() const
{
return _con.empty();
}
private:
// 底层存储的容器(适配器的核心:不自己实现结构,复用容器)
Container _con;
};
}
核心知识点整理
(1)这是「容器适配器」,不是自己实现数据结构
• stack 不直接实现内存管理; 复用已有的容器(deque / vector / list); 只开放栈需要的接口:push / pop / top 等; 封闭其他接口,实现后进先出 LIFO
(2)模板参数意义 template<class T, class Container = deque<T>>
T:栈里存储的数据类型 Container:底层存储的容器,默认是 deque
可以手动改成别的容器:stack<int, vector<int>> st1; stack<int, list<int>> st2;
(3)接口与底层容器对应关系
|---------|-------------------|------|
| stack接口 | 底层容器调用 | 功能 |
| push(x) | _con.push_back(x) | 入栈 |
| pop() | _con.pop_back() | 出栈 |
| top() | _con.back() | 获取栈顶 |
| size() | _con.size() | 元素个数 |
| empty() | _con.empty() | 是否为空 |
stack 是容器适配器,它不自己实现数据结构,只要求底层容器提供 push_back、pop_back、back,然后把这些接口封装成栈的 push、pop、top,从而实现后进先出。
(4)为什么默认用 deque?
头尾插入删除都是 O(1);扩容代价小,不需要拷贝大量数据;比 vector 更适合做栈、队列的底层容器
• vector:扩容要拷贝,效率一般;list:空间碎片多,缓存不友好;deque:头尾都快,扩容不拷贝,综合最好
2. queue(队列)的介绍与使用
2.1 queue 的本质
• 先进先出(FIFO):队尾(back)入队,队头(front)出队。
• 也是容器适配器,STL 默认使用 deque 作为底层容器。
2.2 queue 核心接口
|-----------|---------------|
| 函数声明 | 接口说明 |
| queue() | 构造空队列 |
| empty() | 检测队列是否为空 |
| size() | 返回队列中有效元素个数 |
| front() | 返回队头元素的引用 |
| back() | 返回队尾元素的引用 |
| push(val) | 在队尾将元素 val 入队 |
| pop() | 将队头元素出队 |
代码示例:
cpp
#include <iostream>
#include <queue>
using namespace std;
int main() {
// 1. queue() ------ 构造空队列
queue<int> q;
// 2. empty() ------ 检测队列是否为空
if (q.empty()) {
cout << "队列初始为空" << endl;
}
// 3. push(val) ------ 在队尾将元素 val 入队
q.push(10);
q.push(20);
q.push(30);
cout << "入队 10、20、30" << endl;
// 4. size() ------ 返回队列中有效元素个数
cout << "队列大小:" << q.size() << endl;
// 5. front() ------ 返回队头元素的引用
cout << "队头元素:" << q.front() << endl;
// 6. back() ------ 返回队尾元素的引用
cout << "队尾元素:" << q.back() << endl;
// 修改队头、队尾(因为返回的是引用)
q.front() = 1;
q.back() = 3;
cout << "修改后队头:" << q.front() << endl;
cout << "修改后队尾:" << q.back() << endl;
// 7. pop() ------ 将队头元素出队
q.pop();
cout << "执行一次出队后,新队头:" << q.front() << endl;
cout << "队列大小:" << q.size() << endl;
return 0;
}
运行结果:
cpp
队列初始为空
入队 10、20、30
队列大小:3
队头元素:10
队尾元素:30
修改后队头:1
修改后队尾:3
执行一次出队后,新队头:20
队列大小:2
2.3 queue 的模拟实现
用 list 模拟队列(支持头删尾插,符合队列特性):
cpp
#include <list>
namespace gxy {
template<class T>
class queue {
public:
queue() {}
void push(const T& x) { _c.push_back(x); }
void pop() { _c.pop_front(); }
T& back() { return _c.back(); }
const T& back() const { return _c.back(); }
T& front() { return _c.front(); }
const T& front() const { return _c.front(); }
size_t size() const { return _c.size(); }
bool empty() const { return _c.empty(); }
private:
std::list<T> _c;
};
}
queue 容器适配器模拟实现
cpp
#pragma once // 防止头文件重复包含
#include <deque> // 默认底层容器使用 deque
namespace gxy
{
// 队列的模拟实现 ------ 容器适配器
// 底层容器默认使用 deque<T>
template<class T, class Container = deque<T>>
class queue
{
public:
// 入队:尾插
void push(const T& x)
{
_con.push_back(x);
}
// 出队:头删
void pop()
{
_con.pop_front();
}
// 获取队头元素(只读)
const T& front() const
{
return _con.front();
}
// 获取队尾元素(只读)
const T& back() const
{
return _con.back();
}
// 获取元素个数
size_t size() const
{
return _con.size();
}
// 判断队列是否为空
bool empty() const
{
return _con.empty();
}
private:
Container _con; // 底层存储容器
};
}
核心说明
- queue 为什么是容器适配器?
自己不实现底层结构;复用已有容器的接口,封装成先进先出 FIFO
- 底层容器必须支持的接口
push_back 尾插; pop_front 头删; front 取队头; back 取队尾; size / empty
- 为什么不能用 vector 做 queue 底层?
因为 vector 不支持 pop_front()(或效率极低 O(N))。所以 queue 底层只能用:deque / list。
- 接口对应关系
|---------|-----------|------|
| queue接口 | 底层容器调用 | 功能 |
| push(x) | push_back | 入队 |
| pop() | pop_front | 出队 |
| front() | front() | 取队头 |
| back() | back() | 取队尾 |
| size() | size() | 元素个数 |
| empty() | empty() | 是否为空 |
3. priority_queue(优先级队列)的介绍与使用
3.1 priority_queue 的本质
• 是一个堆,默认是大顶堆(堆顶元素最大)。
• 也是容器适配器,默认使用 vector 作为底层容器,并在其上维护堆结构。
3.2 priority_queue 核心接口
|------------------|---------------|
| 函数声明 | 接口说明 |
| priority_queue() | 构造空优先级队列 |
| empty() | 检测是否为空 |
| size() | 返回元素个数 |
| top() | 返回堆顶元素(最大/最小) |
| push(x) | 插入元素并调整堆 |
| pop() | 删除堆顶元素并调整堆 |
代码示例:
cpp
#include <iostream>
#include <queue>
using namespace std;
int main() {
// 1. priority_queue() ------ 构造空优先级队列(默认:大顶堆)
priority_queue<int> pq;
// 2. empty() ------ 检测是否为空
if (pq.empty()) {
cout << "优先级队列初始为空" << endl;
}
// 3. push(x) ------ 插入元素并自动调整堆结构
pq.push(5);
pq.push(2);
pq.push(8);
pq.push(1);
cout << "插入 5、2、8、1 完成" << endl;
// 4. size() ------ 返回元素个数
cout << "元素个数:" << pq.size() << endl;
// 5. top() ------ 返回堆顶元素(大顶堆:最大值)
cout << "堆顶元素:" << pq.top() << endl;
// 6. pop() ------ 删除堆顶元素,并重新调整堆
pq.pop();
cout << "删除堆顶后,新堆顶:" << pq.top() << endl;
// 再删一次看变化
pq.pop();
cout << "再删堆顶后,新堆顶:" << pq.top() << endl;
return 0;
}
运行结果:
cpp
优先级队列初始为空
插入 5、2、8、1 完成
元素个数:4
堆顶元素:8
删除堆顶后,新堆顶:5
再删堆顶后,新堆顶:2
小顶堆写法:
cpp
// 小顶堆(堆顶是最小值)
priority_queue<int, vector<int>, greater<int>> pq;
3.3 大顶堆与小顶堆
• 大顶堆(默认):priority_queue<int> q;
• 小顶堆:需要指定比较方式 greater<T>:priority_queue<int, vector<int>, greater<int>> q;
cpp
#include <iostream>
#include <algorithm>
using namespace std;
// 仿函数:本质是一个类,重载 operator(),可以像函数一样使用
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
// 用仿函数控制升序降序
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{
for (int j = 0; j < n; j++)
{
int flag = 0;
for (int i = 1; i < n - j; i++)
{
if (com(a[i], a[i - 1]))
{
swap(a[i - 1], a[i]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
int main()
{
Less<int> LessFunc;
Greater<int> GreaterFunc;
// 像函数一样使用
cout << LessFunc(1, 2) << endl;
cout << LessFunc.operator()(1, 2) << endl;
int a[] = { 9,1,2,5,7,4,6,3 };
// 升序
BubbleSort(a, 8, LessFunc);
// 降序
BubbleSort(a, 8, GreaterFunc);
// 匿名对象调用
BubbleSort(a, 8, Less<int>());
BubbleSort(a, 8, Greater<int>());
return 0;
}
代码解释:
一、代码整体是干嘛的?为了讲清楚:什么是仿函数(函数对象)?为什么要有仿函数?怎么用?
并用冒泡排序演示:一套排序代码,通过传入不同仿函数,就能控制升序 / 降序。
二、逐段超详细解释
- 仿函数的定义
cpp
// 仿函数:本质是一个类,重载 operator(),可以像函数一样使用
template<class T>
class Less
{
public:
// 重载括号运算符
bool operator()(const T& x, const T& y)
{
return x < y; // 小于
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y; // 大于
}
};
• 仿函数 = 重载了 operator() 的类; 它的对象可以像函数一样调用:LessFunc(1, 2);
• 作用:把"比较规则"封装成一个对象,方便传给算法。
- 模板冒泡排序(接收仿函数)
cpp
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{
for (int j = 0; j < n; j++)
{
int flag = 0;
for (int i = 1; i < n - j; i++)
{
// 不再写死 a[i] < a[i-1] 或 >
// 而是调用仿函数来判断
if (com(a[i], a[i - 1]))
{
swap(a[i - 1], a[i]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
• 以前排序:if (a[i] < a[i-1]) swap(); // 只能升序
• 现在:if (com(a[i], a[i-1])) 比较规则由外面传进来,排序逻辑不用改!
- main 函数使用
cpp
int main()
{
Less<int> LessFunc; // 创建仿函数对象
Greater<int> GreaterFunc;
// 像函数一样调用,本质是:LessFunc.operator()(1,2)
cout << LessFunc(1, 2) << endl;
cout << LessFunc.operator()(1, 2) << endl;
int a[] = { 9,1,2,5,7,4,6,3 };
// 传 Less → 升序
BubbleSort(a, 8, LessFunc);
// 传 Greater → 降序
BubbleSort(a, 8, GreaterFunc);
// 直接用匿名对象(最常用)
BubbleSort(a, 8, Less<int>());
BubbleSort(a, 8, Greater<int>());
return 0;
}
-
仿函数对象可以像函数一样调用 LessFunc(1,2)
-
一套排序,两种规则: 传 Less → 升序; 传 Greater → 降序
-
匿名对象写法(STL 标准风格)
cpp
Less<int>()
Greater<int>()
三、总结
-
仿函数:一个类,重载了 operator(),对象可以像函数一样使用。
-
作用:把比较逻辑/策略封装成对象,传给算法。
-
好处:一套算法代码,支持多种比较规则,不用改源码。
-
STL 到处都用:sort、priority_queue、map......全都靠仿函数控制规则。
3.4 priority_queue 模拟实现(大堆/小堆 + 仿函数 + 向上/向下调整)
priority_queue.h
cpp
#pragma once
#include <vector>
#include <algorithm> // swap
// 小于比较仿函数:控制建立 大堆
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
// 大于比较仿函数:控制建立 小堆
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
namespace gxy
{
// 优先级队列模拟实现(底层是堆)
// 默认容器:vector
// 默认比较方法:Less<T> → 建立 大顶堆
template<class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
// 向上调整算法(插入时用)
void AdjustUp(int child)
{
// 定义比较对象(用传入的仿函数)
Compare com;
// 父节点下标
int parent = (child - 1) / 2;
// 一直往上调整到根节点
while (child > 0)
{
// 通过仿函数比较:
// Less → parent < child → 交换 → 大堆
if (com(_con[parent], _con[child]))
{
// 交换父子节点
std::swap(_con[child], _con[parent]);
// 继续向上调整
child = parent;
parent = (child - 1) / 2;
}
else
{
// 满足堆性质,停止调整
break;
}
}
}
// 入队:尾插 + 向上调整
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
// 向下调整算法(删除堆顶时用)
void AdjustDown(int parent)
{
Compare com;
size_t child = parent * 2 + 1; // 左孩子
while (child < _con.size())
{
// 找出较大/较小的孩子(由仿函数决定)
if (child + 1 < _con.size()
&& com(_con[child], _con[child + 1]))
{
++child;
}
// 父节点与孩子比较
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 出队(删除堆顶)
void pop()
{
// 1. 堆顶与最后一个元素交换
std::swap(_con[0], _con[_con.size() - 1]);
// 2. 删除最后一个元素(原堆顶)
_con.pop_back();
// 3. 从根节点向下调整
AdjustDown(0);
}
// 获取堆顶元素
const T& top()
{
return _con[0];
}
// 获取元素个数
size_t size() const
{
return _con.size();
}
// 判断是否为空
bool empty() const
{
return _con.empty();
}
private:
Container _con; // 底层容器:默认vector
};
}
核心知识点
-
整体结构: priority_queue 底层是堆; 默认是大顶堆; 底层容器默认用 vector
-
三个模板参数
cpp
template<
class T,
class Container = vector<T>,
class Compare = Less<T>
>
T:数据类型 Container:底层存储容器(vector) Compare:比较方式(仿函数)
-
大堆 / 小堆控制: Less<T> → 大顶堆(默认); Greater<T> → 小顶堆
-
核心算法
1.) push: 尾插; 对最后一个元素 AdjustUp 向上调整
2) pop: 堆顶与最后一个元素交换; 删除最后元素; 对根节点 AdjustDown 向下调整
- 为什么用仿函数?
把比较逻辑抽离; 不用改代码,只换仿函数就能切换大堆 / 小堆; 符合 STL 设计思想
test.cpp
cpp
#include <iostream>
#include "priority_queue.h"
using namespace std;
int main()
{
// 小顶堆
gxy::priority_queue<int, vector<int>, Greater<int>> pq;
pq.push(4);
pq.push(1);
pq.push(5);
pq.push(7);
pq.push(9);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
return 0;
}
// 输出:1 4 5 7 9
大顶堆测试:
cpp
// 大顶堆(默认 Less)
gxy::priority_queue<int> pq;
3.5 自定义类型入优先级队列
自定义类型需要重载 operator< 或 operator>,以提供比较逻辑:
cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
// 日期类
class Date {
public:
// 构造函数
Date(int year = 1900, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
// 重载 < 比较运算符
bool operator<(const Date& d) const {
return _year < d._year ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
// 重载 > 比较运算符
bool operator>(const Date& d) const {
return _year > d._year ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
// 重载 << 用于打印日期
friend ostream& operator<<(ostream& _cout, const Date& d) {
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
// 测试:自定义类型放入优先级队列
void TestPriorityQueue() {
// 大顶堆(默认 less,调用 operator<,最大的在堆顶)
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
// 堆顶是最大的日期
cout << "大顶堆堆顶:" << q1.top() << endl; // 输出 2018-10-30
// 小顶堆(使用 greater,调用 operator>,最小的在堆顶)
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
// 堆顶是最小的日期
cout << "小顶堆堆顶:" << q2.top() << endl; // 输出 2018-10-28
}
int main() {
TestPriorityQueue();
return 0;
}
核心知识点
- 为什么自定义类型要重载运算符?
priority_queue 底层是堆,需要比较大小来调整结构; 自定义类型(如 Date)默认没有比较规则
所以必须提供: operator< 给 less 使用(大顶堆); operator> 给 greater 使用(小顶堆)
- 大顶堆 / 小顶堆原理
• priority_queue<Date>
默认:vector + less<Date>; 调用 Date::operator<; 结果:最大的元素在堆顶
• priority_queue<Date, vector<Date>, greater<Date>>
使用 greater<Date>; 调用 Date::operator>; 结果:最小的元素在堆顶
- 运行结果:大顶堆堆顶:2018-10-30 小顶堆堆顶:2018-10-28
3.6 经典例题:数组中第 K 个最大元素
用大顶堆找到数组中第 K 大的数:
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 用所有元素构造大顶堆
priority_queue<int> p(nums.begin(), nums.end());
// 删除前k-1个最大元素
for (int i = 0; i < k-1; ++i) {
p.pop();
}
// 堆顶即为第k大元素
return p.top();
}
};
4. 容器适配器(Container Adapter)
4.1 什么是适配器
适配器是一种设计模式,将一个类的接口转换成用户期望的另一个接口。
例如:stack 和 queue 就是对底层容器的接口进行了封装,使其表现出栈和队列的行为。
4.2 STL 中 stack 和 queue 的默认底层容器
在 STL 标准库中,stack 和 queue 并非独立容器,而是容器适配器,它们默认使用 deque 作为底层容器:
cpp
// stack 的模板定义
template <class T, class Container = deque<T>>
class stack;
// queue 的模板定义
template <class T, class Container = deque<T>>
class queue;
// priority_queue 的模板定义
template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type>>
class priority_queue;
4.3 deque(双端队列)简介
• 双开口:可以在头尾两端进行 O(1) 复杂度的插入和删除操作。
• 底层结构:并非真正连续,而是由一段段小空间拼接而成,通过中控器(map)管理,对外表现为连续空间。
• 优缺点:
优点:头尾操作效率高,扩容时不需要搬移大量数据。
缺点:不适合遍历,因为迭代器需要频繁检测段边界,导致效率低下。
4.4 为什么选择 deque 作为 stack 和 queue 的默认底层容器
-
无需遍历:stack 和 queue 不需要迭代器,完美避开了 deque 遍历效率低的缺陷。
-
效率更高:
对于 stack:deque 在扩容时比 vector 更高效,不需要搬移大量数据。
对于 queue:deque 不仅效率高,而且内存利用率也比 list 更高。
4.5 STL 标准库中 stack 和 queue 的模拟实现
4.5.1 stack 的模拟实现
stack.h
cpp
#pragma once // 防止头文件被重复包含
#include <deque> // 默认底层容器使用 deque
namespace gxy
{
// 栈的模拟实现 ------ 容器适配器
// 第二个模板参数 Container:底层存储的容器
// 默认使用 deque<T>,也可以传 vector、list
template<class T, class Container = deque<T>>
class stack
{
public:
// 入栈:尾插
void push(const T& x)
{
// 底层容器调用尾插,适配成栈的push
_con.push_back(x);
}
// 出栈:尾删
void pop()
{
// 底层容器调用尾删,适配成栈的pop
_con.pop_back();
}
// 获取栈顶元素(只读)
const T& top() const
{
// 栈顶 = 底层容器的最后一个元素
return _con.back();
}
// 获取栈中元素个数
size_t size() const
{
return _con.size();
}
// 判断栈是否为空
bool empty() const
{
return _con.empty();
}
private:
// 底层存储的容器(适配器的核心:不自己实现结构,复用容器)
Container _con;
};
}
4.5.2 queue 的模拟实现
queue.h
cpp
#pragma once // 防止头文件重复包含
#include <deque> // 默认底层容器使用 deque
namespace gxy
{
// 队列的模拟实现 ------ 容器适配器
// 底层容器默认使用 deque<T>
template<class T, class Container = deque<T>>
class queue
{
public:
// 入队:尾插
void push(const T& x)
{
_con.push_back(x);
}
// 出队:头删
void pop()
{
_con.pop_front();
}
// 获取队头元素(只读)
const T& front() const
{
return _con.front();
}
// 获取队尾元素(只读)
const T& back() const
{
return _con.back();
}
// 获取元素个数
size_t size() const
{
return _con.size();
}
// 判断队列是否为空
bool empty() const
{
return _con.empty();
}
private:
Container _con; // 底层存储容器
};
}
测试:test.cpp
cpp
#include<iostream>
#include<vector>
#include<list>
#include<stack>
#include<queue>
#include<algorithm>
using namespace std;
#include"Stack.h"
#include"Queue.h"
// 测试 stack / queue 容器适配器(支持切换底层容器)
int main()
{
// ------------------- stack 测试 -------------------
// 底层容器可以自由切换:
// gxy::stack<int, vector<int>> st;
// gxy::stack<int, list<int>> st;
// 默认底层容器是 deque<int>
gxy::stack<int, vector<int>> st;
// 类模板特点:按需实例化
// 用到哪个成员函数,才实例化哪个,不会全部生成
st.push(1);
st.push(2);
st.push(3);
st.push(4);
// 获取栈顶 + 出栈
cout << st.top() << endl; // 输出 4
st.pop();
// ------------------- queue 测试 -------------------
// gxy::queue<int, list<int>> q; // 底层可以用 list
gxy::queue<int> q; // 默认底层用 deque
q.push(1);
q.push(2);
q.push(3);
q.push(4);
// 获取队头 + 出队
cout << q.front() << endl; // 输出 1
q.pop();
return 0;
}
核心知识点
- 容器适配器可以自由切换底层容器
stack 底层支持: vector, list, deque(默认)
queue 底层支持:list,deque(默认),不支持 vector(因为 vector 没有 pop_front,效率极低)
- 类模板的重要特性:按需实例化
并不是把所有成员函数都编译出来; 你调用了哪个成员函数,才实例化哪个; 没用到的函数,不会生成代码,更不会报错
4.6 测试:vector 与 deque 排序效率对比
cpp
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
#include <ctime>
#include <cstdlib>
using namespace std;
void test_op1()
{
// 设置随机数种子
srand(time(0));
const int N = 1000000;
deque<int> dq;
vector<int> v;
// 同时给 vector 和 deque 插入 100 万随机数
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
v.push_back(e);
dq.push_back(e);
}
// 排序 vector 并计时
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
// 排序 deque 并计时
int begin2 = clock();
sort(dq.begin(), dq.end());
int end2 = clock();
// 打印耗时(单位:ms)
printf("vector sort:%d\n", end1 - begin1);
printf("deque sort:%d\n", end2 - begin2);
}
运行结果(典型值):vector sort:31 deque sort:58
核心结论:
-
vector 的排序速度明显比 deque 快
-
原因:内存结构差异
vector 是连续内存; 缓存命中率高; 迭代器就是原生指针,访问极快
deque 是分段连续内存;一段一段存储;访问时需要计算偏移、跨段;缓存命中率低,sort 会慢很多
- 所以:需要频繁 sort、随机访问 → 优先用 vector;需要频繁头尾插删 → 用 deque
cpp
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
#include <ctime>
#include <cstdlib>
using namespace std;
void test_op2()
{
srand(time(0));
const int N = 1000000;
deque<int> dq1;
deque<int> dq2;
// 两个 deque 插入一模一样的数据
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
dq1.push_back(e);
dq2.push_back(e);
}
// 方案1:直接对 deque 排序
int begin1 = clock();
sort(dq1.begin(), dq1.end());
int end1 = clock();
// 方案2:deque → 拷贝到 vector → 排序 → 拷贝回 deque
int begin2 = clock();
vector<int> v(dq2.begin(), dq2.end());
sort(v.begin(), v.end());
dq2.assign(v.begin(), v.end());
int end2 = clock();
printf("deque sort:%d\n", end1 - begin1);
printf("deque copy vector sort, copy back deque:%d\n", end2 - begin2);
}
核心思路
1)对比两种给 deque 排序 的方式:
-
直接 sort(dq.begin(), dq.end())
-
deque → 拷贝到 vector → 排序 → 拷回 deque
2)为什么要这么做?
deque 是分段连续内存,访问慢、缓存命中率低;vector 是连续内存,sort 速度快很多;即使多了两次拷贝,整体速度仍然可能更快
3)典型运行结果(直观感受):deque sort:58 deque copy vector sort, copy back deque:35
结论:对 deque 排序最优做法:拷贝到 vector 排序,再拷回 deque;哪怕多两次拷贝,整体效率也比直接 sort deque 更高。
5. 核心知识点总结
|----------------|------------|----------|-------------------------------|
| 数据结构 | 特性 | 底层容器(默认) | 核心操作 |
| stack | 后进先出(LIFO) | deque | push(), pop(), top() |
| queue | 先进先出(FIFO) | deque | push(), pop(), front(),back() |
| priority_queue | 堆(默认大顶堆) | vector | push(), pop(), top() |
关键结论:
• stack 和 queue 是容器适配器,而非独立容器,它们限制了底层容器的接口,以实现特定的行为。
• deque 被选为默认底层容器,是因为它在头尾操作上的高效性,完美适配了 stack 和 queue 的需求,同时避开了其遍历效率低的缺点。
• priority_queue 本质是堆,通过在 vector 上维护堆结构实现,默认是大顶堆,可通过比较器改为小顶堆。