我们在学习之前的数据结构链表和顺序表的时候,发现顺序表插入删除慢,但是下标查找等速度快,链表下面查找等速度慢,但是插入删除快。它们互补。
那么我们是否可以将两个数据结构结合起来,产生一个完美的数据结构呢?
那么就有一个结合了链表、顺序表两中数据结构的数据结构交双端队列,我们来看看它是否完美:
双端队列deque
从名字来看,它肯定擅长两端的操作。如下图,
结构介绍
同时它还能支持下标的访问,这是和链表大不相同的。那么既然能支持下标访问,肯定就有循序表的结构。我们来看看它具体的结构图:
它的结构和我们自己手动堆区申请的二维数组很像:
有一个二阶指针数组(map数组)存放一维数组的指针,然后模拟二维数组。而deque的结构为一与二维数组不同的就是deque的map数组是从中间开始存的。
下标访问
它的下标访问也就可以很快,例如我们要访问第N个数据,我们假设每组数据的长度是len,那么就是要访问N/len组数据,访问这组数据的第N%len个数据
前插和尾插
如果我们要进行前插,空间够就从后往前插数据;空间不够了,我们就可以往前开辟一个len长的数组,然后从这个数组末尾往前插入数据。
如果我们要进行尾插,空间够就往后插入数据;空间不够了,同样是开辟len长的数组,然后从前往后插入数据。
扩容
如果我们中央数组map有一边的空间不够了,那么就要扩容,然后继续保证两边都有空间。
它的扩容也不是很耗时,大量的数据是存在我们的一维数组里面的,我们只要拷贝一维数组的指针就行了。
结构的局限性
它的结构局限性很明显
一、中间的插入很慢,而且中间的数组都是满的,你里面再插一个数据,后面的就要挤到下一个数组里面去,下一个数组的数据就要依次挤直到挤到边上一格。所以中间插入耗时且复杂。
二、它的下标访问也比数组的下标访问慢了几倍。例如我们要进行数组的排序这种需要很多次下标访问的操作,那么就会有很多次的计算,相对耗时一点。但是比链表还是快不少。
STL的deque相关函数介绍
首先是构造函数的介绍。
构造函数
其实只要是学了前面string、vecotr、list就可以发现,它们的构造函数类型都是那六个:默认、带参、迭代器、拷贝、右值引用、初始化设定项列表构造。
那我就不多进行介绍了,这里面的内存池我先不介绍,后面统一出一期进行讲解。
我以举例来看这几个构造函数的实现:
析构函数
析构函数大概要释放我们的一维数组和map,系统自动在这个实例化deque类生命周期结束后调用。
operator=
赋值重载函数:
其实可以只用写第一个就行,下面的编译器会通过对应的构造函数隐式类型转换成第一个,然后用它赋值,版本较高的编译器会直接优化成对应的构造。但是我们如果写了这个可以保证在每个编译器版本下都有相同的运行速度。
迭代器相关函数
这都不用再说了吧,还不会的读者可以看这一篇博客:c++迭代器的介绍-CSDN博客
空间操作的函数
szie
size就是返回当前数据的个数
max_size
就是返回size_t形式的-1、
resize
重构数据大小到n
如果比原size小就缩到n这么大,删除的方式是尾删。
如果比原size要小就用val值来扩容,val可以是缺省参数默认给T(),要么是自己传val值。
可以通过下面的例子来看:
empty
判断容器是否为空。
shrink_ti_fit
我们看到上面map是有多余空间的,这个函数就是将多余空间去掉
数据操作函数
operator[]
重载的下标访问,返回引用让我们可以直接操作对应的数据。
at
它和operator[]是一个意思,这是上面的assert报错,这个则抛异常
front
返回第一个数据的引用
back
返回最后一个数据的引用
修改类函数
assign
就是重新给容器值覆盖之前的内容。
push_back
尾插
push_front
头插
pop_back
尾删
pop_front
头删
insert
定点从它前面插入,我们用迭代器来定点,然后插入的内容可以当做是新的一个deque内容,所以支持了5种函数(除了默认),和构造函数差不多
erase
定点删除,或者定区间删除。
swap
交换两个deque容器的内容
clear
清除数据
下面三个是用右值引用实现的插入,在某些情况下速度快于普通的插入
emplace
emplace_front
emplace_back
全局且和deque相关的函数
第一个就是逻辑运算符重载函数
应该没有什么多说的
第二个是全局的实例化swap,因为在函数参数匹配的情况下,实例化的swap优先于模板swap。防止用模板swap(会有中间变量产生)耗时。
deque的自我实现
首先我们知道了中间插入删除的复杂,所以平时大家也不会用这个,毕竟耗时,所以这里我也不实现了。
主要实现一下两边的插入删除操作。
这个容器是目前我实现的最复杂的了,不仅迭代器复杂,而且本体也复杂。
成员变量介绍
首先我们来看本体成员变量有哪些:
本体是由两个迭代器掌管始末节点,然后map掌管主数组的指针,map_size记录map的长度。
我们再来看看迭代器的成员变量
迭代器成员变量有当前一维数组的头指针begin和尾指针end,还有当前指针cur,同时有指向当前一维数组的指针node。还有个room_capacity常量是一维数组的固定长度。
首先我们来看迭代器的实现:
deque_iterator
首先我重命名了T*和T**,方便理清逻辑。
然后是构造函数我这里只写了三个,足够了:
构造函数
默认构造不比多说,全部指向空就行了。
拷贝构造其实都可以不用写,因为和编译器默认的浅拷贝是一样的。
第三个带参构造是给一维数组指针val和当前所在指针n(cur)实现初始化。
val可以确定begin、end以及node,cur就由n直接给。
这里还有析构和赋值,因为我们的参数都不涉及堆区内存,所以直接交给编译器就行了。
然后我们实现加减操作
操作符重载
这里我们可以先实现set_node,就是让node从当前位置往后或者前移动n位并改变begin和end
那么这里的+=和-=操作就好实现了
首先这里的if是防止输入负数的情况,我们直接调用对应的operator。
其次这里的x是我们要往前或者往后移动的距离,因为我们往里面push或者pop后,指针不一定会在起始或者末尾,所以我们可以把它当作在其实或者末尾来看,并加上对应的长度,方便我们计算。
这里我们还发现我的-=的x后面有备注,是当时自己琢磨实现的时候被坑了。如果没有-1就会在某些情况是10,下面的set_node就会跳转,但事实上我们不用跳转。
这里其实逻辑上并不难,但是实现是有点看细节的,大家可以不用看我的代码自己去实现一下。
后面的-、+、++、--都是直接复用我们的+=和-=的。
这里我要提一个小问题,我能否先实现operator+,operator-然后复用-=、+=呢?
理论上我们是可以的,但是有一些细节问题:
我们实现+、-的时候,是一定会创建拷贝的,并用它作为返回值。所以我们用+、-来复用+=和-=都会产生拷贝。而我们直接实现+=和-=是不需要创建临时变量的。
所以我们还是实现+=、-=来复用+、-而不是反过来。否则会增加我们的拷贝。
这里其实拷贝还好只是浅拷贝,但是这个问题不局限与此,比如我们string就用+=和+,string的拷贝是深拷贝,使用多了是很耗时的。
接下来是其他的操作符重载。应该是可以看得懂的
第一个我用的是累加来查看两个迭代器之间相隔了多少数据,其实有更加简单的,这里我就懒得写了。
这里我还有值得提示的重要点就是:
我们的deque成员变量是iterator类型的,如果我们创建const deque类,那么就会让所有成员变量变成const的,那么就会有const iterator的start和finish变量,但是我们实际需要的是const_iterator的start和finish,那么会在下面这个代码中出问题:
编译器就会因为无法从const iterator转变为const_iterator而报错,这个时候我们就要用到隐式类型转换,支持相应的隐式类型转换就要对应的构造函数,所以我们还要写一个对应的构造函数:
这样就会从const iterator转变为const_iterator。
deque
deque唯一比较复杂的就是扩容,但是原理是简单的,大家可以自己试试。