STL容器解析之deque

vector我们可以理解为单向开口的线性空间,因为在只有在vector的尾端进行插入、删除操作才能在常数时间中完成。而deque我们可以理解为双向开口的线性空间,在deque的头部与尾部进行插入、删除操作都为常数时间复杂度。deque与vector还有一大差异,就是deque没有所谓的“容量”概念,它并没有像vector一样,划定空间的头部尾部界限,也不会发生因空间不足导致的大搬家的状况,所以deque在处理动态的空间需求变化时,比vector更具活性。

但deque也有缺陷。相比于vector的迭代器操作,deque复杂的不是一点半点,这是由于deque在其内部不只维护了一段线性空间,迭代器需要在多个线性空间中跳转。所以,即使是一个简单的递增操作,也可能花费数倍的时间来调整指针。

在一探deque内部实现之前,我们先明确几个概念,这些名词可能每个人都有不同的叫法,但其核心理念是一致的:

1.中控器

因为deque需要在内部维护多段线性空间,而且空间与空间之间可能在地址上并不连续,所以我们需要保存指向这些空间的指针,实现空间之间的“联结”。我们专门维护这样一个数组,其每个元素都是指向一个空间的指针,这样我们在空间中转移时只需要在数组中跳到其余元素就可以了。很自然的,我们需要保存一个在数组中当前位置的信息。这个信息的类型为指向指针的指针(T**),其指向数组中的某一位置(元素),而数组元素为指针类型。

2.缓冲区(节点)

我们一定要给那些空间起个名字,老是叫他们“那个空间”实在是难受~

上面我们提到过,deque为了保证在前后两端的动态插入,维护了多个缓冲区。如果我们push_front,它就可以视情况(如果当前将缓冲区已满)在前面添加一段空间,push_back也是一样。如果我们pop_front,它也可以视情况(如果当前缓冲区在去除一个元素后为空)去除首部的缓冲区,pop_back也一样。可以看出,这种多段缓冲区的设置给了deque无与伦比的灵活性。但是,也带来了超出vector一大截的编程复杂度,因为对于在任意位置的插入、删除操作,我们都需要检查是否要添加、去除缓冲区。这之间的利弊需要仔细的权衡。

我们依旧将deque分解来看:

1.迭代器操作

2.内存操作

3.元素操作

因为deque内部维护多段空间,所以在迭代器中我们不能只维护一个指向当前位置的信息,我们还需要指明当前所在的缓冲区是哪一个,也就是一个T**类型的成员。因为迭代器的移动操作需要跳转缓冲区,所以必须知道当前缓冲区的界限,也就是指向缓冲区收尾的指针。最后是指向当前位置的指针。

	public:
		//数据成员
		T* cur;       //指向当前元素
		T* first;     //指向当前缓冲区的头部
		T* last;      //指向当前缓冲区的尾部(尾后)
		map_pointer node;   //指向当前所在的缓冲区

其中map_pointer类型为:

	typedef T** map_pointer;typedef T** map_pointer;


上面已提到过,deque的迭代器类型为RandomAccessIterator,所以要支持的基本操作为:前进(++)、后退(--)、跳跃(+= ,-=)、迭代器加减。

首先来看迭代器加减操作,当两个deque迭代器做加减法时,我们要考虑当两个迭代器中隔着多个缓冲区的情况(我们默认被减数迭代器位置于减数之前)。其结果等于

被减数迭代器与其所在缓冲区尾的距离 + 缓冲区大小×中间相隔的缓冲区数 + 减数迭代器与其缓冲区头部距离

		typedef ptrdiff_t Distance; //迭代器距离类型
		Distance operator-(const self& rhs) const
		{
			Distance d=Distance(buf_siz(BufSiz,sizeof(T))*(node - rhs.node - 1)+
				(cur - first) + (rhs.last - rhs.cur));
			return d;
		}

迭代器的前进、后退操作只需加上判断条件与缓冲区跳转操作就可以了:

#define buf_siz(x,y) _deque_buf_size(x,y)
	size_t _deque_buf_size(size_t n, size_t sz)
	{//制定缓冲区大小,n为用户指定的模板非类型参数Bufsiz,sz为元素大小
		return n != 0 ? n : ((sz > 512) ? size_t(1) : size_t(512 / sz));
	}

	protected:
		//缓冲区跳跃操作(new_node为目的缓冲区)
		//cur会在使用set_node的那些操作中单独设置
		void set_node(map_pointer new_node)
		{//调整界限指针
			first = *new_node;
			last = *new_node + Distance(buf_siz(BufSiz,sizeof(T)));
			node = new_node;
		}

		self& operator++()
		{
			++cur;
			if (cur == last)
			{
				set_node(node + 1);
				cur = first;
			}
			return *this;
		}
		self operator++(int)
		{
			iterator temp = *this;
			++*this;
			return temp;
		}

		self& operator--()
		{//不能在判断出界之前递减cur,因为在first之前并没有能判断cur出界的缓冲单元
			if (cur == first)
			{//在递减之前已到达头部,递减后出界
				set_node(node - 1);
				cur = last;
			}
			--cur;
			return *this;
		}
		self operator--(int)
		{
			iterator temp = *this;
			--*this;
			return temp;
		}

迭代器跳跃操作是实现随机存取的基本。首先我们要算出要跳跃多少个元素,然后我们计算出需要跳跃多少个缓冲区,然后先跳跃缓冲区,再在缓冲区中找到定位。值得一提的是,我们允许在任何跳跃操作中跳跃值为负,可能在直观上感到别扭,但简化了代码:

		//实现随机存取,迭代器跳跃n个距离
		//允许n为负值以简化代码
		self& operator+=(Distance n)
		{
			Distance offset = n + (cur - first);//计算需要跳跃的元素数
			if (offset >= 0 && offset < Distance(buf_siz(BufSiz,sizeof(T))))//目标在同一缓冲区内
				cur += n;
			else
			{//下面计算缓冲区偏移量
				Distance node_offset =
					offset>0 ? Distance(offset / buf_siz(BufSiz,sizeof(T)))
					: -Distance((offset - 1) / buf_siz(BufSiz,sizeof(T))) - 1;//这里offset-1是减掉前一个缓冲区的last
				set_node(node+node_offset);
				//设置cur
				cur = first + (offset - (node_offset*buf_siz(BufSiz,sizeof(T))));
			}
			return *this;
		}


当我们实现了一个操作后,类似其他操作皆可以基于其实现,可以避免代码的重复:

		self& operator-=(Distance n)
		{
			return *this += -n;
		}

		self operator+(Distance n)const
		{
			iterator temp = *this;
			temp += n;
			return temp;
		}
		
		self operator-(Distance n)const
		{
			iterator temp = *this;
			temp -= n;
			return temp;
		}

然后是迭代器的成员操作:

		//迭代器随机存取
		Reference operator[](size_t n){ return *(*this + n); }

		bool operator==(const self& rhs)const{ return cur == rhs.cur; }
		bool operator!=(const self& rhs)const{ return !(*this == rhs); }
		bool operator<(const self& rhs)const
		{
			return node == rhs.node ? (cur < rhs.cur) : (node < rhs.node);
		}


以上就是deque迭代器支持的所有操作。由此可见,即使是一个最简单的移动操作,相比于vector的原生指针迭代器,增加了判断,还有可能引起一次函数调用以及三次赋值操作。其复杂度增加了不只一点半点。

解析内存操作的第一步是看当我们构造一个deque时发生了什么:

	public:
		stl_deque()
		{
			fill_initialize(0, T());
		}
		stl_deque(size_type n, const T& value) :start(), finish(), map(0), map_size(0)
		{
			//以下函数配置中控器空间以及缓冲区空间,并构造n个值为value的元素
			fill_initialize(n, value); 
		}
		stl_deque(size_type n) :start(), finish(), map(0), map_size(0)
		{
			fill_initialize(n, T());
		}


fill_initialize是我们真正构造一个deque的第一步:

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T,Alloc,BufSiz>::fill_initialize(size_type n,const T& value)
	{
		create_map_and_nodes(n);	//配置中控器空间以及缓冲区空间
		map_pointer cur;
		try{
			if (n == 0)
				return;
			//以下操作构造元素
			for (cur = start.node; cur < finish.node; ++cur)
				uninitialized_fill(*cur, *cur +buf_siz(BufSiz,sizeof(T)), value);
			//最后一个缓冲区并不填满
			uninitialized_fill(finish.first, finish.cur, value);
		}
		catch (...)
		{
			//commit or rollback
			for (; cur >= start.node; --cur)
				destroy(*cur, *cur + buf_siz(BufSiz, sizeof(T)));
			for (cur = start.node; cur <= finish.node; ++cur)
				data_allocator::deallocate(*cur, buf_siz(BufSiz, sizeof(T)));
			throw;
		}
	}


fill_initialize内部按照惯例将空间配置与元素构造分而治之。create_map_and_nodes负责空间配置:

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::create_map_and_nodes(size_type num_of_elem)
	{
		//若整除,则额外多配置一个缓冲区
		size_type num_of_node = num_of_elem / buf_siz(BufSiz,sizeof(T)) + 1;
		//最少为8个缓冲区
		map_size = max(num_of_node, 8);
		//配置中控器
		map = map_allocator::allocate(map_size);
		//将起始位置设为所有缓冲区的中间部分,这样两边的可扩展区域对称
		map_pointer node_start = map+(map_size-num_of_node)/2;
		map_pointer node_finish = node_start+num_of_node-1;
		map_pointer cur;
		//配置缓冲区空间
		try
		{//下面配置缓冲区只在node_start与node_finish之间配置,其余作为备用
			for (cur = node_start; cur <= node_finish; ++cur)
				*cur = data_allocator::allocate(buf_siz(BufSiz,sizeof(T)));
		}
		catch (...)
		{
			//commit or rollback
			for (; cur >= node_start; --cur)
				data_allocator::deallocate(*cur);
		}
		//首先设置deque内部两个界限迭代器所在的缓冲区
		start.set_node(node_start);
		finish.set_node(node_finish);
		//其次让设置迭代器当前指向的元素位置
		start.cur = start.first;	//第一个迭代器指向第一个缓冲区的首部
		finish.cur = finish.first + num_of_elem%buf_siz(BufSiz,sizeof(T));
		//如果正好整除,cur指向额外多配的节点的起始处
		//finish.cur指向最后一个元素的后方
	}

在create_map_and_node之后,fill_initialize为每个缓冲区配置元素。

对deque的元素操作我们先从push_back()与push_front()入手:

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::push_back(const T& value)
	{
		if (finish.cur != finish.last - 1)
		{//有两个及以上的备用空间(至少有finish.cur与finish.last-1两个空间)
			construct(finish.cur, value);
			++finish.cur; //在之前已经判断过是否出界,这里不使用迭代器的递增操作符,避免函数调用
		}
		else
			push_back_aux(value);
	}

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::push_front(const T& value)
	{
		if (start.cur != start.first)
		{
			construct(start.cur, value);
			--start.cur;
		}
		else
			push_front_aux(value);
	}

push_back()与push_front()都调用了一个辅助函数来处理特殊情况。对于push_back(),当前缓冲区的剩余空间可能不足,需要在尾端加入一个新缓冲区。对于push_front(),则有可能需要在头部加入一个新缓冲区。

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::push_back_aux(const T& value)
	{
		reserve_map_at_back();//首先判断map是否有多余空间
		*(finish.node + 1) = data_allocator::allocate(buf_siz(BufSiz, sizeof(T)));
		try{
			construct(finish.cur, value);
			finish.node = finish.node + 1;
			finish.cur = finish.first;
		}
		catch (...)
		{
			finish.node = finish.node - 1;
			finish.cur = finish.last;
			data_allocator::deallocate(*(finish.node + 1), buf_siz(BufSiz, sizeof(T)));
			throw;
		}
	}
	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::push_front_aux(const T& value)
	{
		reserve_map_at_front();
		*(start.node - 1) = data_allocator::allocate(buf_siz(BufSiz, sizeof(T)));
		try{
			start.node = start.node - 1;
			start.cur = start.last - 1;
			construct(start.cur, value);
		}
		catch (...)
		{
			start.node = start.node - 1;
			start.cur = start.first;
			data_allocator::deallocate((*start.node - 1),buf_siz(BufSiz,sizeof(T)));
		}
	}


但又面临一个更特殊的情况:中控器的空间是在初始化时以固定大小设置的,也有可能出现中控器空间不足的情况,这时我们需要重新配置中控器空间,并将原来的中控器拷贝过来:

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::reserve_map_at_front(size_type node_to_add = 1)
	{
		if (node_to_add + 1 > map_size - (finish.node - map))//计算剩余空间是否满足需求
			reallocate_map(node_to_add, true);
	}

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::reallocate_map(size_type node_to_add, bool add_to_front)
	{
		size_type old_num_node = finish.node - start.node + 1;
		size_type new_num_node = old_num_node + node_to_add;
		map_pointer new_start;
		if (map_size > 2 * new_num_node)
		{/*如果还有很多备用空间存在,说明由不平衡的前端(后端)插入造成的
		 此时不需要重新分配map,只需调整一下位置*/
			new_start = map + (map_size - new_num_node) / 2 +
				(add_to_front ? node_to_add : 0); //是否由前端插入造成的空间不足?是就多为前端分配空间
												  //使插入以后前后两端的剩余空间一致
			copy(start.node, finish.node+1, new_start);
		}
		else
		{//空间不足,需要重新配置一个更大的map
			size_type new_map_size = map_size + max(map_size, node_to_add)+2;
			map_pointer new_map = map_allocator::allocate(new_map_size);
			new_start = new_map + (new_map_size - new_num_node) / 2 +
				(add_to_front ? node_to_add : 0);
			copy(start.node, finish.node+1, new_start);
			map_allocator::deallocate(map,map_size); 
			map = new_map;
			map_size = new_map_size;
		}
		start.set_node(new_start);
		finish.set_node(new_num_node + new_start - 1);
	}

类似的,在处理头部尾部的移除元素操作时,有可能出现在清除头部/尾部元素后头部/尾部出现空的缓冲区。我们要将这些空的缓冲区清除:

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::pop_front()
	{
		if (!empty())
		{
			if (start.cur != start.last - 1)
			{
				destroy(start.cur);
				++start.cur;
			}
			else
				pop_front_aux();
		}

	}

	template <typename T, typename Alloc, size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::pop_front_aux()
	{
		destroy(start.cur);
		data_allocator::deallocate(start.first,buf_siz(BufSiz,sizeof(T)));
		start.set_node(start.node + 1);
		start.cur = start.first;
	}

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::pop_back()
	{
		if (!empty())
		{
			if (finish.cur != finish.first)
			{
				destroy(finish.cur);
				--finish.cur;
			}
			else
				pop_back_aux();	//最后一个缓冲区没有任何元素
		}
	}

	template <typename T,typename Alloc,size_t BufSiz>
	void stl_deque<T, Alloc, BufSiz>::pop_back_aux()
	{
		data_allocator::deallocate(finish.first, buf_siz(BufSiz, sizeof(T)));
		finish.set_node(finish.node - 1);
		finish.cur = finish.last - 1;
		destroy(finish.cur);
	}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值