一、STL 容器与适配器概述
1. 适配器(Adapter)的概念
适配器是一种封装底层容器的设计模式,目的是提供统一的访问接口,同时屏蔽底层容器的实现细节。例如 std::queue
(队列)和 std::stack
(栈)是典型的适配器,它们默认以 std::deque
作为底层容器。
2. 容器分类
- 序列容器:按顺序存储元素,如
vector
(动态数组)、list
(双向链表)、deque
(双端队列)。 - 适配器容器:基于其他容器封装而来,如
queue
(队列)、stack
(栈)、priority_queue
(优先队列)。
二、deque(双端队列)的底层实现
deque
是 “分段连续” 的容器,结合了 vector
(连续内存)和 list
(节点式管理)的优点,支持双端高效操作与随机访问。
1. 底层结构组成
- 缓冲区(Buffer):多个连续的小内存块,用于存储实际元素。
- 中控器(Map):一个指针数组,每个指针指向一个缓冲区,负责管理所有缓冲区的 “逻辑连续”。
2. 迭代器设计(__deque_iterator
)
为了模拟 “连续内存” 的随机访问,deque
的迭代器需要处理跨缓冲区的情况,其核心结构如下:
struct __deque_iterator {
typedef T** map_pointer; // 指向中控器指针数组的指针
T* cur; // 指向当前元素
T* first; // 当前缓冲区的起始位置
T* last; // 当前缓冲区的结束位置(下一个元素的位置)
map_pointer node; // 指向中控器中当前缓冲区的指针
// 解引用操作:返回当前元素
reference operator*() const { return *cur; }
// 前置++操作:处理跨缓冲区的情况
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 + difference_type(buffer_size()); // 计算新缓冲区的结束位置
}
};
3. 核心操作(push_back、push_front 等)
deque
支持双端高效插入 / 删除,核心逻辑依赖 “中控器 + 多缓冲区” 的结构:
- push_back:在队尾缓冲区添加元素;若当前缓冲区已满,则新增缓冲区,并更新中控器的指针数组。
- push_front:在队首缓冲区添加元素;若当前缓冲区已满,则新增缓冲区,并更新中控器的指针数组。
- 插入 / 删除时,只需操作当前缓冲区的首尾,无需像
vector
那样移动大量元素,也无需像list
那样分配节点,效率较高。
三、vector、list、deque 的性能对比
容器 | 随机访问 | 头尾插入 / 删除 | 中间插入 / 删除 | 扩容开销 | CPU 缓存命中率 |
---|---|---|---|---|---|
vector | O(1) | O(n) | O(n) | 有(可能浪费内存) | 高(连续内存) |
list | O(n) | O(1) | O(1) | 无(按需分配) | 低(节点分散) |
deque | O (1)(逻辑连续,迭代器计算) | O(1) | O (n)(需移动当前缓冲区元素) | 小(分段扩容) | 中(分段连续) |
1. vector
- 优点:支持随机访问(
operator[]
时间复杂度 O (1)); CPU 高速缓存命中率高(连续内存,易被缓存)。 - 缺点:头部 / 中部插入 / 删除效率低(需移动元素,时间复杂度 O (n)); 扩容时有内存浪费(如容量从 100 扩容到 200,若只存 120 个元素则浪费 80 空间)。
2. list
- 优点:任意位置插入 / 删除效率高(O (1),无需移动元素,只需修改节点指针); 无扩容开销(节点按需分配,不浪费空间)。
- 缺点:不支持随机访问(需遍历链表,时间复杂度 O (n)); CPU 高速缓存命中率低(节点分散在内存中,难以被缓存)。
3. deque
- 优点:双端插入 / 删除高效(O (1)); 支持随机访问(通过迭代器计算跨缓冲区的位置,时间复杂度 O (1)); 扩容时无大规模内存拷贝(分段扩容)。
- 缺点:随机访问的底层逻辑比
vector
复杂(需通过中控器和迭代器计算缓冲区与偏移); CPU 缓存命中率不如vector
(分段内存,缓存连续性差)。
四、适配器容器:queue、stack、priority_queue
1. std::queue(队列)
- 适配器,默认底层容器为
deque
。 - 先进先出(FIFO
push
:队尾入队;pop
:队首出队;front
:访问队首元素;back
:访问队尾元素。
2. std::stack(栈)
- 适配器,默认底层容器为
deque
。 - 后进先出(LIFO)
push
:栈顶入栈;pop
:栈顶出栈;top
:访问栈顶元素。
3. std::priority_queue(优先队列)
- 底层基于堆结构(默认用
vector
作为底层容器,再通过堆算法维护顺序)。 - 元素按优先级排序,
top
始终返回优先级最高的元素; - 插入操作需维护堆结构,时间复杂度 O (log n)。
五、内存访问与 CPU 缓存(背景知识)
CPU 访问数据时,会先查询三级缓存(L1、L2、L3):
-
若缓存命中,则直接从缓存取数据(速度快);
-
若缓存不命中,则从内存加载数据(速度慢,且会把相邻数据一并加载到缓存)。
-
vector
的连续内存易被缓存命中,效率高; -
list
的分散节点缓存命中率低; -
deque
的分段连续介于两者之间。
六、deque 的适用场景
- 需要双端频繁插入 / 删除,且偶尔需要随机访问的场景;
- 作为
queue
和stack
的默认底层容器(两者只需单端 / 双端操作,deque
能高效支持)。