前言
双端队列,Double-ended queue,简称为deque是一种线性结构的一种容器;
在数据结构中出现的顺序表与链表,或者栈与队列都算是线性结构;
在结构中,它与vector相比较会相似一些;
但是在实际当中,双端队列 - deque 包含了vector与list的优点;
- vector(顺序表)
支持随机访问,空间连续;尾插尾删效率高,但是头部插入删除以及中间插入删除的开销过大,扩容代价高; - list(链表)
任何位置的插入删除效率高,无扩容代价,但是内存碎片较多,且不支持随机访问;
而deque综合了上面两者部分的优点;
- deque(双端队列)
支持随机访问,头尾插入删除效率高,扩容代价低;
deque的结构
为什么双端队列 - deque既能支持头尾插入删除,又能支持下标随机访问?
这应该和它的结构有关;
deque
的结构类似于vector
,因为和vector
一样,总体的框架为一个连续的物理空间;
但是与vector
不同的是vector
作为类模板容器,数据类型为 T;
而在这里deque
所存储的其实为一个指针;
这个指针的类型为T*
;
可以理解为一个数组内存储多个小数组从而达到对每个数据进行存储;
但是在结构中的起始位置,为了能便于支持头尾插入删除,初始的buff
数组所在的位置并不是在deque
总体框架的开头,而是在中间;
deque的接口设置
deque
的接口设置与大部分的容器都相同;
在接口设置中较为相似list;
为什么说是list与vector的结合呢,还有一点;
deque
重载了operator[]
;
这也是它可以对数据进行随机访问的一个原因;
那么有个问题,在如此复杂的条件下是怎么进行数据的随机访问?
可以进行假设;
假设存在一个大小为100的
deque
对象,其中每个buff
小数组的大小为10,且100个数据中有3个头插的数据,现在需要去访问它的第25个数据应该怎么进行访问;只需要2步即可:
用n减去头插的数据数个数(单独未满的
buff
数组数据个数)再除以数组总大小得是第几个buff
数组;即(25-3)/10;
再用n减去头插的数据数个数(单独未满的
buff
数组数据个数)再除0数组总大小得是buff
数组中的第几个数据;即(25-3)%10;
数据的插入删除
从上图中可以看出,若是需要进行头删头插或者尾删尾插时,只需要控制每个buff
小数组即可;
由于初始buff
数组所在位置处中间位置,所以可以更好的进行插入删除;
- 头部插入删除
第一次头插时只需要在指向首段buff
的位置前再申请一块同样大小的空间即可;
再进行头插的时候,由于是头插,需要数据从后往前插入;
删除也为同样的操作;
[如图所示,头插依次插入0,-1];
- 尾部插入删除
尾部插入删除与头部插入删除相同,若是该段buff
数组已满,则需要新开一个buff
小数组用于存储数据;
删除也是如此;
- 中间插入删除
在deque
中较难的是这个在中间位置的插入删除;
就如中间插入而言,deque的处理办法有两个办法,但是无论是哪个办法都会有缺点;
但两点办法的总结也就是:
固定buff 数组大小 |
不固定buff 数组大小 |
若是中间插入时固定buff 数组大小,则在中间插入删除时需要大量的挪动数据,造成大量的开销 |
若是中间插入时不固定buff 数组大小,即在每次插入删除的时候,尤其是在插入时,扩容所对应的buff 数组,该方法可以优化中间插入,使得在中间插入时不需要大量的挪动数据,但是对应的缺点是无法使用/配合% 的方式进行下标的随机访问; |
然而在STL的源码中所使用的方法为固定buff
数组的大小,也就是抛弃了deque
的中间插入删除;
双端队列的应用场景
在实际的应用场景中,使用到双端队列deque
的场景并不多;
虽然它结合了vector
的优点和list
的优点,但是并没有十分的优异,换句话说就是无论是在效率上还是在有点伤都不能完全的取代vector
与list
;
在STL中,栈与队列所采用的方式为适配器模式,它们的模板参数为:
template<class T , class Container = deque<T>>
在适配器模式中的模板参数Container默认为deque<T>
,这也是双端队列中最经典的使用场景;