前言
为什么会存在deque呢?在c++标准库中deque是作为 stack和queue的底层容器就是deque,我们要是了解过list和vector就会知道这两种容器各有优劣,vector的优点是支持随机访问,进而可以支持排序和二分查找等算法,它的缺点是如果空间不够增容的话要付出一定的代价(性能的损失),头插头删和中间位置的插入删除需要挪动数据,因此比较慢,存在空间的浪费。
vector的优点是不需要扩容,没有空间的浪费,任意位置的删除和插入都很快(时间复杂度是O(1)) ,但是它也有很大的缺点,就是不支持随机访问(这个缺陷导致很多的算法都不能采用它)。
deque就是vector和list的代替方案,deque支持随机访问,而且任意位置的插入和删除都很快(时间复杂度为O(1)),空间的浪费也很少,所以可以用一个容器deque来替代vector和list,那么大家肯定很好奇它的底层是怎么实现的,那么让我们一起来看看吧!
目录
1.deque的接口介绍
2.deque实现随机访问和任意位置插入删除的原理
3.deque的缺陷
1.deque的接口介绍
从上面的接口中我们发现deque不仅支持头尾的删除和插入(时间复杂度都是O(1))还支持operator[],这就可以很好的支持了随机访问。
我们可以来使用一下:
#include<deque>
void test()
{
deque<int> dq;
dq.push_back(1);//尾插
dq.push_back(2);
dq.push_back(3);
dq.push_back(4);
dq.push_back(5);
dq.push_back(6);
dq.push_back(7);
for (int i = 0; i < dq.size(); ++i)
{
cout << dq[i]<<" ";
}
cout << endl;
//尾删
dq.pop_back();
dq.pop_back();
dq.pop_back();
dq.pop_back();
//头插
dq.push_front(10);
dq.push_front(20);
dq.push_front(30);
dq.push_front(40);
for (auto& e : dq)
cout << e << " ";//支持范围for就支持迭代器
cout << endl;
//头删
dq.pop_front();
dq.pop_front();
}
int main()
{
test();
return 0;
}
2.deque实现随机访问和任意位置插入删除的原理
为什么deque可以成为vector和list的替代方案呢?因为deque在一定程度上弥补了list不支持随机访问的缺点,也弥补了vector头插头删效率低和增容代价大的缺点,那么deque是通过怎么样来实现的那呢?
其实deque是一种折中的实现方式,它是通过一段一段的连续空间buf来存放数据的,这些buf则通map的中控映射来联系到一起,它是一种折中的实现方式。如图:
map实际上一个指针数组用来存放这个一段段空间的地址,如果map中存满了,开一段更大的map然后将map中存放的数组拷到新的空间中就好了。
那么deque是如何实现头插头删的呢,这一段一段的buf是从map的中间开始的,如果头插就在重新开一段buf空间,将数据插入buf中,再将新的buf的地址存到map的前面就可以了,如图:
deque的随机访问与它的迭代器实现有关,如图:
如果想要实现遍历的话,首先它的迭代器cur从first开始,判断cur是否等于last,如果不等于last,cur就一直++向后走,当cur等于last,node++,找到下一个buf,让first指向buf的开始,last指向buf的结束,cur从buf的开始向后走,直到走完所有的buf,如果是随机访问的话,就要进行计算来找到数据在哪个buf中。
deque地层上是假想的连续空间,实际上是分段连续的,为了维护他整体连续和随机访问,才有了这么复杂的结构。
3.deque的缺陷
deque看似很完美,但是实际上是有很大的缺陷的,它的迭代器实现的很复杂,而且在遍历的时候要频繁检查是否移动到某段小空间的边界,导致效率下降,随机访问的效率也不够高。而在序列式场景下,可能需要经常遍历,因此在实际中,需要线性结构时会优先考虑vector和list。
排序的效率测试,数据越多效率会越低,例如:
#include<vector>
#include<deque>
#include<iostream>
#include<time.h>
#include<algorithm>
using namespace std;
int main()
{
int i = 0;
srand((unsigned int)time(NULL));//给随机数种子
vector<int> v1;
deque<int> d1;
while (i < 1000000)//分别给vector和deque相同的十万个随机数
{
int k = rand();
v1.push_back(k);
d1.push_back(k);
i++;
}
//对vector和deque采用相同的算法进行排序,并记录时间
int v1begin = clock();
sort(v1.begin(),v1.end());
int v1end = clock();
int d1begin = clock();
sort(d1.begin(), d1.end());
int d1end = clock();
cout << "vector的排序时间:" << v1end - v1begin << endl;
cout << "deque的排序时间:" << d1end - d1begin << endl;
return 0;
}
那么为什么会选择deque作为stack和queue的底层默认容器呢?
因为stack和 queue不需要遍历(它们是没有迭代器的),只需要在固定的一端或者两端操作即可,stack中元素增长时,deque比vector效率高(扩容时不需要搬运大量数据),queue中的元素增长时,deque不仅效率高,而且内存使用率高(与vector相比)。它们结合了deque的优点避开了deque的缺点。