首页 > 代码库 > STL之deque容器的实现框架

STL之deque容器的实现框架

说明:本文仅供学习交流,转载请标明出处,欢迎转载!

       vector底层采用的是一个数组来实现,list底层采用的是一个环形的双向链表实现,而deque则采用的是两者相结合,所谓结合,并不是两种数据结构的结合,而是某些性能上的结合。我们知道,vector支持随机访问,而list支持常量时间的删除,deque支持的是随机访问以及首尾元素的删除

        deque是double ended queue的缩写,读作deck。首先我们用一个图来说明deque底层所采用的数据结构。


        这个数据结构有一种似曾相识的感觉吧!没错,你好像在哪里看过:你可以将这个数据结构变相地看成是一个二维数组,可将map看成是该二维数组的数组名;当然,你也可以将该数据结构与当初我们学习OS时的内存分配中的分页式存储管理相结合。这样,你会发现,其实知识都是相同的,这就是知识运用能力和迁移能力的体现吧。

        好了,说了那么多,下面我们给出deque容器的设计思路吧。

        (1)迭代器的设计。与list容器相类似的地方是,我们需要重新规划下deque迭代器的定义,从上图中我们也很容易可以看出,deque容器采用的就是一种分段连续的存储空间,既然是分段连续,那么支持随机访问的deque容器就需要对迭代器做特殊地处理,首先我们给出迭代器的设计图


       从上图可以看出,deque的迭代器由四个属性组成,这四个属性组成了我们随机访问一个容器内元素的必要成分。我们再介绍利用该迭代器随机访问容器内的元素时,先介绍下我们是如何随机访问一个二维数组a[i][j]的。我们知道,对一个二维数组,我们首先需要找到第一维i的入口地址,找到该入口地址后,我们再去找对应j的地址,所以对于数组a[i][j]的另外一种访问方式是*(*(a+i)+j),当然我们再访问这个一个数组元素的时候,一定要注意不要越界,否则会引起严重的运行错误。根据这一点,我们每次访问一个数组元素的时候,都会判断该元素的下标是否越界。类似地,deque容器的迭代器每一个组成部分都有着重要的不可缺少的作用。上图中,node用于指向的“第一维”的某个位置,该位置对应要访问元素的入口地址,剩下的三个属性则用于“第二维”,[first,last)规定了访问本区间元素的边界条件,而curr则指向实际我们要访问的元素。当然,所有对该类迭代器的访问操作都必须建立在相应运算符的重载上。下面我们给出deque迭代器的实现框架:

/***********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;//定义下表访问功能,随机访问迭代器的属性
};
      deque容器的两个重要的迭代器

        1. start:绑定到第一个有效的map结点和该结点对应的缓冲区。当然STL将有效的元素防止中间,预留空间放两边,这样做是为了更好地实现首尾的操作。

        2.finish:绑定到最后一个有效的map结点(不是下一个位置哈,别被end()给弄傻了)和该结点对应的缓冲区。

        具体如下图所示:


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


    总结:1.deque容器所有缓冲区的容量都是一样的,范围都是:[first,last)。

             2.deque容器中,必定包括两个迭代器,分别是:start和finish, map元素个数(第一维)>=2时,start绑定到第一个有效的map结点及其对应的缓冲区,finish当定到最后一个有效的map结点及其对应的缓冲区。

             3.start.curr和finish.cur严格遵守STL的原则,左闭右开元组[...),意思是说:start.cur指向的是其所绑定缓冲区第一个有效的元素,指针start.cur的移动方向是first<-------last;而finish.cur指向的是其所绑定缓冲区的最后一个有效元素的下一个位置,指针finish.cur的移动方向是first------->last。记住:start.cur指向的永远是一个含有实际值的位置,而finish.cur永远指向的是一个空位置。

            4.start所绑定的缓冲区已满的条件是start.cur=start.first,而当该缓冲区为空start.cur=start.last时,start会自动绑定到别的缓冲区(删除最后一个有效元素时发生);finish所绑定的缓冲区已满的条件finish.cur=finish.last,为空的条件是finish.cur=cur.first。

     (2)deque容器的设计。最上面的那个给出了deque容器的结构,我要说明的是,与vector相比,deque容器多出了pop_front()和push_fron()操作这两个首部的增删操作;而与list相比,deque多出了对容器元素的随机访问功能。然而,任何方便的操作都需要背后复杂数据结构的支撑,与前两个容器相比,deque容器在中间插入和删除操作是相当复杂的。在正式给出deque的代码框架之前,我首先介绍下deque容器的插入操作和删除操作中,内部所做的事情。

        deque容器的插入操作

        deque容器的插入操作分三种情况。

      (1)在容器的首部插入,push_front(const T &t)。先找到start绑定的缓冲区,判断该缓冲区是否还有空闲位置(若start.curr==start.first,则表示没有空闲位置,否则表示有。因为头部的curr指向的是含有实际值的位置,而尾部的curr指向的是含有实际值的下一个位置,这符合stl的规则,即左闭由开的原则,[..)),如果有则在该空闲位置插入元素t,同时更新start.curr,若没有空闲位置可以插入元素t,则在start.node(这是第一维)的前一个位置申请一个缓冲区,同时将start绑定到该新缓冲区(即设定start的first,last),并将start.cur指向新申请缓冲区的最后一个有效的位置(即star.cur=star.last-1)。

    (2)在容器的尾部插入,push_back(const T& t)。先找到finish所绑定的缓冲区,判断该缓冲区是否有空闲位置(若finish.curr==finish.last,则表示没有空闲位置,否则表示有。因为finish的curr指向的是含有实际值的下一个位置,last表示最后一个实际值的下一个位置,若两者相等,则表示最后一个空闲位置用完了)。

     (3)在容器的指定位置前插入元素,insert(iter,t)、insert(iter,n,t)、insert(iter,b,e)。这里,我们将容器空间分成三个部分(前面部分,iter,后面部分),在插入之前,我们需要比较前面部分元素的个数与后面部分元素的个数,由于指定位置的插入必然会引起元素的移动(首、尾插入除外),那么我们移动的是前后两部分中,元素个数比较少的那部分元素,这样做的效率会更高,STL就是凡事以效率为前提

       需要特别指的注意的是,在首部或尾部的插入操作中,有可能引发在插入端没有空余的map空间了,这个时候我们需要特别注意,STL的做法并不是因为该操作端没有空闲的空间了就直接重新申请新的空间,而是先判断下另一端是否有足够的空间(SGI STL的判断标准是 map_size>2*已使用的map个数),如果空间足够,则不需要重新分配map,只需要移动下原来已有map元素的指针的位置即可。如果不够,则重新分配(三部曲:申请空间、复制元素、删除旧空间)。

         deque容器的删除操作

      (1)删除首部元素,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。

     (2)删除尾部元素,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)。

     总结:start的cur一定是指向一个有效元素,而finish的cur一定是指向最后一个有效元素的下一个位置,也就是说finish的cur指向的一定是一个空位置。所以,start.cur所绑定的缓冲区为空时,一定要重新为其绑定一个新的缓冲区,不过有没有其他操作请求;而即便finish.cur所绑定的缓冲区为空时,如果没有其他操作的请求,无需为finish绑定新的缓冲区。

     (3)指定位置删除元素,erase(iter),erase(b,e)。这里我只想提下与插入操作相似的点,那就是删除元素之后,一定会涉及到移动操作,是移动左边部分的元素还是右边部分的元素呢?这取决于两者元素的个数。STL从效率的角度考虑,只会移动元素个数少的那部分。

       下面总结下使得deque迭代器失效的操作。

       使deque迭代器失效的操作

       1.在deque的首部或尾部插入操作时,通常情况下迭代器是不会失效的,但是也有例外。例外情况是,当在容器的首部或尾部做频繁的插入操作使得deque的两端的预留空间用得差不多时(还记得上面的判断条件SGI STL的判断标准是 map_size>2*已使用的map个数吗)会引起内存空间的重新配置(三部曲),这种特殊情况下容器内所有的迭代器都将失效

       2.在deque的首部或尾部做删除操作时,除了被删除元素所对应的迭代器外,其他任何元素对应的迭代器都不会失效。

       3.在deque的中间做插入或删除操作时,可能会使得被插入位置或删除位置的左边所有迭代器失效,也可能使得其右边的所有迭代器失效,具体那边的迭代器失效得视左右两边元素的个数而定,元素个数少的那边的所有迭代器都会失效,而元素个数多的那边的所有迭代器都不会失效。这里也是我跟C++primer不一样的地方,《C++primer 第4版 中文版》P282中提到,对于deque容器,如果删除时不包含第一个元素或最后一个元素,那么该queue容器相关的所有迭代器都会失效,我觉得这句话是错误的,应该视具体情况而定,正如我分析的那样。

      最后,给出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;//定义第一维的元素个数
};

参考文献:

[1]《STL源码剖析 侯捷》

[2]《C++ primer 第4版》