【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) |
|
头部(push_front) | O(n) | O(1) |
|
中间(指定迭代器位置) | O(n) | O(1) |
3、对于删除数据时的时间复杂度对比分析:
删除位置 | vector 时间复杂度 | list 时间复杂度 | 核心原因 |
---|---|---|---|
尾部(pop_back) | O(1) | O(1) |
|
头部(pop_front) | O(n) | O(1) |
|
中间(指定迭代器位置) | O(n) | O(1) |
|
4、迭代器特性对比⭐️
特性 | vector 迭代器 | list 迭代器 |
---|---|---|
迭代器类型 | 随机访问迭代器(RandomAccessIterator) | 双向迭代器(BidirectionalIterator) |
支持的操作 | 支持++、--、 + 、- 、+= 、-= 、[] 等算术操作(如 it + 5 直接跳 5 个元素) | 仅支持 ++ 、-- 操作(只能逐个移动) |
迭代器失效⭐️ | - 扩容时,所有迭代器、指针、引用均失效; - 插入 / 删除中间元素时,插入 / 删除位置后的迭代器失效 | - 仅「被删除节点的迭代器」失效; - 插入元素时,所有迭代器均有效 |
5、总结:
优先选择 vector
的场景:
1、需要频繁随机访问元素(如通过下标读取、排序、二分查找 ——vector
支持 std::sort
,list
需用自身的 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的特殊结构就刚好结合了 v
ector
(随机访问)和 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的底层数据结构。