前言
我们熟知 vector 擅长尾部增删、随机访问高效,但头部插入删除代价巨大;list 任意位置 O (1) 插入删除,却不支持随机访问、缓存命中率差。 那有没有容器,可以同时高效支持头部、尾部快速插入删除 ,并且保留随机下标访问能力?答案就是 std::deque。
很多人只知道 deque 是双端队列,常作为 stack、queue 的默认底层容器,却不清楚它和 vector、list 的本质区别。本文结合底层结构、迭代器实现、核心操作带你彻底搞懂 deque。
一、容器横向对比:vector /list/deque
vector(连续一维数组)
✅ 优点
-
内存连续,随机下标访问速度极快
-
CPU 高速缓存命中率高
-
尾部 push_back/pop_back 效率优秀 ❌ 缺点
-
头部、中间插入删除需要大规模挪动元素
-
容量不足时需要整体扩容、拷贝数据,存在性能损耗,会产生内存碎片 / 空间浪费
list(双向链表)
✅ 优点
-
任意位置插入、删除 O (1),仅修改指针
-
按需申请释放内存,无扩容拷贝 ❌ 缺点
-
不支持随机访问,只能遍历迭代器
-
内存碎片化,节点分散,CPU 缓存命中率极低
deque 设计目标
取长补短:支持首尾高效增删 + 支持随机访问 ,但它不是一段连续内存,采用「分段缓冲区 + 中控指针数组」的架构。
二、deque 底层存储结构
deque 由两大核心部分组成:
-
map(中控指针数组)
T** map,一块连续数组,数组内每个元素是指针,指向一块独立缓冲区 buffer。 -
多个独立 buffer 缓冲区 每一块 buffer 是连续数组,存放真实数据;不同 buffer 之间内存不连续。
术语说明:
buffer:固定大小连续数组,存储元素;GCC 默认 buffer 大小:元素大小 < 512 字节时,buffer 容纳 512 字节数据
map:中控目录,管理所有 buffer 地址
三、deque 迭代器实现原理
deque 的迭代器不能像 vector 那样简单用裸指针实现,因为 buffer 之间内存不连续,迭代器走到 buffer 末尾时,需要自动跳转到下一块 buffer。
struct __deque_iterator {
T* cur; // 当前指向元素
T* first; // 当前buffer首地址
T* last; // 当前buffer尾后地址
T** node; // 指向map中控数组对应的buffer指针
};
四个成员各司其职:
-
cur:迭代器当前访问元素地址 -
first / last:标记当前 buffer 边界 -
node:定位当前 buffer 在 map 目录中的位置
迭代器自增 operator++ 核心逻辑
self& operator++() {
++cur;
if (cur == last) {
// 当前buffer走完,切换到下一块buffer
set_node(node + 1);
cur = first;
}
return *this;
}
当cur触达当前 buffer 末尾last,迭代器通过node找到 map 中下一个 buffer,重置first/last/cur,实现跨 buffer 遍历。
同理,operator--向前迭代,如果 cur 走到 buffer 头部,切换到前一块 buffer。
随机访问 operator+=
deque 支持 it + n 随机跳跃,底层会计算偏移量:
-
算出当前 buffer 剩余元素个数
-
如果偏移超出当前 buffer,计算需要跨越多少个 buffer
-
修改 node 切换对应 buffer,定位最终元素位置
重点:deque 随机访问虽然可行,但性能弱于 vector。vector 直接地址偏移;deque 需要计算 buffer 跨块,存在分支判断开销。
四、核心接口底层流程
1. push_back 尾部插入
-
判断当前最后一块 buffer 是否还有空闲空间
-
有空闲:直接 cur 向后移动,写入元素
-
buffer 已满:在 map 中分配新 buffer,更新 map 目录,写入数据
-
2. push_front 头部插入
-
判断第一块 buffer 头部是否还有空余位置
-
有空位:cur 向前移动写入
-
buffer 用尽:向 map 前端新增一块 buffer,存放头部元素
-
✨ 这就是 deque 首尾插入都高效的根本原因:只需要操作对应 buffer,不需要挪动已有元素!
3. erase /insert 中间操作(短板)
如果在 deque 中间位置插入 / 删除元素: 需要比较待删除位置前后元素数量,选择挪动较少一侧的数据。 依然会大量移动元素,效率很低! 👉 结论:deque 只适合首尾增删,尽量避免中间插入删除。
五、operator \[\] 下标访问实现
reference operator[](size_type n) {
return *(start + difference_type(n));
}
底层逻辑:
-
起始迭代器 start 偏移 n 个位置
-
自动处理 buffer 跨块逻辑 时间复杂度:均摊 O (1) ,但是常数远大于 vector。 性能排序参考:
vector[] > deque[] >> list(不支持)
六、deque 优缺点总结
✅ 优势
-
头部 push_front /pop_front、尾部 push_back /pop_back 均摊 O (1) 高效
-
支持随机下标访问
[]、迭代器随机偏移 -
扩容不需要拷贝所有已有元素,新增 buffer 即可,不存在 vector 巨大拷贝开销
-
内存按需分段分配,不会像 vector 预留大量闲置连续空间
❌ 劣势
-
中间位置 insert/erase 性能差,需要移动元素
-
内存分段不连续,相比 vector 缓存命中率更低
-
随机访问常数开销高于 vector
七、和 stack、queue 的关系
template<class T, class Container = deque<T>> class stack;
template<class T, class Container = deque<T>> class queue;
std::stack、std::queue 是容器适配器,本身不实现存储,默认选用 deque 作为底层容器:
-
stack 需要:尾插、尾删、取尾部元素 → deque 满足
-
queue 需要:尾入队、头出队、取头尾 → deque 完美匹配
为什么默认不用 vector? vector 没有高效头删 pop_front; 为什么不用 list? list 随机访问差,内存碎片化,deque 综合性能更好。
注意:stack、queue 屏蔽了底层容器迭代器,无法遍历内部元素。
八、使用场景建议
-
✅ 使用 deque
-
需要频繁在容器头部、尾部增删数据
-
偶尔需要随机下标访问
-
不想承担 vector 头部插入巨大开销
-
-
❌ 不推荐 deque
-
大量中间位置插入删除 → 优先 list
-
极致高频随机访问、几乎不操作头部 → 优先 vector
-
九、高频面试考点整理
-
deque 内存是否连续? 答:不连续,由多段连续 buffer 组成,依靠中控 map 数组管理。
-
deque 迭代器为什么比 vector 复杂? 答:vector 内存连续,迭代器可用裸指针;deque 跨 buffer,迭代器需要记录 buffer 边界,buffer 切换逻辑。
-
deque、vector、list 如何选型?
-
为什么 stack/queue 默认底层容器是 deque?
-
deque 随机访问效率和 vector 对比?
-
deque 迭代器失效规则:新增 buffer 可能导致 map 中控数组扩容,迭代器全部失效;buffer 内部元素删除,未涉及区域迭代器有效(和 vector 有区别)。