在 C++ STL 中,stack(栈)、queue(队列)和 priority_queue(优先队列)并非传统意义上的 “容器”,而是容器适配器—— 它们通过封装底层容器(如 vector、list、deque)的接口,实现特定的数据访问规则(如 LIFO、FIFO、优先级排序)。本文将从 “使用→模拟实现→底层原理” 的路径,系统讲解这三种适配器的核心特性,并揭秘 STL 选择 deque 作为默认底层容器的深层原因。
一、stack:后进先出(LIFO)的栈适配器
1.1 认识 stack
stack 遵循 “后进先出”(Last In First Out)规则,仅允许在 “栈顶” 进行插入(push)和删除(pop)操作,无法直接访问栈中间的元素。其核心场景包括:表达式求值(如逆波兰表达式)、函数调用栈、括号匹配等。
STL 中 stack 的底层默认容器是deque,也可指定为 vector 或 list(只要底层容器支持push_back()、pop_back()、back()等接口)。
1.2 stack 核心接口实战
stack 的接口简洁,仅围绕 “栈顶操作” 和 “状态查询” 设计,具体如下:
| 函数声明 | 接口说明 | 代码示例 |
|---|---|---|
stack() | 无参构造,创建空栈 | stack<int> st; |
empty() | 判断栈是否为空(空返回 true) | if (st.empty()) { cout << "栈空"; } |
size() | 返回栈中元素个数 | cout << "栈大小:" << st.size(); |
top() | 返回栈顶元素的引用(不删除) | cout << "栈顶元素:" << st.top(); |
push(const T& val) | 栈顶插入元素 val | st.push(10);(10 成为新栈顶) |
pop() | 删除栈顶元素(无返回值) | st.pop();(栈顶元素出栈) |
注意:
pop()仅删除栈顶元素,若需获取栈顶元素,需先调用top()。
1.3 stack 经典 OJ 案例
案例 1:最小栈(LeetCode 155)
需求:设计一个栈,支持push、pop、top操作,并能在 O (1) 时间内检索到最小元素。思路:用两个栈实现 ——
_elem栈:存储所有元素,实现基本栈操作;
_min栈:存储当前栈中的最小值(仅当新元素≤当前最小值时入栈,出栈时若与_elem栈顶元素相等则同步出栈)。
代码实现:
class MinStack {
public:
void push(int x) {
_elem.push(x); // 所有元素入_elem栈
// 新元素≤当前最小值时,入_min栈
if (_min.empty() || x <= _min.top()) {
_min.push(x);
}
}
void pop() {
// 若出栈元素是最小值,_min栈同步出栈
if (_min.top() == _elem.top()) {
_min.pop();
}
_elem.pop();
}
int top() {
return _elem.top(); // 直接返回_elem栈顶
}
int getMin() {
return _min.top(); // 返回_min栈顶(当前最小值)
}
private:
stack<int> _elem; // 存储所有元素
stack<int> _min; // 存储当前最小值
};
案例 2:逆波兰表达式求值(LeetCode 150)
需求:根据逆波兰表达式(后缀表达式)计算结果(如["2","1","+","3","*"]结果为(2+1)*3=9)。思路:遇到数字入栈,遇到运算符则弹出栈顶两个元素计算,结果重新入栈。
代码实现:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (const string& token : tokens) {
// 若为运算符,弹出两个元素计算
if (token == "+" || token == "-" || token == "*" || token == "/") {
int right = st.top(); st.pop(); // 右操作数(后入栈)
int left = st.top(); st.pop(); // 左操作数(先入栈)
switch (token[0]) {
case '+': st.push(left + right); break;
case '-': st.push(left - right); break;
case '*': st.push(left * right); break;
case '/': st.push(left / right); break; // 题目保证无除数为0
}
} else {
// 若为数字,转换为int入栈
st.push(atoi(token.c_str()));
}
}
return st.top(); // 最终结果在栈顶
}
};
1.4 stack 的模拟实现
stack 的本质是 “封装底层容器的尾操作”,因此只需选择一个支持push_back()、pop_back()、back()的容器(如 vector、deque),即可快速实现 stack。
以 vector 为底层容器的模拟实现:
#include <vector>
namespace bit {
template <class T, class Container = std::vector<T>> // 默认vector
class stack {
public:
// 无参构造(底层容器默认构造)
stack() {}
// 栈顶插入
void push(const T& x) { _container.push_back(x); }
// 栈顶删除
void pop() { _container.pop_back(); }
// 获取栈顶元素
T& top() { return _container.back(); }
const T& top() const { return _container.back(); }
// 栈状态查询
bool empty() const { return _container.empty(); }
size_t size() const { return _container.size(); }
private:
Container _container; // 底层容器(被封装的核心)
};
}
二、queue:先进先出(FIFO)的队列适配器
2.1 认识 queue
queue 遵循 “先进先出”(First In First Out)规则,仅允许在 “队尾” 插入(push)、在 “队头” 删除(pop),无法访问队列中间的元素。其核心场景包括:任务调度、消息队列、广度优先搜索(BFS)等。
STL 中 queue 的底层默认容器是deque,也可指定为 list(需支持push_back()、pop_front()、front()、back()接口)—— 但不能用 vector(vector 的pop_front()需搬移所有元素,效率极低)。
2.2 queue 核心接口实战
queue 的接口围绕 “队尾插入”“队头删除” 和 “状态查询” 设计,具体如下:
| 函数声明 | 接口说明 | 代码示例 |
|---|---|---|
queue() | 无参构造,创建空队列 | queue<int> q; |
empty() | 判断队列是否为空(空返回 true) | if (q.empty()) { cout << "队空"; } |
size() | 返回队列中元素个数 | cout << "队列大小:" << q.size(); |
front() | 返回队头元素的引用(不删除) | cout << "队头元素:" << q.front(); |
back() | 返回队尾元素的引用(不删除) | cout << "队尾元素:" << q.back(); |
push(const T& val) | 队尾插入元素 val | q.push(20);(20 成为新队尾) |
pop() | 删除队头元素(无返回值) | q.pop();(队头元素出队) |
注意:
pop()仅删除队头元素,若需获取队头元素,需先调用front()。
2.3 queue 的模拟实现
queue 的核心是 “队尾插入 + 队头删除”,因此需选择支持push_back()和pop_front()的底层容器(如 list、deque)。以 list 为底层容器的模拟实现如下:
#include <list>
namespace bit {
template <class T, class Container = std::list<T>> // 默认list
class queue {
public:
// 无参构造
queue() {}
// 队尾插入
void push(const T& x) { _container.push_back(x); }
// 队头删除
void pop() { _container.pop_front(); }
// 获取队头/队尾元素
T& front() { return _container.front(); }
const T& front() const { return _container.front(); }
T& back() { return _container.back(); }
const T& back() const { return _container.back(); }
// 队列状态查询
bool empty() const { return _container.empty(); }
size_t size() const { return _container.size(); }
private:
Container _container; // 底层容器
};
}
三、priority_queue:优先级排序的队列适配器
3.1 认识 priority_queue
priority_queue(优先队列)是一种 “优先级驱动” 的队列,每次出队的不是队头元素,而是优先级最高的元素(默认是最大值,即 “大堆”)。其底层基于 “堆” 数据结构实现,默认使用 vector 作为底层容器(需支持随机访问迭代器以维护堆结构)。
核心场景包括:Top-K 问题(如数组中第 K 个最大元素)、任务调度(高优先级任务先执行)等。
3.2 priority_queue 核心接口实战
priority_queue 的接口与 queue 类似,但内部会自动维护堆结构,具体如下:
| 函数声明 | 接口说明 | 代码示例 |
|---|---|---|
priority_queue() | 无参构造,创建空优先队列(默认大堆) | priority_queue<int> pq; |
priority_queue(InputIterator first, InputIterator last) | 迭代器构造,用 [first, last) 区间元素初始化 | int arr[] = {3,1,2}; priority_queue<int> pq(arr, arr+3); |
empty() | 判断优先队列是否为空 | if (pq.empty()) { cout << "队空"; } |
size() | 返回元素个数 | cout << "队列大小:" << pq.size(); |
top() | 返回优先级最高的元素(堆顶,不删除) | cout << "最高优先级元素:" << pq.top(); |
push(const T& val) | 插入元素,自动调整堆结构 | pq.push(4);(插入后仍为大堆) |
pop() | 删除堆顶元素,自动调整堆结构 | pq.pop();(删除后仍为大堆) |
关键配置:大堆与小堆
默认大堆:底层用less<T>比较(需包含<functional>头文件),如priority_queue<int> pq;;
小堆:需显式指定三个模板参数:priority_queue<T, Container, greater<T>>,如priority_queue<int, vector<int>, greater<int>> pq;。
自定义类型的优先级配置
若优先队列存储自定义类型(如Date),需重载operator<(大堆)或operator>(小堆),以定义优先级规则:
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);
}
// 友元函数:支持cout输出
friend ostream& operator<<(ostream& os, const Date& d) {
os << d._year << "-" << d._month << "-" << d._day;
return os;
}
private:
int _year, _month, _day;
};
// 测试:大堆与小堆
void TestPriorityQueue() {
// 大堆(默认,按日期降序,最新日期优先级最高)
priority_queue<Date> pq1;
pq1.push(Date(2024, 5, 20));
pq1.push(Date(2024, 5, 18));
cout << "大堆顶(最新日期):" << pq1.top() << endl; // 2024-5-20
// 小堆(按日期升序,最旧日期优先级最高)
priority_queue<Date, vector<Date>, greater<Date>> pq2;
pq2.push(Date(2024, 5, 20));
pq2.push(Date(2024, 5, 18));
cout << "小堆顶(最旧日期):" << pq2.top() << endl; // 2024-5-18
}
3.3 priority_queue 经典 OJ 案例:数组中第 K 个大的元素(LeetCode 215)
需求:找到数组中第 K 个最大的元素(如数组[3,2,1,5,6,4],K=2 时结果为 5)。思路:用大堆存储所有元素,弹出前 K-1 个最大值,剩余堆顶即为第 K 个最大值。
代码实现:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 用数组初始化大堆
priority_queue<int> pq(nums.begin(), nums.end());
// 弹出前K-1个最大值
for (int i = 0; i < k - 1; ++i) {
pq.pop();
}
return pq.top(); // 剩余堆顶为第K个最大值
}
};
四、容器适配器的底层原理:为什么默认选 deque?
4.1 什么是容器适配器?
适配器(Adapter)是一种设计模式,核心是 “接口转换”—— 将一个类的接口转换成客户期望的另一个接口。STL 中的 stack、queue、priority_queue 并非直接管理内存,而是通过封装底层容器的接口,实现特定的数据访问规则(如 LIFO、FIFO),因此被称为 “容器适配器”。
4.2 deque 的特性:stack 与 queue 的理想底层容器
deque(双端队列)是一种 “双开口” 的分段连续容器,支持:
头尾两端 O (1) 时间插入 / 删除;
随机访问(效率低于 vector,但高于 list);
扩容时无需搬移大量元素(仅需拼接新的分段空间)。
其底层结构类似 “动态二维数组”:
中控器(map):存储各分段空间(缓冲区)的指针;
缓冲区:存储实际元素的连续小空间(默认大小为 512 字节)。
deque 的缺陷是遍历效率低(迭代器需频繁检测是否跨越缓冲区边界),但 stack 和 queue 恰好不需要遍历 —— 这使其成为两者的理想底层容器。
4.3 STL 选择 deque 作为默认容器的原因
-
stack 的需求匹配:stack 仅需尾插(
push_back())和尾删(pop_back()),deque 的尾操作效率与 vector 相当,且扩容时无需搬移大量元素(vector 扩容需拷贝旧元素),效率更高。 -
queue 的需求匹配:queue 需尾插(
push_back())和头删(pop_front()):- list 的头删效率虽高,但节点分散存储,空间利用率低;
- deque 的头删无需搬移元素(直接操作首缓冲区),且空间利用率高于 list。
-
避开 deque 的缺陷:deque 的遍历效率低,但 stack 和 queue 均不支持迭代器(无需遍历),完美避开了这一缺陷。
五、总结
stack、queue、priority_queue 作为 STL 的容器适配器,其核心价值在于 “封装底层容器,实现特定访问规则”:
stack:LIFO 规则,适合栈顶操作场景,默认底层容器为 deque;
queue:FIFO 规则,适合头尾操作场景,默认底层容器为 deque;
priority_queue:优先级规则,基于堆实现,默认底层容器为 vector(需随机访问维护堆结构)。
学习这三种适配器的关键在于:
- 能用:掌握核心接口,结合场景选择合适的适配器(如 Top-K 问题用 priority_queue);
- 明理:理解容器适配器的设计思想,以及底层容器的选择逻辑(如 deque 为何成为 stack/queue 的默认容器);
- 会扩展:能根据需求指定自定义底层容器(如 stack 用 vector、queue 用 list),甚至模拟实现适配器。
3681

被折叠的 条评论
为什么被折叠?



