C++ STL 完整入门笔记[8]:deque 底层原理深度剖析

前言

我们熟知 vector 擅长尾部增删、随机访问高效,但头部插入删除代价巨大;list 任意位置 O (1) 插入删除,却不支持随机访问、缓存命中率差。 那有没有容器,可以同时高效支持头部、尾部快速插入删除 ,并且保留随机下标访问能力?答案就是 std::deque

很多人只知道 deque 是双端队列,常作为 stack、queue 的默认底层容器,却不清楚它和 vector、list 的本质区别。本文结合底层结构、迭代器实现、核心操作带你彻底搞懂 deque。

一、容器横向对比:vector /list/deque

vector(连续一维数组)

✅ 优点

  1. 内存连续,随机下标访问速度极快

  2. CPU 高速缓存命中率高

  3. 尾部 push_back/pop_back 效率优秀 ❌ 缺点

  4. 头部、中间插入删除需要大规模挪动元素

  5. 容量不足时需要整体扩容、拷贝数据,存在性能损耗,会产生内存碎片 / 空间浪费

list(双向链表)

✅ 优点

  1. 任意位置插入、删除 O (1),仅修改指针

  2. 按需申请释放内存,无扩容拷贝 ❌ 缺点

  3. 不支持随机访问,只能遍历迭代器

  4. 内存碎片化,节点分散,CPU 缓存命中率极低

deque 设计目标

取长补短:支持首尾高效增删 + 支持随机访问 ,但它不是一段连续内存,采用「分段缓冲区 + 中控指针数组」的架构。

二、deque 底层存储结构

deque 由两大核心部分组成:

  1. map(中控指针数组) T** map,一块连续数组,数组内每个元素是指针,指向一块独立缓冲区 buffer。

  2. 多个独立 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指针
};

四个成员各司其职:

  1. cur:迭代器当前访问元素地址

  2. first / last:标记当前 buffer 边界

  3. 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 随机跳跃,底层会计算偏移量:

  1. 算出当前 buffer 剩余元素个数

  2. 如果偏移超出当前 buffer,计算需要跨越多少个 buffer

  3. 修改 node 切换对应 buffer,定位最终元素位置

重点:deque 随机访问虽然可行,但性能弱于 vector。vector 直接地址偏移;deque 需要计算 buffer 跨块,存在分支判断开销。

四、核心接口底层流程

1. push_back 尾部插入

  1. 判断当前最后一块 buffer 是否还有空闲空间

    1. 有空闲:直接 cur 向后移动,写入元素

    2. buffer 已满:在 map 中分配新 buffer,更新 map 目录,写入数据

2. push_front 头部插入

  1. 判断第一块 buffer 头部是否还有空余位置

    1. 有空位:cur 向前移动写入

    2. buffer 用尽:向 map 前端新增一块 buffer,存放头部元素

✨ 这就是 deque 首尾插入都高效的根本原因:只需要操作对应 buffer,不需要挪动已有元素!

3. erase /insert 中间操作(短板)

如果在 deque 中间位置插入 / 删除元素: 需要比较待删除位置前后元素数量,选择挪动较少一侧的数据。 依然会大量移动元素,效率很低! 👉 结论:deque 只适合首尾增删,尽量避免中间插入删除。

五、operator \[\] 下标访问实现

复制代码
reference operator[](size_type n) {
    return *(start + difference_type(n));
}

底层逻辑:

  1. 起始迭代器 start 偏移 n 个位置

  2. 自动处理 buffer 跨块逻辑 时间复杂度:均摊 O (1) ,但是常数远大于 vector。 性能排序参考:vector[] > deque[] >> list(不支持)

六、deque 优缺点总结

✅ 优势

  1. 头部 push_front /pop_front、尾部 push_back /pop_back 均摊 O (1) 高效

  2. 支持随机下标访问 []、迭代器随机偏移

  3. 扩容不需要拷贝所有已有元素,新增 buffer 即可,不存在 vector 巨大拷贝开销

  4. 内存按需分段分配,不会像 vector 预留大量闲置连续空间

❌ 劣势

  1. 中间位置 insert/erase 性能差,需要移动元素

  2. 内存分段不连续,相比 vector 缓存命中率更低

  3. 随机访问常数开销高于 vector

七、和 stack、queue 的关系

复制代码
template<class T, class Container = deque<T>> class stack;
template<class T, class Container = deque<T>> class queue;

std::stackstd::queue容器适配器,本身不实现存储,默认选用 deque 作为底层容器:

  • stack 需要:尾插、尾删、取尾部元素 → deque 满足

  • queue 需要:尾入队、头出队、取头尾 → deque 完美匹配

为什么默认不用 vector? vector 没有高效头删 pop_front; 为什么不用 list? list 随机访问差,内存碎片化,deque 综合性能更好。

注意:stack、queue 屏蔽了底层容器迭代器,无法遍历内部元素。

八、使用场景建议

  1. ✅ 使用 deque

    1. 需要频繁在容器头部、尾部增删数据

    2. 偶尔需要随机下标访问

    3. 不想承担 vector 头部插入巨大开销

  2. ❌ 不推荐 deque

    1. 大量中间位置插入删除 → 优先 list

    2. 极致高频随机访问、几乎不操作头部 → 优先 vector

九、高频面试考点整理

  1. deque 内存是否连续? 答:不连续,由多段连续 buffer 组成,依靠中控 map 数组管理。

  2. deque 迭代器为什么比 vector 复杂? 答:vector 内存连续,迭代器可用裸指针;deque 跨 buffer,迭代器需要记录 buffer 边界,buffer 切换逻辑。

  3. deque、vector、list 如何选型?

  4. 为什么 stack/queue 默认底层容器是 deque?

  5. deque 随机访问效率和 vector 对比?

  6. deque 迭代器失效规则:新增 buffer 可能导致 map 中控数组扩容,迭代器全部失效;buffer 内部元素删除,未涉及区域迭代器有效(和 vector 有区别)。

相关推荐
神罚天下ET1 小时前
.NET 新增功能系列文章——C# 中的新增功能
开发语言·c#·.net
noipp2 小时前
推荐题目:洛谷 P3726 [AHOI2017/HNOI2017] 抛硬币
c语言·数据结构·c++·算法·编程·洛谷·luogu
c238562 小时前
C/C++每日一练14
c语言·c++·算法
zmzb01032 小时前
C++课后习题训练记录Day176
开发语言·c++
王维同学2 小时前
跨位数进程的映像、命令行与环境块读取
c++·windows·安全
Rabitebla2 小时前
C++ STL 之 set 详解:从底层红黑树到实际应用
开发语言·数据结构·c++·算法·leetcode
520拼好饭被践踏2 小时前
JAVA+Agent学习day26
java·开发语言·数据结构·学习·agent
凉、介2 小时前
ARMv8-A 指令学习
linux·笔记·学习·嵌入式·arm·指令
157092511342 小时前
优先队列:从Java源码到实战
开发语言·python