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

【C++】容器进阶:deque的“双端优势” vs list的“链式灵活” vs vector的“连续高效”

  

                                                       🔥拾Ծ光:个人主页

👏👏👏欢迎来到我的专栏:《C++》,《C++类和对象》,《数据结构》,《C语言》

想必大家都很好奇:为什么STL中实现了vector容器和list容器后,还要实现一个deque容器呢?接下来我们就来看看这个容器到底有什么神奇的地方,在此之前,我们先讨论一下vector和list的不同之处:

目录

一、vector容器和list容器对比

1、内存结构对比⭐️

2、对于插入数据时的时间复杂度对比分析:

3、对于删除数据时的时间复杂度对比分析:

4、迭代器特性对比⭐️

5、总结:

二、双端队列——deque(了解)

1、什么是deque?

2、deque的特殊结构

3、常用接口说明

4、deque的缺陷

三、总结


一、vector容器和list容器对比

对于这两个容器的区别也是面试中的高频考点(⭐️⭐️⭐️)

两者的本质区别源于底层实现(核心数据结构与内存布局),这直接决定了它们的性能特性:

1、内存结构对比⭐️

容器底层数据结构⭐️内存布局⭐️核心特点⭐️
vector动态数组连续内存空间元素存储在连续的内存块中,当空间不足时会自动扩容(通常扩容为原大小的 1.5~2 倍,拷贝旧元素到新空间)

(1)支持随机访问

(2)内存局部性好(缓存利用率高);

(3)扩容可能产生内存浪费和拷贝开销

list双向链表非连续内存节点每个元素存储在独立的节点中,节点包含「数据域」和两个「指针域」(分别指向前驱和后继节点),节点在内存中分散存储

(1)不支持随机访问;

(2)内存局部性差;

(3)插入 / 删除操作无需移动元素,仅需修改指针

2、对于插入数据时的时间复杂度对比分析:

插入位置vector 时间复杂度list 时间复杂度核心原因
尾部(push_back)

O (1)

( amortized,均摊)

O(1)

vector 尾部插入:若内存充足,直接在末尾添加;若需扩容,需拷贝旧元素(O (n)),但均摊后为 O (1);

list 尾部插入:仅需创建新节点,修改尾节点指针,无需移动元素

头部(push_front)O(n)O(1)

vector 头部插入:需将所有元素向后移动 1 位,覆盖新元素位置;

list 头部插入:仅需创建新节点,修改头节点指针,无需移动元素

中间(指定迭代器位置)O(n)O(1)

3、对于删除数据时的时间复杂度对比分析:

删除位置vector 时间复杂度list 时间复杂度核心原因
尾部(pop_back)O(1)O(1)

vector 尾部删除:仅需将「有效元素个数」减 1,无需修改内存;

list 尾部删除:修改尾节点前驱的指针,释放尾节点内存

头部(pop_front)O(n)O(1)

vector 头部删除:需将所有元素向前移动 1 位,覆盖被删除元素;

list 头部删除:修改头节点后继的指针,释放头节点内存

中间(指定迭代器位置)O(n)O(1)

vector 中间删除:需将删除位置后的元素向前移动,填补空缺;

list 中间删除:若已拿到迭代器,仅需修改前后节点指针,释放当前节点

4、迭代器特性对比⭐️

特性vector 迭代器list 迭代器
迭代器类型随机访问迭代器(RandomAccessIterator)双向迭代器(BidirectionalIterator)
支持的操作支持++、--、 +-+=-=[] 等算术操作(如 it + 5 直接跳 5 个元素)仅支持 ++-- 操作(只能逐个移动)
迭代器失效⭐️

- 扩容时,所有迭代器、指针、引用均失效;

- 插入 / 删除中间元素时,插入 / 删除位置后的迭代器失效

- 仅「被删除节点的迭代器」失效;

- 插入元素时,所有迭代器均有效

5、总结:

优先选择 vector 的场景:

1、需要频繁随机访问元素(如通过下标读取、排序、二分查找 ——vector 支持 std::sortlist 需用自身的 sort() 成员函数);

2、插入 / 删除操作主要在尾部(如实现栈、动态数组);

3、对内存局部性要求高(如高频访问元素,依赖缓存加速);

4、存储小数据类型,希望控制内存开销。

优先选择 list 的场景:

1、需要频繁在头部或中间插入 / 删除元素(如实现双向队列、频繁调整顺序的列表);

2、不依赖随机访问,仅需顺序遍历;

3、对迭代器稳定性要求高(如遍历中频繁插入 / 删除,避免迭代器失效);

4、存储大数据类型(指针开销占比低,插入 / 删除无需移动大元素)。

二、双端队列——deque(了解)

1、什么是deque?

通过上面的对比,我们知道,vector有随机访问,缓存利用率高等特点,但插入删除操作时间复杂度为O(N);而对于list恰好相反,不支持随机访问,缓存利用率低,但插入删除操作时间复杂度仅为O(1)。不难发现,这两个容器的优劣势几乎是相对的,那么有没有一个容器能够将这两个容器的优劣势互补一下呢!

deque(Double-Ended Queue,双端队列)是 C++ STL(标准模板库)中一种双向动态数组容器,核心特点是支持在首尾两端高效插入和删除元素,同时兼顾对中间元素的随机访问能力(但效率低于 vector)。

deque的特殊结构就刚好结合了 vector(随机访问)和 list(首尾操作高效)部分优势,是一种平衡了多种需求的容器。

2、deque的特殊结构

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

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

deque底层有一个中控器(map数组)存储每个buffer小数组的地址,同时,还有4个迭代器来维护buffer数组,其中,node指向buffer数组,first指向buffer数组的起始位置,cur指向buffer数组的最后一个有效数据的下一个位置,last指向buffer数组末尾的下一个位置。

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

比如要在中控数组map中node指针指向的buffer1插入数据,就要先判断cur迭代器是否与last迭代器指向位置相同,防止越界,若未越界,则在cur迭代器指向的位置插入,然后cur++;反之,则在node迭代器后面的一个迭代器指向的buffer数组插入,若中控数组已经满了,就先扩容,然后插入。

3、常用接口说明

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩 容时,也不需要搬移大量的元素,因此其效率是必vector高的。 与list比较,deque底层是连续空间,空间利用率比较高,不需要存储额外字段

而我们知道,栈(stack)——先进后出,其最常用的操作就是尾删,尾插,获取栈顶数据等;队列(queue)——先进先出,其最常用的操作就是头删,尾插,获取队头或队尾数据。对于这些操作,deque容器由于结合了vector和lis的部分优势,而恰好能够满足,所以deque容器作为stack和queue这种容器适配器(这个我会专门为大家介绍)的底层数据结构就非常完美。

所以,下面我们介绍deque的头/尾插,头/尾删等操作,其他操作由于deque的缺陷而消耗很大,我们并不推荐使用,所以也并不作介绍,有兴趣可以自己查看文档。

        接口函数                                  功能
push_front (const value_type& val)
                                头插val
push_back (const value_type& val)
                                尾插val
pop_front()
                                头删
pop_back()
                                尾删
size()
                       获取有效数据个数
front()
                        返回第一个数据
back()
                       返回最后一个数据
empty()
                               判空
deque<int> dq; // 实例化对象dq
// 尾插
dq.push_back(1);
dq.push_back(2);
// 头插
dq.push_front(10);
// 获取队头数据
int top = dq.front();
// 获取队尾数据
int end = dq.back();
// 头删
dq.pop_front();
// 尾删
dq.pop_back();
// 获取有效数字个数
size_t n = dq.size();

下面我提供一段测试代码,我们看对比一下对于vector和deque容器实例化的数组,用sort函数排序,那个更快呢?

#include<ctime> // time函数头文件
#include<cstdlib> // rand函数头文件
#include<algorithm> // sort头文件
void test1() {srand(time(0));int N = 100000;vector<int> v; deque<int> de;for (int i = 0; i < N; i++) {auto e = rand() + i;v.push_back(e);de.push_back(e);}int begin1 = clock();sort(v.begin(), v.end()); // 排序v对象的数据int end1 = clock(); int begin2 = clock();sort(de.begin(), de.end()); // 排序de对象的数据int end2 = clock();cout << "vector sort" << end1 - begin1 << endl;cout << "deque sort" << end2 - begin2 << endl;
}

输出结果

可以看到,vector数组的速度几乎是deque的5倍。

4、deque的缺陷

deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其 是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实 际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

同时,在中间位置插入或删除数据时,如果原来的数据较多,就需要移动数据,但是由于deque的特殊结构,移动数据更加复杂,消耗也很大。

三、总结

deque容器并不是完美的,但是,deque在作为容器适配器stack和queue的底层默认的结构时却非常恰当,因为结合了vector和list的部分优势恰好满足栈和队列。所以,deque一般仅用于作为stack和queue的底层数据结构。

http://www.dtcms.com/a/392134.html

相关文章:

  • llm的ReAct
  • C++ 参数传递方式详解
  • 前端实战开发(一):从参数优化到布局通信的全流程解决方案
  • iOS 层级的生命周期按三部分(App / UIViewController / UIView)
  • 第一章 自然语言处理领域应用
  • GitHub又打不开了?
  • OpenAI回归机器人:想把大模型推向物理世界
  • QML学习笔记(五)QML新手入门其三:通过Row和Colunm进行简单布局
  • 按键检测函数
  • CTFshow系列——PHP特性Web109-112
  • 字符函数与字符串函数
  • 酷9 1.7.3 | 支持自定义添加频道列表,适配VLC播放器内核,首次打开无内置内容,用户可完全自主配置
  • Slurm sbatch 全面指南:所有选项详解
  • 使用SCP命令在CentOS 7上向目标服务器传输文件
  • Kindle Oasis 刷安卓系统CrackDroid
  • 最新超强系统垃圾清理优化工具--Wise Care 365 PRO
  • JeecgBoot权限控制系统解析:以具体模块为例
  • 2025年职场人AI认证与学习路径深度解析
  • 硬件开发_基于STM32单片机的智能垃圾桶系统2
  • CSS Display Grid布局 grid-template-columns grid-template-rows
  • 在 Spring Boot 中,针对表单提交和请求体提交(如 JSON) 两种数据格式,服务器端有不同的接收和处理方式,
  • NL2SQL简单使用
  • 数据结构:二叉树OJ
  • 【Linux手册】生产消费者模型的多模式实践:阻塞队列、信号量与环形队列的并发设计
  • Python + Flask + API Gateway + Lambda + EKS 实战
  • 【OpenGL】openGL常见矩阵
  • DeepSeek大模型混合专家模型,DeepSeekMoE 重构 MoE 训练逻辑
  • 450. 删除二叉搜索树中的节点
  • 实用工具:基于Python的图片定位导出小程序
  • 滚珠螺杆在工业机器人关节与线性模组的智能控制