

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- 前言
- 一、deque底层原理深度解析
-
- [1.1 deque的核心设计思想](#1.1 deque的核心设计思想)
- [1.2 deque的三层结构](#1.2 deque的三层结构)
-
- [1.2.1 中控器(Map)](#1.2.1 中控器(Map))
- [1.2.2 缓冲区(Buffer)](#1.2.2 缓冲区(Buffer))
- [1.2.3 迭代器(Iterator)](#1.2.3 迭代器(Iterator))
- [1.3 关键操作源码分析](#1.3 关键操作源码分析)
-
- [1.3.1 迭代器跳跃逻辑](#1.3.1 迭代器跳跃逻辑)
- [1.3.2 push_back操作](#1.3.2 push_back操作)
- [1.3.3 内存管理策略](#1.3.3 内存管理策略)
- 二、基于deque的容器适配器实现
-
- [2.1 队列(queue)的实现](#2.1 队列(queue)的实现)
- [2.2 优先队列(priority_queue)的实现](#2.2 优先队列(priority_queue)的实现)
- [2.3 容器适配器的设计模式](#2.3 容器适配器的设计模式)
- 三、实战示例:自定义容器适配器
- 四、性能分析与选择建议
-
- [4.1 各容器时间复杂度对比](#4.1 各容器时间复杂度对比)
- [4.2 使用建议](#4.2 使用建议)
- 五、总结
前言
在C++标准模板库(STL)中,deque(双端队列)以其独特的分段连续空间设计,实现了头尾两端的高效操作,是容器家族中设计最为精妙的成员之一。与vector的单一连续内存块不同,deque通过精巧的中控器管理多个缓冲区,既保持了随机访问的高效性,又避免了大规模数据搬移的开销。理解deque的底层实现,不仅能帮助我们更好地使用这一强大容器,还能深入领悟STL设计的精髓,为构建自己的高效数据结构奠定坚实基础。
一、deque底层原理深度解析
1.1 deque的核心设计思想
deque(双端队列)是STL中最复杂的序列容器之一,它的设计精巧而复杂。与vector的单向连续空间不同,deque采用了分段连续空间的设计理念:
cpp
// deque的典型声明
deque<int> dq; // 底层由多个缓冲区组成,通过中控器map管理
关键特性对比:
- vector:单向开口,尾部操作高效,但头部插入/删除需要移动所有元素
- deque:双向开口,头尾操作都是O(1)时间复杂度
- list:双向链表,任意位置插入删除O(1),但随机访问O(n)
1.2 deque的三层结构
deque的实现包含三个关键组件:
1.2.1 中控器(Map)
cpp
// 简化版deque结构
template<class T>
class deque {
private:
T** map; // 指向指针数组的指针
size_t map_size; // map的大小
iterator start; // 指向第一个缓冲区
iterator finish; // 指向最后一个缓冲区
};
map是一个二级指针,它的每个元素指向一个缓冲区(buffer):
map → [ptr1, ptr2, ptr3, ...] // 中控器数组
↓ ↓ ↓
buffer1 buffer2 buffer3 // 实际存储数据的缓冲区
1.2.2 缓冲区(Buffer)
缓冲区是实际存储元素的地方,大小可以通过模板参数指定:
cpp
template<class T, class Alloc = alloc, size_t BufSize = 0>
class deque {
// 如果BufSize为0,则根据元素大小自动计算
// sizeof(T) < 512 ? 512/sizeof(T) : 1
};
1.2.3 迭代器(Iterator)
deque迭代器是它最复杂的部分,需要维护四个指针:
cpp
template<class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
T* cur; // 当前元素指针
T* first; // 当前缓冲区起始
T* last; // 当前缓冲区末尾
T** node; // 指向中控器中的对应指针
};
1.3 关键操作源码分析
1.3.1 迭代器跳跃逻辑
cpp
// 前置++运算符重载
self& operator++() {
++cur; // 先移动到下一个元素
if (cur == last) { // 如果到达缓冲区末尾
set_node(node + 1); // 切换到下一个缓冲区
cur = first; // 从新缓冲区的起始开始
}
return *this;
}
// 切换缓冲区的实现
void set_node(map_pointer new_node) {
node = new_node; // 更新中控器指针
first = *new_node; // 新缓冲区起始
last = first + buffer_size(); // 新缓冲区末尾
}
1.3.2 push_back操作
cpp
void push_back(const value_type& value) {
if (finish.cur != finish.last - 1) {
// 当前缓冲区还有空间
construct(finish.cur, value);
++finish.cur;
} else {
// 需要新分配缓冲区
push_back_aux(value);
}
}
void push_back_aux(const value_type& value) {
value_type t_copy = value;
reserve_map_at_back(); // 确保map有空间
*(finish.node + 1) = allocate_node(); // 分配新缓冲区
construct(finish.cur, t_copy); // 构造元素
finish.set_node(finish.node + 1); // 更新迭代器
finish.cur = finish.first; // 指向新缓冲区起始
}
1.3.3 内存管理策略
cpp
// 创建map和节点
void create_map_and_nodes(size_type num_elements) {
// 计算需要的缓冲区数量
size_type num_nodes = num_elements / buffer_size() + 1;
// map大小策略:至少8个,或num_nodes+2(前后预留)
map_size = max(initial_map_size(), num_nodes + 2);
// 配置map空间
map = map_allocator::allocate(map_size);
// 让start和finish指向中间位置,便于双向扩展
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
// 为每个节点配置缓冲区
for (map_pointer cur = nstart; cur <= nfinish; ++cur)
*cur = allocate_node();
}
二、基于deque的容器适配器实现
理解了deque的底层原理后,我们可以基于它实现更高级的容器适配器。
2.1 队列(queue)的实现
queue是典型的FIFO(先进先出)数据结构,使用deque作为底层容器是最佳选择:
cpp
#pragma once
#include <deque>
namespace bit
{
template<class T, class Container = std::deque<T>>
class queue
{
public:
// 入队操作:直接调用deque的push_back
void push(const T& x)
{
_con.push_back(x); // O(1)时间复杂度
}
// 出队操作:调用deque的pop_front
void pop()
{
_con.pop_front(); // O(1)时间复杂度
}
// 获取队首元素
const T& front() const
{
return _con.front(); // 直接访问,O(1)
}
// 获取队尾元素
const T& back() const
{
return _con.back(); // 直接访问,O(1)
}
// 队列大小
size_t size() const
{
return _con.size(); // deque的size()是O(1)
}
// 判断队列是否为空
bool empty() const
{
return _con.empty(); // deque的empty()是O(1)
}
private:
Container _con; // 底层容器,默认为deque
};
}
为什么选择deque作为queue的底层容器?
- 高效的头尾操作:queue只需要在头部删除、尾部插入,deque两端操作都是O(1)
- 连续内存优势:相比list,deque的缓存局部性更好
- 自动扩容:无需手动管理内存,deque会自动处理缓冲区分配
2.2 优先队列(priority_queue)的实现
priority_queue虽然也名为"队列",但实际上是堆(heap)的实现,通常使用vector作为底层容器:
cpp
#pragma once
#include <vector>
#include <functional>
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y) const
{
return x < y; // 小堆比较器
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y) const
{
return x > y; // 大堆比较器
}
};
namespace bit
{
// 默认是大堆
template<class T, class Container = std::vector<T>,
class Compare = Less<T>>
class priority_queue
{
public:
// 向上调整(插入时使用)
void AdjustUp(int child)
{
Compare com; // 比较器对象
int parent = (child - 1) / 2; // 完全二叉树性质
while (child > 0) // 调整到根节点为止
{
// 如果父节点小于子节点(大堆)
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()
{
// 将堆顶与最后一个元素交换
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back(); // 删除原堆顶元素
AdjustDown(0); // 向下调整
}
// 获取堆顶元素
const T& top() const
{
return _con[0]; // 堆顶元素
}
size_t size() const { return _con.size(); }
bool empty() const { return _con.empty(); }
private:
Container _con; // 底层容器,通常为vector
};
}
为什么priority_queue使用vector而不是deque?
- 随机访问需求:堆操作需要频繁访问父节点和子节点,vector的O(1)随机访问更合适
- 连续内存优势:vector的连续内存布局对CPU缓存更友好
- 尾部插入高效:堆只需要在尾部插入,vector的push_back是平摊O(1)
2.3 容器适配器的设计模式
queue和priority_queue都使用了适配器模式(Adapter Pattern):
cpp
// 适配器模式的典型结构
template<class T, class Container = SomeContainer>
class Adapter {
private:
Container _con; // 组合一个已有容器
public:
// 通过封装底层容器的方法提供新接口
void some_operation() {
_con.some_underlying_operation();
}
};
设计优势:
- 代码复用:复用已有容器的成熟实现
- 灵活性:可以通过模板参数更换底层容器
- 关注点分离:适配器只关注特定接口,底层容器关注数据存储
三、实战示例:自定义容器适配器
让我们实现一个简单的双端队列适配器,演示如何基于现有容器构建新数据结构:
cpp
#pragma once
#include <vector>
#include <cassert>
namespace bit
{
// 简易双端队列(使用两个vector模拟)
template<class T>
class simple_deque
{
public:
// 头部插入
void push_front(const T& value)
{
_front.push_back(value); // 在前vector尾部插入
}
// 头部删除
void pop_front()
{
if (!_front.empty())
{
_front.pop_back(); // 从前vector尾部删除
}
else if (!_back.empty())
{
// 如果前vector为空,从后vector移动元素
move_back_to_front();
_front.pop_back();
}
else
{
assert(false); // 队列为空
}
}
// 尾部插入
void push_back(const T& value)
{
_back.push_back(value); // 在后vector尾部插入
}
// 尾部删除
void pop_back()
{
if (!_back.empty())
{
_back.pop_back(); // 从后vector尾部删除
}
else if (!_front.empty())
{
// 如果后vector为空,从前vector移动元素
move_front_to_back();
_back.pop_back();
}
else
{
assert(false); // 队列为空
}
}
// 获取头部元素
T& front()
{
if (!_front.empty())
{
return _front.back();
}
else if (!_back.empty())
{
move_back_to_front();
return _front.back();
}
else
{
assert(false);
return _back[0]; // 不会执行到这里
}
}
// 获取尾部元素
T& back()
{
if (!_back.empty())
{
return _back.back();
}
else if (!_front.empty())
{
move_front_to_back();
return _back.back();
}
else
{
assert(false);
return _front[0]; // 不会执行到这里
}
}
size_t size() const { return _front.size() + _back.size(); }
bool empty() const { return _front.empty() && _back.empty(); }
private:
// 将后vector的元素移动到前vector
void move_back_to_front()
{
while (!_back.empty())
{
_front.push_back(_back.back());
_back.pop_back();
}
}
// 将前vector的元素移动到后vector
void move_front_to_back()
{
while (!_front.empty())
{
_back.push_back(_front.back());
_front.pop_back();
}
}
private:
std::vector<T> _front; // 存储前半部分元素(反向)
std::vector<T> _back; // 存储后半部分元素(正向)
};
}
四、性能分析与选择建议
4.1 各容器时间复杂度对比
| 操作 | vector | deque | list | queue(deque) | priority_queue(vector) |
|---|---|---|---|---|---|
| 头部插入 | O(n) | O(1) | O(1) | O(1) | O(log n) |
| 尾部插入 | O(1) | O(1) | O(1) | O(1) | O(log n) |
| 头部删除 | O(n) | O(1) | O(1) | O(1) | O(log n) |
| 尾部删除 | O(1) | O(1) | O(1) | O(1) | O(log n) |
| 随机访问 | O(1) | O(1) | O(n) | 不支持 | 不支持 |
4.2 使用建议
-
选择vector的情况:
- 需要频繁随机访问
- 主要在尾部进行插入删除
- 内存连续性很重要
-
选择deque的情况:
- 需要在头部和尾部都进行高效操作
- 需要队列或双端队列功能
- 不想预先分配大量空间
-
选择queue适配器的情况:
- 明确的FIFO需求
- 不需要随机访问
- 代码清晰性和类型安全性重要
-
选择priority_queue适配器的情况:
- 需要按优先级处理元素
- 频繁获取最大/最小元素
- 处理任务调度等场景
五、总结
通过深入分析STL deque的源码实现,我们理解了:
- deque采用分段连续空间设计,通过中控器map管理多个缓冲区
- deque迭代器是STL中最复杂的,需要维护缓冲区的跳跃逻辑
- queue适配器基于deque实现,提供FIFO语义
- priority_queue适配器基于vector实现堆,提供优先级处理能力
关键设计思想:
- deque:以空间换时间,用复杂的结构换取两端的O(1)操作
- queue:适配器模式,封装底层容器的特定操作
- priority_queue:堆算法与容器的结合,提供高效的优先级管理
理解这些底层实现不仅有助于我们更好地使用STL,还能在需要时设计出自己的高效数据结构。

加油!志同道合的人会看到同一片风景。
看到这里请点个赞 ,关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!