当前位置: 首页 > news >正文

C++ STL之deque的使用和模拟实现

目录

deque

核心本质与定位

与stack和queue的关系:

deque的使用

deque的底层实现

deque的原理介绍

deque的缺陷

总结:


deque

deque文档 : deque

翻译:

双端队列

  • deque(通常发音类似“deck”)是“double-ended queue”(双端队列)的非常规缩写。双端队列是动态大小的序列容器,可在两端(前端或后端)进行扩展或收缩操作。
  • 具体的库可能会以不同方式实现 deque,一般是某种形式的动态数组。但无论如何,它们允许通过随机访问迭代器直接访问单个元素,并且容器会根据需要自动扩展或收缩来管理存储。
  • 因此,它们提供的功能与 vector 类似,但在序列的开头也能高效地插入和删除元素,而不仅仅是在末尾。但与 vector 不同的是,deque 不保证所有元素都存储在连续的内存位置:通过偏移一个元素的指针来访问 deque 中的其他元素会导致未定义行为。
  • vector 和 deque 提供的接口非常相似,可用于类似的场景,但在内部工作方式上有很大不同:
  • vector 使用需要偶尔重新分配的单个数组来实现增长,而 deque 的元素可以分散在不同的存储块中,容器在内部保存必要的信息,以常数时间直接访问任何元素,并通过迭代器提供统一的顺序接口。因此,deque 在内部比 vector 更复杂,但这使得它在某些情况下(尤其是超长序列,重新分配开销更大时)能更高效地增长。
  • 对于需要频繁在开头或末尾之外的位置插入或删除元素的操作,deque 的表现比 list 和 forward_list 更差,且迭代器和引用的一致性也更弱。

在 C++ STL 中,deque(双端队列,Double-Ended Queue) 是核心的序列式容器之一,设计目标是平衡“两端高效操作”与“随机访问”能力,填补了 vector(尾部高效、头部低效)和 list(两端高效、无随机访问)的功能空白。虽然它名字里有队列,但是并不是队列,并不要求先进先出。

核心本质与定位

  1. 基础容器而非适配器:deque 是直接管理内存和元素的底层容器,与 vector、list 同级,不属于 stack/queue 这类“封装底层容器的适配器”,能独立提供完整的元素操作接口。
  2. 功能核心:以 O(1) 时间复杂度实现头部和尾部的插入/删除操作,同时支持 O(1) 随机访问(像 vector 一样用  []  下标访问元素),是兼顾“两端灵活性”和“访问效率”的中间选择。

与stack和queue的关系:

在上篇文章中,在介绍stack和queue时就提到了deque,deque 是 stack 和 queue 的“默认底层实现”,在 C++ 中,stack、queue 与 deque 的核心关系是 “容器适配器”与“默认底层基础容器”的依赖关系,具体可拆解为两点:

1. deque 是 stack 和 queue 的“默认底层实现”
stack(栈)和 queue(队列)本身不存储元素,必须依赖一个基础容器来管理数据,而 deque 是 STL 为它们预设的默认底层容器。

  • 当你直接定义  stack<int> s;  或  queue<int> q;  时,底层实际是用 deque 来存储元素的,相当于  stack<int, deque<int>> s;  和  queue<int, deque<int>> q; 。
  • 选择 deque 作为默认的原因:deque 支持 O(1) 时间复杂度的两端操作(头部/尾部插入/删除),刚好匹配 stack(仅需栈顶,即 deque 尾部操作)和 queue(需队尾插入、队首删除)的功能需求,且比 vector(头部操作低效)、list(无随机访问)更适配。

2. 三者本质不同:适配器 vs 基础容器

  • stack/queue:是“容器适配器”,作用是“封装底层容器的接口”,强制特定的数据访问规则(stack 先进后出、queue 先进先出),自身不具备元素存储和内存管理能力。
  • deque:是“基础序列容器”,能独立管理元素存储和内存,提供完整的元素操作接口(如随机访问、两端/中间操作),是 stack/queue 实现功能的“底层支撑”。

下面我们就来详细学习deque的主要内容:

deque的使用

deque 、 vector  和  list  均作为 STL 基础序列容器,在成员函数上的共同点主要有以下几类:

  • 元素增删(基础):都支持  push_back (尾部添加元素)和  pop_back (尾部删除元素)操作;也都提供  clear  函数用于清空容器内所有元素。
  • 状态查询:均包含  empty  函数判断容器是否为空, size  函数获取容器内元素的个数。
  • 迭代器遍历:都提供  begin 、 end  用于正向遍历元素, rbegin 、 rend  用于反向遍历元素。
  • 元素访问(部分):都支持  front  获取容器头部元素的引用, back  获取容器尾部元素的引用。
  • 赋值与交换:都实现了  operator=  用于容器间的赋值操作,也都有  swap  函数用于交换两个同类型容器的元素。
#include<deque>
#include<iostream>
using namespace std;void test_deque()
{deque<int> dp;//尾插dp.push_back(1);dp.push_back(2);dp.push_back(3);dp.push_back(4);//头插dp.push_front(10);dp.push_front(20);dp.push_front(30);dp.push_front(40);//尾删dp.pop_back();//头删dp.pop_front();//随机访问for (size_t i = 0; i < dp.size(); i++){cout << dp[i] <<" ";}cout << endl;
}
int main()
{test_deque();return 0;
}

可以看到它既支持头插尾插,又支持头删尾删,还支持随机访问,它融合了vector和list的优点,从使用的角度,避开了它们各自的缺点:

  • list的缺点:不支持随机访问
  • vector的缺点:头部和中间插入删除效率低

deque的底层实现

deque 、 vector  和  list  均作为 STL 基础序列容器,它们之间究竟有什么区别呢 ? 首先我们来看一下vector和list的优缺点:

vector

核心特点:

  • 基于连续内存空间存储,支持随机访问(通过下标  []  或  at()  直接定位元素)。
  • 元素在内存中紧密排列,尾部增删效率高,中间/头部增删效率低。
  • 容量(capacity)会自动扩容(通常扩容为原容量的1.5-2倍),扩容时需拷贝旧元素到新内存,可能产生性能开销。

优点:

  • 1. 随机访问速度快(时间复杂度 O(1)),适合需要频繁通过下标读取元素的场景(如数组遍历、索引访问)。
  • 2. 内存缓存利用率高(连续内存易被CPU缓存命中),遍历元素时速度比  list  快。
  • 3. 尾部  push_back / pop_back  操作高效(O(1),无内存移动)。
  • 4. 接口简洁,支持  reserve()  预分配容量,避免频繁扩容,优化性能。

缺点:

  • 1. 中间或头部插入/删除元素效率低(O(n)):需移动插入点后所有元素,内存移动开销大。
  • 2. 扩容时可能浪费内存(扩容后的容量大于实际元素个数),且拷贝旧元素会产生额外性能消耗。
  • 3. 不适合存储大量大型对象(扩容时拷贝大型对象开销高),也不适合频繁修改中间元素的场景。

list

核心特点:

  • 基于非连续内存空间存储,元素通过前后指针连接,不支持随机访问。
  • 元素在内存中分散存储,任意位置增删效率高,访问元素需从头/尾遍历。
  • 无容量概念,元素增删时直接分配/释放单个节点内存,无需扩容和拷贝。

优点:

  • 1. 任意位置(头部、中间、尾部)插入/删除元素效率高(时间复杂度 O(1)):只需修改对应节点的指针,无需移动其他元素。
  • 2. 无扩容开销,内存分配灵活,不会浪费内存(每个元素按需分配节点)。
  • 3. 适合频繁在中间位置增删元素的场景(如链表结构、频繁插入的队列)。

缺点:

  • 1. 不支持随机访问(访问指定元素需遍历链表,时间复杂度 O(n)),无法通过下标快速定位元素。
  • 2. 内存缓存利用率低(非连续内存难被CPU缓存命中),遍历元素时速度比  vector  慢。
  • 3. 每个元素需额外存储前后指针,内存开销比  vector  大(存储相同数量元素时, list  占用更多内存)。
  • 4. 不支持  reserve()  预分配内存,也无法通过下标访问,功能比  vector  受限。

结合上面deque的使用我们可以看出deque既支持头插尾插,又支持头删尾删,还支持随机访问,它融合了vector和list的优点,从使用的角度,避开了它们各自的缺点,如果deque真的像上面说的那么优秀,那么vector和list可能就被淘汰了。

其实,deque还是有缺陷的,它的底层怎么实现的呢?

deque的原理介绍

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端 进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与 list比较,空间利用率比较高。

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个 动态的二维数组,其底层结构如下图所示:

这张图准确展示了 deque(双端队列)的底层结构,核心由三部分组成:

  1. 中控器(map):是指针数组,每个指针指向一个缓冲区(buffer)(连续内存块),用于管理所有分段的内存。
  2. 缓冲区(buffer):多个分段的连续内存块,实际存储元素。图中不同缓冲区的填充状态体现了元素的分布(如中间缓冲区有部分元素已存储)。
  3. 迭代器(iterator):
  • start(iterator) :标记 deque 的起始范围,由  cur (当前元素指针)、 first (起始缓冲区的起始指针)、 last (起始缓冲区的末尾指针)、 node (指向中控器对应节点的指针)组成。
  • finish(iterator) :标记 deque 的末尾范围,结构与  start  一致,用于界定元素的结束位置。

这种结构的核心优势是:

  • 双端操作高效:在起始缓冲区执行  push_front / pop_front ,在末尾缓冲区执行  push_back / pop_back ,均只需操作分段内存的两端,无需整体移动元素。
  • 随机访问可行:通过中控器的指针数组快速定位目标元素所在的缓冲区,再结合迭代器的  cur  指针偏移,实现 O(1) 时间的随机访问。
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问 的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

  1. 中控器(map):一个指针数组,每个指针指向一块连续的内存区域(称为“缓冲区 buffer”)。
  2. 缓冲区(buffer):分段的连续内存块,用于实际存储元素,解决了 vector 单一连续内存扩容时的拷贝开销问题。
  3. 迭代器(iterator):通过  cur (当前元素指针)、 first (缓冲区起始指针)、 last (缓冲区末尾指针)、 node (指向中控器对应节点的指针),实现跨分段的元素访问和迭代。

 那deque是如何借助其迭代器维护其假想连续的结构呢?

这张图通过可视化的方式,解释了 deque 执行  push_back (尾插)操作时的内部逻辑:
 
1. 缓冲区与中控器的配合:
deque 底层由多个缓冲区(buffer)(连续内存块)组成,这些缓冲区的地址由中控器(map,指针数组)管理。图中可以看到多个缓冲区(如存储 0-7、8-15、16-19 及后续元素的块),中控器的指针数组指向这些缓冲区的起始地址。
2. 尾插的执行流程:
当调用  push_back(0) 、 push_back(1) 、 push_back(2)  时:
- deque 会找到最末尾的缓冲区(图中存储 16-19 及后续元素的块)。
- 从该缓冲区的空闲位置开始依次插入元素 0、1、2。
- 若当前缓冲区已满,deque 会自动分配新的缓冲区,并通过中控器的指针数组维护新缓冲区的地址,保证后续尾插操作的连续性。
3. 迭代器的角色:
 finish(iterator)  标记了 deque 的末尾范围,其内部的  cur  指针会随着尾插操作不断后移,始终指向“下一个可插入的位置”,从而维护了 deque“假想连续”的逻辑结构——尽管物理内存是分段的,但通过迭代器和中控器的配合,用户可以感知到元素是连续存储的。
 
简言之,这张图直观呈现了 deque 如何通过分段缓冲区 + 中控器管理 + 迭代器标记,实现高效的尾插操作,同时维持“逻辑连续”的抽象结构。

deque的缺陷

deque 虽然在双端操作和随机访问之间做了平衡,但也存在以下缺陷:
1. 中间操作效率低
在中间位置(非头部、非尾部)插入或删除元素时,需要移动该位置前后的元素,时间复杂度为 O(n),远不如 list(中间操作 O(1))高效。
2. 随机访问略逊于 vector
尽管支持随机访问,但由于内存是分段存储的,访问元素时需要先通过中控器定位到对应的缓冲区,再在缓冲区内偏移访问,实际速度比 vector(单一连续内存直接访问)稍慢。
3. 内存结构复杂
底层需要维护“中控器(map)+ 分段缓冲区”的结构,实现逻辑比 vector 和 list 更复杂,这也导致其迭代器的实现和维护成本较高。
4. 迭代器稳定性不足
当在 deque 的头部或尾部之外的位置进行插入/删除操作,或者因中控器扩容导致缓冲区地址变化时,迭代器可能会失效,稳定性不如 list(list 仅被删除节点的迭代器失效)。
5. 不适合大量中间操作场景
如果业务场景中需要频繁对中间元素进行增删,deque 的性能表现会明显不如 list;而如果需要频繁随机访问,又不如 vector 高效,存在一定的使用场景局限性。

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其 是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实 际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看 到的一个应用就是,STL用其作为stack和queue的底层数据结构
为什么选择deque作为stackqueue的底层默认容器?
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性 结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据 结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如 list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
  • 1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
  • 2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的 元素增长时,deque不仅效率高,而且内存使用率高。 结合了deque的优点,而完美的避开了其缺陷。

总结:

摘要: 双端队列deque是STL中的动态序列容器,支持两端高效插入/删除和随机访问,填补了vector和list的功能空白。其底层通过"中控器+分段缓冲区"实现,兼具vector的随机访问和list的双端操作优势,但中间操作效率较低。deque是stack和queue的默认底层容器,因其完美适配二者仅需单端或双端操作的特点。虽然deque平衡了功能与性能,但在实际应用中仍存在遍历效率低、中间操作慢等缺陷,通常在需要频繁随机访问时选择vector,频繁中间操作时选择list更为合适。

感谢大家的观看!


文章转载自:

http://CFzIYhq5.kpzbf.cn
http://pzG8clUv.kpzbf.cn
http://DRMVwjOL.kpzbf.cn
http://1AFhQrpa.kpzbf.cn
http://SIsCWh3F.kpzbf.cn
http://vuFxiE6V.kpzbf.cn
http://CVPENx5U.kpzbf.cn
http://eBAtoUXN.kpzbf.cn
http://NEQ9ge28.kpzbf.cn
http://qkOba65s.kpzbf.cn
http://WwI9FGd9.kpzbf.cn
http://DK3W6LtQ.kpzbf.cn
http://YvMVcSJk.kpzbf.cn
http://6RxNbA8L.kpzbf.cn
http://bbe0luIu.kpzbf.cn
http://d2iUOXBX.kpzbf.cn
http://0N7liOhL.kpzbf.cn
http://mvDMaD1D.kpzbf.cn
http://xG2oXnhP.kpzbf.cn
http://rj8qjUsw.kpzbf.cn
http://tzVoXoV8.kpzbf.cn
http://df33imyd.kpzbf.cn
http://nicUi8KC.kpzbf.cn
http://Aq8oDzDG.kpzbf.cn
http://udaVNh7e.kpzbf.cn
http://MndvHZUx.kpzbf.cn
http://cSA9Oicc.kpzbf.cn
http://1fFrYvCU.kpzbf.cn
http://ITG2DBfn.kpzbf.cn
http://aWzNwA8r.kpzbf.cn
http://www.dtcms.com/a/385705.html

相关文章:

  • 【网络安全就业】信息安全专业的就业前景(非常详细)零基础入门到精通,收藏这篇就够了
  • SpringBoot中的循环依赖以及解决办法
  • np.linalg 函数一览
  • 网络:UDP协议
  • 【开题答辩全过程】以 JAVA汽车年审管理系统为例,包含答辩的问题和答案
  • 【Linux网络编程】Socket-UDP
  • OpenCV物体跟踪:从理论到实战的全面解析
  • Linux:线程同步
  • Day24_【深度学习(3)—PyTorch使用(2)—张量的数值计算】
  • 9月15日
  • 【langchain】构建简单检索问答链
  • 简单的数组
  • ENVI系列教程(四)——图像几何校正
  • 数据结构基础--散列表
  • 【Redis】-- 主从复制
  • 输入1.8V~5.5V 输出28V DCDC升压芯片TLV61046A
  • Windows 上安装 FFmpeg 8.0(2025 版)——从“手动解压”到“一条命令”的进化之路
  • 红黑树(RBTree)知识总结
  • 若依框架前端通过 nginx docker 镜像本地运行
  • 二十、瑞萨RZT2N2 PROFINET SDK正式发布
  • SpringAI框架接入Deepseek和豆包实现智能聊天
  • 江协科技STM32课程笔记(一) —GPIO
  • 江协科技STM32课程笔记(二)—外部中断EXTI
  • 科技信息差(9.15)
  • 龙珠KS6 10.5T矿机评测:性能、功耗、噪音与冷却分析
  • 打工人日报#20250915
  • 新一代车载诊断框架简介
  • 05-索引-性能分析
  • 【数据工程】 2. Unix 基础与文件操作
  • 第四课、 TypeScript 中 Cocos 的生命周期