一、deque的一些特点
- 支持随机访问,即支持[ ]以及at(),但是性能没有vector好。
- 可以在内部进行插入和删除操作,但性能不及list。
- deque 两端 都能够快速插入和删除元素,而vector只能在尾端进行。
- deque的元素存取和迭代器操作会稍微慢一些,因为deque的内部结构会多一个间接过程。
- deque迭代器是特殊的智能指针,而不是一般指针,它需要在不同的区块之间跳转。
- deque可以包含更多的元素,其max_size可能更大,因为不止使用一块内存。
- deque不支持对容量和内存分配时机的控制。(?)
- 在除了首尾两端的其他地方插入和删除元素**,都将会导致指向deque元素的任何pointers、references、iterators失效**。但,deque的内存重分配优于vector,因为其内部结构显示不需要复制所有元素。
- deque的内存区块不再被使用时,会被释放,deque的内存大小是可缩减的。不过,是不是这么做以及怎么做由实际操作版本定义。
- deque 不提供容量操作:capacity()和reverse(),但是vector可以
二、deque实现框架
直接用侯捷老师的deque底层示意图说明其数据结构吧,
结合上述的特点我们就能明白deque的线性空间其实是一个假象,真实的底层是用一个map主管来管理每一小块连续内存的地址,是一种分段连续的存储空间, 因此迭代器需要在不同区块之间跳转,也就造成了随机访问和内部插入删除比不上vector和list。
ok, 开始deque的设计框架
1, 从图片可知基本信息
从上述分析我们知道了deque容器采用一种分段式存储空间,那么支持随机访问的deque就需要对迭代器进行处理,从图我们就知道的是:
1,首先我们要保证每一个小连续内存的容量(缓冲区容量)都是一样的;
2,需要两个迭代器,start
和finish
,分别指向第一个有效的map节点及其缓冲区,和最后一个有效的map节点及对应缓冲区,依旧是严格的STL原则左闭右开
3,最重要的一点,start和finish对应的缓冲区的空或满判断是不一样的,每一个缓冲区都有三个指针first
、cur
、last
,开头缓冲区start满了是start.cur == start.first
,空了是start.cur == start. last
, 而finish缓冲区对应的就是我们下意识的状态, 满了就是finish.cur == finish.last
4,当然还有管理中心map
2,deque框架代码👇
依旧是熟悉的:
公有访问属性、构造函数和析构函数、插入操作、删除操作、大小操作、访问操作、和上述分析的成员变量们
/***************deque容器的定义**********************/
template<class T,class Alloc=alloc,size_t Buffsiz=0>
class deque
{
public:
/***********定义公有访问属性****************/
typedef T value_type;//定义元素类型
typedef value_type *pointer;//定义指针类型
typedef T& reference;//定义引用类型
typedef ptrdiff_t difference_type;//定义迭代器才差值类型
typedef size_t size_type;//定义大小类型
typedef _deque_iterator<T,T&,T*,BuffSiz> iterator;//迭代器类型
typedef _deque_iterator<T,const T&,const T*,BuffSiz>const_iterator;//指向常量的迭代器类型
typedef deque<T,Alloc,Buffsize> self;//定义容器类型
/***********构造函数/析构函数*****************/
deque();//无参构造函数,将其map和map_size设置为0
deque(const self & deq);//定义复制构造函数
deque(Input_Iterator b,Input_Iterator e);//[b,e)可以是任意容器的迭代器
deque(size_type n,const T &t);//用n个初值t去创建容器
deque(size_type n);//创建含有n个元素的容器
~deque();//析构函数的定义
/*************插入操作*********************/
void push_back(const T &);//后插入
void push_front(const T &);//前插入
iterator insert(iterator iter,const T &t);//在iter前插入,返回新插入元素的位置
void insert(iterator iter,iterator b,iterator e);//将[b,e)范围内的元素插入到iter之前
void insert(iterator iter,size_type n,const T& t);//在iter前插入n个初值为t的元素
/*************删除操作********************/
iterator erase(iterator iter);//删除iter所指向元素,返回该元素对应的下一个元素的迭代器
iterator erase(iterator b,iterator e);//删除[b,e)范围内的元素,返回原先e的位置
void clear();//删除容器内的所有元素
void pop_back();//返回容器内最后一个有效的元素
void pop_front();//返回容器内第一个有效的元素
/*************大小操作*******************/
size_type size()const;//返回容器内元素的格式
size_type max_size()const;//返回容器可容纳的最多元素的个数
bool empty()const;//判断容器是否为空
void resize(size_type n);//将容器的大小设置为n
void resize(size_type n,const T t);//将容器的大小设置为n,容需要新添加元素,则其初值为t
/*************访问操作*****************/
iterator begin()const;//返回头指针
iterator end()const;//返回末端元素的下一个位置
iterator rbegin()const;//返回最后一个有效的元素
iterator rend()const;//返回第一个有效元素的前一个位置
reference front()const;//返回第一个元素的引用
reference back();//返回最后一个元素的引用
reference operator [](size_type i);//返回第i个元素的引用
refrence at(size_type i);//返回第i个元素的引用
protected:
typedef T** map_pointer;//定义指向map类型(相当于第一维)的类型别名
iterator start;//指向map(第一维)的第一个元素
iterator finish;//指向map(第二维)的最后一个有效的元素
map_pointer map;//相当于二维数组的首地址
size_type map_size;//定义第一维的元素个数
};
3, 迭代器实现框架
其中迭代器是实现deque的核心
迭代器由四个属性组成,这四个属性组成了我们随机访问一个容器内元素的必要蹭分,node
用于指向map的某个位置(第一维的),该位置对应要访问元素的入口地址,剩下的三个属性则作用对应的缓冲区(第二维),[first, last)
规定了访问元素所在缓冲区的边界条件,而cur
则指向实际我们要指向的元素。
所有对类迭代器的访问操作都必须建立在相应运算符的重载上
下面是deque迭代器的实现框架:
①定义迭代器类型 ②固定缓冲区的大小 ③迭代器的几个类型别名
④迭代器的三个属性,first
、cur
、last
⑤迭代器的操作(依赖各种运算符重载)
/***********deque迭代器的设计***********/
template<class T,class Ref=T&,class Ptr=T*,size_t Bufsize=0>//Bufsize表示缓冲区的大小
struct _deque_iterator
{
typedef _deque_iterator<T,T&,T*,Bufsize> iterator;//定义迭代器的类型
typedef _deque_iterator<T,const T&,const T*,Bufsize> const_iterator;//定义常迭代器的类型,可以看出只是对引用和指针加以const限定
static size_t buffer_size();//返回缓冲区的大小,以个为单位
/************迭代器的几个类型别名*********/
typedef random_access_iterator_tag iterator_category;//deque迭代器的tag为随机访问迭代器
typedef T value_type;//元素类型
typedef Ptr pointer;//指针类型
typedef Ref reference;//引用类型
typedef size_t size_type;//大小类型
typedef ptrdiff_t difference_type;//指针差值类型
typedef T** map_pointer;//想到二维数组的数组名就是一个二级指针
map_pointer node;//node可以看成是第一维
/*************deque迭代器包含的几种重要的属性******/
T *first;//缓冲区开始处,可看成是第二维第一个元素的位置
T *last;//缓冲区末端的下一个位置,第二维的最后元素的下一个位置
T *curr;//可以看成是原生指针,表示当前的元素位置
//如果是第一个块缓冲区,则指向当前实际的元素位置;
//如果是最后一个缓冲区,则指向当前实际元素的下一个位置,符合STL中[...)的原则
/************迭代器的操作*************/
reference operator *()const;//return *current;定义解引用操作
reference operator ->()const;//return current;定义箭头操作符
difference_type operator-(const iterator & x)const;//定义两迭代器相减的操作
typedef iterator self;
bool operator==(const self &x)const;//判断两个迭代器是否相等
bool operator!=(const self &x)const;//判断两个迭代器是否不相等
bool operator<(const self &x)const;//先比较第一维,相同则再比较第二维
void set_node(map_pointer new_node);//将当前的迭代器设置为new_node,主要是设置node、first、last属性的值
self& operator++();//定义前置自加,++p,返回引用,因为是return *this
self operator++(int);//定义后置自加,p++,返回非引用,因为是return temp,不能返回局部对象的引用
self& operator--();//定义前置自减,--p,返回引用
self operator--(int);//定义后置自减,p--,返回非引用
self& operator+=(difference_type n);//将本迭代器自加n步,随机访问迭代器的属性
self& operator-=(difference_type n);//本迭代器自减n步,随机访问迭代器的属性
self operator-(difference_type n);//返回一个将本迭代器减n后的temp值,随机访问迭代器的属性
reference operator[](difference_type n)const;//定义下表访问功能,随机访问迭代器的属性
};
4,阅读源码前预防针
deque在vector的基础上增加了pop_front
和push_front
功能,在list基础上增加了随机访问功能。任何方便的操作背后都有复杂的数据结构,先了解一下实现deque的插入和删除操作,内部所做的事情
deque的插入操作分为三种情况:
- 在容器的首部插入:
push_front(const T& t)
先找到start绑定的缓冲区,判断该缓冲区是否还有空闲位置,如果有则在该空闲位置插入元素t,同时更新start.cur,若没有空闲位置可以插入元素t,则在start.node(map) 的前一个位置申请一个缓冲区,同时将start绑定到该新缓冲区(即设定start的first,last),并将start.cur指向新申请缓冲区的最后一个有效的位置 - 在容器的内部插入:
push_back(const T& t)
先找到finish所绑定的缓冲区,判断该缓冲区是否有空闲位置, 有的话插入并更新,没有的话到map处申请 - 在容器的指定位置前插入元素:
insert(iter,t)
、insert(iter,n,t)
、insert(iter,b,e)
我们将容器空间分成三个部分前面部分,iter,后面部分
,在插入之前,我们需要比较前面部分元素的个数与后面部分元素的个数,由于指定位置的插入必然会引起元素的移动(首、尾插入除外),我们移动元素个数少的那一端
需要特别指的注意的是,在首部或尾部的插入操作中,有可能引发在插入端没有空余的map空间
了,这个时候我们需要特别注意,STL的做法并不是因为该操作端没有空闲的空间了就直接重新申请新的空间,而是先判断下另一端是否有足够的空间(SGI STL的判断标准是 map_size>2*已使用的map个数),如果空间足够,则不需要重新分配map,只需要移动下原来已有map元素的指针的位置即可。如果不够,则重新分配(像vector的三步:申请空间、复制元素、删除旧空间)
deque的三种删除操作:
- 删除首部元素:
pop_front()
若start所绑定的缓冲区上有多个(两个及以上)元素,此时只需将第一个有效元素析构即可destroy(start.curr)
,析构完后再将start.cur++
;
若start所绑定的缓冲区上只有一个元素,即start.curr==start.last-1,则将该元素析构后destory(start.cur),需要先将start绑定的缓冲区的空间收回deallocate_node(start.first)
,再将start重新绑定map结点 和缓冲区start.set_node(start.node+1),start.cur=start.first
。 - 删除尾部元素:
pop_back()
同理,若finish所绑定的缓冲区上有1个或多个元素,此时只需将最后一个有效元素析构即可destroy(--finish.curr)
;
若start所绑定的缓冲区是空缓冲区,即finish.curr==finish.first,则先将该空缓冲区的空间释放掉deallocate_node(finish.first)
,再将finish绑定到上一个map结点和缓冲区finish.set_node(finish.node-1),finish.cur=finish.last-1
,最后将最后一个元素析构destroy(finish.cur)。 - 删除指定位置元素: 指定位置删除元素,
erase(iter)
,erase(b,e)
删除操作一定会涉及到移动操作,是移动左边部分的元素还是右边部分的元素取决于两者元素的个数。STL从效率的角度考虑,只会移动元素个数少的那部分。
使deque迭代器失效的操作
-
在deque的首部或尾部插入操作时,通常情况下迭代器是不会失效的,但是当频繁的插入操作使得deque的两端的预留空间用得差不多时,map快用完时(SGI STL的判断标准是
map_size>2*已使用的map个数
)会引起内存空间的重新配置,这种情况下容器内所有的迭代器都将失效。 -
在deque的首部或尾部做删除操作时,除了被删除元素所对应的迭代器外,其他任何元素对应的迭代器都不会失效。因此在deque的中间做插入或删除操作时,可能会使得被插入位置或删除位置的左边所有迭代器失效或右边的所有迭代器失效,具体哪边的迭代器失效得视左右两边元素的个数而定,元素个数少的那边的所有迭代器都会失效,而元素个数多的那边的所有迭代器都不会失效。
这部分内容感谢并参考侯捷老师《STL源码剖析 侯捷》和博客https://blog.csdn.net/JXH_123/article/details/34114139
三、源码
源码见下篇吧,放上代码觉得,这篇略长了
简单开个思想:
deque的构造与内存管理:
主要还是那张图,由于deque的设计思想就是由一块块的缓存区连接起来的,因此它的内存管理会比较复杂。插入的时候要考虑是否要跳转缓存区、是否要新建map节点(和vector一样,其实是重新分配一块空间给map,删除原来空间)、插入后元素是前面元素向前移动还是后面元素向后面移动(谁小移动谁)。而在删除元素的时候,考虑是将前面元素后移覆盖需要移除元素的地方还是后面元素前移覆盖(谁小移动谁)。移动完以后要析构冗余的元素,释放冗余的缓存区。
四、常用函数总结
deq[ ]:用来访问双向队列中单个的元素。
deq.front():返回第一个元素的引用。
deq.back():返回最后一个元素的引用。
deq.push_front(x):把元素x插入到双向队列的头部。
deq.pop_front():弹出双向队列的第一个元素。
deq.push_back(x):把元素x插入到双向队列的尾部。
deq.pop_back():弹出双向队列的最后一个元素。