引言
C++ 标准模板库(STL)中,stack、queue 和 priority_queue 是三个非常实用的容器。初学者往往只知道怎么用——push、pop、top,却很少思考它们的底层是怎么工作的。
实际上,它们并不真正"存储"数据,而是站在其他容器的肩膀上,通过限制接口和封装来实现特定行为。这种设计模式叫做容器适配器(Container Adapter)。
比如 stack 默认依赖 deque,但也可以换成 vector 或 list;queue 默认也是 deque,却不能直接使用 vector(因为 pop_front 效率太低);而 priority_queue 则不同,它默认基于 vector,并在其上构建了堆结构来保证每次取出的都是最大(或最小)元素。
理解这三者的底层实现,是吃透 C++ 容器适配器的关键。本文将从 deque 的存储结构讲起,再到堆的向上/向下调整算法,最后带你一步步手撕 stack、queue 和 priority_queue 的完整代码。
deque:双端队列
什么是 deque
deque(double-ended queue,双端队列)是一种可以在头尾两端进行常数时间插入和删除操作的序列容器。它在 C++ STL 中的声明在 <deque> 头文件中。
deque 结合了 vector 的随机访问能力和 list 的头尾操作高效优点,是 stack 和 queue 默认底层容器的理想选择。
deque 并不是一块连续的大内存,而是由一段段固定大小的连续缓冲区(buffer)组成,并通过一个中控器(map)来管理这些缓冲区。
![![[Pasted image 20260722203558.png]]](/https://i-blog.csdnimg.cn/direct/0a1e2956ffc94095866882e397d269c4.png)
- 中控器(map):一个指针数组,每个元素指向一块缓冲区
- 缓冲区(buffer):一段连续的固定长度内存,默认大小通常为 512 字节 / 元素大小
- 迭代器:deque 的迭代器内部维护了四个指针——
cur(当前元素)、first(缓冲区首)、last(缓冲区尾)、node(指向中控器中的位置),实现跨缓冲区遍历
deque 的迭代器深入解析
迭代器的内部结构
deque 的迭代器不是普通的指针,而是一个自定义类,内部维护了四个指针:
struct __deque_iterator {
T* cur; // 当前指向的元素
T* first; // 当前缓冲区的起始位置
T* last; // 当前缓冲区的结束位置(末尾哨兵)
map_pointer node; // 指向中控器中当前缓冲区的指针
};
cur:迭代器当前指向的具体元素first / last:当前所在缓冲区的[first, last)范围,用来判断是否走到缓冲区边界node:指向中控器(map)中对应的元素,即&map[pos],用于跨缓冲区跳转

迭代器如何在缓冲区内部移动
前移(++)
self& operator++() {
++cur; // 先移动到下一个元素
if (cur == last) { // 是否走到了当前缓冲区的末尾?
set_node(node + 1); // 切换到下一个缓冲区
cur = first; // cur 指向新缓冲区的开头
}
return *this;
}
后移(–it):
self& operator--() {
if (cur == first) { // 是否走到了当前缓冲区的开头?
set_node(node - 1); // 切换到上一个缓冲区
cur = last; // cur 指向新缓冲区的末尾
}
--cur; // 再往前走一个元素
return *this;
}
跨缓冲区跳转(set_node):
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node; // 新缓冲区的起始
last = first + buffer_size(); // 新缓冲区的末尾
}
![![[Pasted image 20260722204135.png]]](/https://i-blog.csdnimg.cn/direct/06d06613c686486687830632c7b75587.png)
随机访问(+n / -n)
deque 迭代器支持 it + n 的随机访问,计算比 vector 复杂得多:
self& operator+=(difference_type n) {
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < difference_type(buffer_size()))
cur += n; // 没跨缓冲区,直接偏移
else {
// 跨了缓冲区,需要计算跳到哪个缓冲区
difference_type node_offset =
offset > 0 ? offset / buffer_size()
: -((-offset - 1) / buffer_size()) - 1;
set_node(node + node_offset);
// 计算在新缓冲区中的偏移
cur = first + (offset - node_offset * buffer_size());
}
return *this;
}
这也是为什么 deque 的 operator[] 是 O(1) 但常数比 vector 大——需要做一次除法和取模计算目标位置所在的缓冲区和偏移。
stack 的模拟实现
什么是 stack
栈(stack)是一种先进后出(FILO)的数据结构。元素只能从栈顶一端插入和删除,就像一摞盘子——你永远只能拿走最上面的那个。
STL 的 stack 本身并不存储数据,而是适配到底层容器上,通过限制接口来实现栈的行为。
容器适配器的概念
template <class T, class Con = deque<T>>
class stack { ... };
第二个模板参数 Con 就是底层容器,默认是 deque<T>。这意味着 stack 并不关心数据怎么存,它只是把底层容器的某些接口"包装"成自己的语义:
push→ 调用底层容器的push_backpop→ 调用底层容器的pop_backtop→ 调用底层容器的back
你也可以传入其他容器,只要它支持 push_back、pop_back、back 等操作:
stack<int, vector<int>> st; // 用vector做底层
stack<int, list<int>> st; // 用list做底层
完整代码实现
#pragma once
#include <deque>
namespace zy
{
template <class T, class Con = deque<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:
Con _c;
};
}
关键点说明
push:直接尾插——栈顶就是底层容器的尾部pop:尾删——移除栈顶元素top:返回back()——即底层容器的最后一个元素- 为什么用 deque 而不是 vector:
虽然 vector 也能做同样的操作,但 deque 在扩容时不需要搬移旧数据,性能更优;并且如果用户想用 list 替换,deque 的接口天然兼容
queue 的模拟实现
什么是 queue
队列(queue)是一种先进先出(FIFO)的数据结构。元素从队尾入队,从队头出队,就像排队买票——先到的人先服务。
与 stack 的不同
queue 需要在两端操作:
push:队尾插入 →push_backpop:队头删除 →pop_front
这就排除了vector作为默认容器的可能,因为vector没有pop_front,即使手动erase头部也是O(n)的。
完整代码实现
#pragma once
#include <deque>
namespace zy
{
template <class T, class Con = deque<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:
Con _c;
};
}
关键点说明
back和front分别对应底层容器的back()和front(),因为queue需要同时访问队头和队尾- 默认不能用
vector:vector的erase( begin() )是O(n),不符合queue的O(1)要求 - 可以换成
list:list同时支持push_back、pop_front、front、back,完全胜任
stack 与 queue 对比小结
| stack | queue | |
|---|---|---|
| 数据流向 | 一端进出(后进先出) | 两端操作(先进先出) |
| push | push_back | push_back |
| pop | pop_back | pop_front |
| 访问 | top() = back() | front() + back() |
| 默认容器 | deque | deque |
| 可选容器 | vector / list / deque | list / deque(不能用 vector) |
堆的向上调整与向下调整
priority_queue(优先级队列)的底层核心就是堆。在正式看代码之前,必须先理解堆的这两个关键操作。
什么是堆
堆是一棵完全二叉树,用数组存储时满足:
- 大堆(大根堆):任意节点的值 ≥ 其子节点的值,根节点是最大值
- 小堆(小根堆):任意节点的值 ≤ 其子节点的值,根节点是最小值
父子节点的下标关系(数组从 0 开始):
parent = (kid - 1) / 2leftKid = parent * 2 + 1rightKid = parent * 2 + 2``
向上调整(adjust_up)
场景:向堆中插入一个新元素(插入到数组末尾),需要将其调整到正确位置以维持堆结构。
过程:
- 从新插入的节点(最后一个元素)开始
- 与父节点比较:如果当前节点比父节点"更优先"(大堆中更大/小堆中更小),则交换
- 向上走到父节点位置,重复步骤 2
- 直到到达根节点或不再需要交换
代码实现:
void adjust_up(int kid)
{
int parent = (kid - 1) / 2;
while (kid > 0)
{
if (comp(_c[parent], _c[kid])) // 如果父节点"小于"子节点,则交换
{
std::swap(_c[kid], _c[parent]);
kid = parent;
parent = (kid - 1) / 2;
}
else
{
break;
}
}
}
向下调整(adjust_down)
场景:
- 删除堆顶元素(最大值/最小值):将最后一个元素移到堆顶,然后向下调整
- 从一个无序数组构建堆:从最后一个非叶子节点开始向下调整
过程(以大堆为例):
- 从指定节点(通常是根节点)开始
- 找出当前节点及其左右子节点中最大的那个
- 如果当前节点不是最大的,则与最大子节点交换
- 向下走到交换后的子节点位置,重复上述步骤
- 直到到达叶子节点或当前节点就是最大的
代码实现:
void adjust_down()
{
int parent = 0;
int kid = parent * 2 + 1; // 默认先选左孩子
while (kid < _c.size())
{
// 如果右孩子存在且右孩子比左孩子"更优先",则选右孩子
if (kid + 1 < size() && comp(_c[kid], _c[kid + 1]))
{
kid++;
}
// 如果父节点"小于"子节点,则交换
if (comp(_c[parent], _c[kid]))
{
std::swap(_c[kid], _c[parent]);
parent = kid;
kid = parent * 2 + 1;
}
else
{
break;
}
}
}
两个操作的时间复杂度都是 *O(log n)*,这也是堆排序和优先级队列高效的关键。
这里补充一下无序建堆
从最后一个非叶子节点开始向前做向下调整,时间复杂度 O(n)。
无序数组快速建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--){
adjust_down(i);}`
向下调整:从底部的非叶子节点开始,往前走到根 → 先把底层的小堆建好,上层再往下调整时,下面的子树已经合法了
adjust_down(i) 的前提是:i 的左右子树已经是堆。
仿函数(Functor)
什么是仿函数
仿函数是一个重载了operator()的类对象,它可以像函数一样被调用。在 priority_queue 中,仿函数用于决定元素的优先级比较规则。
为什么需要仿函数
如果直接用 > 或 < 硬编码比较逻辑,那么最大堆和最小堆需要写两份重复的代码。使用仿函数作为模板参数,可以在不修改算法代码的前提下,通过传入不同的仿函数来改变比较行为。
实现 Greater 和 Less
// Less:判断 a < b,用于构建大堆(父节点"小于"子节点时交换)
template <class T>
class Less
{
public:
bool operator()(const T& a, const T& b)
{
return a < b;
}
};
// Greater:判断 a > b,用于构建小堆
template <class T>
class Greater
{
public:
bool operator()(const T& a, const T& b)
{
return a > b;
}
};
使用示例
Less<int> ls;
cout << ls(3, 5) << endl; // 输出 1(true),因为 3 < 5
Greater<int> gt;
cout << gt(3, 5) << endl; // 输出 0(false),因为 3 > 5 为假
在 priority_queue 中的使用
template <class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
// ...
Compare comp; // 仿函数成员
// ...
};
比较时用 comp(a, b) 代替 a < b:
comp = Less<T> → comp(a, b) 等价于 a < b → 大堆(小的往下沉,大的浮上来)comp = Greater<T> → comp(a, b) 等价于 a > b → 小堆(大的往下沉,小的浮上来)
之所以仿函数能内联,是因为 comp(a, b) 在编译期就确定了具体类型(Less<T> 或 Greater<T>),编译器可以直接内联 operator() 的调用;而函数指针需要运行时间接寻址。
priority_queue 的模拟实现
什么是 priority_queue
优先级队列(priority_queue)是一种允许以任意顺序插入元素,但总是取出优先级最高元素的容器适配器。它的底层数据结构就是堆——默认是大堆,堆顶始终是最大值。
当调用 pop() 时,堆顶元素被移除并重新调整堆;当调用 top() 时,返回堆顶元素。
完整代码实现
#pragma once
#include <vector>
#include <functional>
namespace zy
{
// 仿函数:用于比较两个元素
template <class T>
class Less
{
public:
bool operator()(const T& a, const T& b)
{
return a < b;
}
};
template <class T>
class Greater
{
public:
bool operator()(const T& a, const T& b)
{
return a > b;
}
};
template <class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
priority_queue()
{}
// 迭代器区间构造函数:快速建堆(O(n) 向下调整)
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _c(first, last)
{
for (int i = (_c.size() - 1 - 1) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
void push(const T& x)
{
_c.push_back(x); // 尾插
adjust_up(_c.size() - 1); // 向上调整
}
void pop()
{
std::swap(_c[0], _c[_c.size() - 1]); // 堆顶与最后一个元素交换
_c.pop_back(); // 删除堆顶(原最后一个元素)
adjust_down(0); // 从根开始向下调整
}
T& top()
{
return _c[0];
}
const T& top() const
{
return _c[0];
}
size_t size() const
{
return _c.size();
}
bool empty() const
{
return _c.empty();
}
private:
void adjust_up(int kid)
{
int parent = (kid - 1) / 2;
while (kid > 0)
{
if (comp(_c[parent], _c[kid]))
{
std::swap(_c[kid], _c[parent]);
kid = parent;
parent = (kid - 1) / 2;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
int kid = parent * 2 + 1;
while (kid < _c.size())
{
if (kid + 1 < _c.size() && comp(_c[kid], _c[kid + 1]))
{
++kid;
}
if (comp(_c[parent], _c[kid]))
{
std::swap(_c[kid], _c[parent]);
parent = kid;
kid = parent * 2 + 1;
}
else
{
break;
}
}
}
private:
Container _c;
Compare comp;
};
}
关键点说明
-
默认模板参数
Compare = Less<T>
用Less作为大堆(最大堆):当_c[parent] < _c[kid]时交换,大的元素向上浮。
//Less:a < b返回true→ 父小于子就交换 → 大堆
priority_queue<int> pq;// 大堆
priority_queue<int, vector<int>, Greater<int>> pq;// 小堆 -
pop()的三步操作
第一步:堆顶与末尾交换
第二步:删除末尾
第三步:向下调整根节点 -
迭代器区间构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _c(first, last) // 直接用数据初始化底层容器
{
// 从最后一个非叶子节点开始向下调整
for (int i = (_c.size() - 1 - 1) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
先用传入区间构造 vector,再统一向下调整建堆。时间复杂度 O(n),比一个个 push 的 O(n log n) 更优。
总结
从 deque 的底层结构到堆的调整算法,再到三个容器适配器的完整实现,贯穿其中的设计思想是复用与适配:
- stack 和 queue 复用了 deque 的线性操作,通过限制接口得到特定的数据结构
- priority_queue 复用了 vector 的随机访问能力,在上面构建了堆
- 仿函数将比较策略抽象出来,让同一份算法代码同时支持大堆和小堆
理解容器的底层,才能更好地使用容器。 希望这篇文章能帮你彻底搞懂这三个适配器的来龙去脉。谢谢大家观看

5178

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



