【STL】deque(了解)

d e q u e deque deque d o u b l e − e n d e d   q u e u e double-ended\ queue doubleended queue双端队列是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出和插入,也支持下标的随机访问


一、deque 的介绍

d e q u e deque deque(双端队列):是一种双开口的"连续"空间的数据结构。双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为 O ( 1 ) O(1) O(1),与 v e c t o r vector vector 比较,头插效率高,不需要搬移元素;与 l i s t list list 比较,空间利用率比较高

1. deque 概述

v e c t o r vector vector 是单向开口的连续线性空间, d e q u e deque deque 则是双向开口的连续线性空间。所谓双向开口,即可以在头尾两端分别做元素的插入和删除操作,如下图所示。( v e c t o r vector vector 当然也可以在头尾两端进行插入删除等操作(从技术角度),但是其头部操作效率极差(需要挪动数据),无法被接受)

在这里插入图片描述

d e q u e deque deque v e c t o r vector vector 最大差异

  1. 在于 d e q u e deque deque 允许于常数时间 O ( 1 ) O(1) O(1)对其头端进行元素的插入或删除操作

  2. 在于 d e q u e deque deque 没有所谓容量( c a p a c i t y capacity capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并连接起来

也就是说,像 v e c t o r vector vector 那样"因旧空间不足而配置一块更大的空间,然后复制元素,再释放旧空间"(空间不够就不断扩容)这样的事情在 d e q u e deque deque 是不会发生的。因此, d e q u e deque deque 没有必要提供所谓的空间保留 r e s e r v e reserve reserve 功能。

虽然 d e q u e deque deque 也提供了随机访问迭代器 R a m d o n   A c c e s s   I t e r a t o r Ramdon\ Access\ Iterator Ramdon Access Iterator),但它的迭代器并不是普通指针,其复杂程度让 v e c t o r vector vector 简直不能和其相提并论,这当然影响了各个运算层面,如:sort 算法:

void test1()
{
	//利用时间戳生成一百万个随机数进行排序比较效率
	srand(time(0));
	const int N = 1000000;
	
	vector<int> v;
	deque<int> dq;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand() + i;
		v.push_back(e);
		dq.push_back(e);
	}

	//使用vector进行排序的时间
	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();
	
	//使用deque进行排序的时间
	int begin2 = clock();
	sort(dq.begin(), dq.end());
	int end2 = clock();
	
	cout << "vector sort:" << end1 - begin1 << "  ms" << endl;
	cout << "deque  sort:" << end2 - begin2 << " ms" << endl;
}

在这里插入图片描述

由于 s o r t sort sort 算法需要用到随机访问迭代器,这一点 v e c t o r vector vector d e q u e deque deque 都支持,但是使用相同的排序算法 s o r t sort sort d e q u e deque deque 排序的时间比 v e c t o r vector vector 排序的时间整整慢了将近五、六倍左右!

因此,我们通常对 d e q u e deque deque 进行排序操作,为了最高的效率,会 d e q u e deque deque 先完整复制到一个 v e c t o r vector vector 身上,将 v e c t o r vector vector 排序后再复制回 d e q u e deque deque

void test2()
{
	//利用时间戳生成一百万个随机数进行排序比较效率
	srand(time(0));
	const int N = 1000000;
	
	deque<int> dq;
	
	for (int i = 0; i < N; ++i)
	{
		auto e = rand() + i;
		dq.push_back(e);
	}
	
	// 在deque中排序的时间
	int begin1 = clock();
	sort(dq.begin(), dq.end());
	int end1 = clock();
	
	// 拷贝到vector
	vector<int> v(dq.begin(), dq.end());
	
	int begin2 = clock();
	// 在vector中排序
	sort(v.begin(), v.end());
	
	// 拷贝回deque
	dq.assign(v.begin(), v.end());
	int end2 = clock();
	
	cout << "deque sort:" << end1 - begin1 << " ms" << endl;
	cout << "copy  sort:" << end2 - begin2 << "  ms" << endl;
}

在这里插入图片描述

可见,即使是需要拷贝数据再返回,也比直接用 d e q u e deque deque 排序效率高很多

结论:因此,除非必要,我们应尽可能选择使用 v e c t o r vector vector 而非 d e q u e deque deque

2. deque 的中控器

d e q u e deque deque连续空间(至少逻辑上如此),连续线性空间总令我们联想到 a r r a y array array v e c t o r vector vector a r r a y array array 无法变长, v e c t o r vector vector 虽可变长,却只能向尾端变长,而且其所谓变长是个假象,事实上是:

  1. 另觅更大的空间

  2. 将原数据复制过去

  3. 释放原空间

扩容三部曲。如果不是 v e c t o r vector vector 每次配置新空间时都有留下一些余裕,其变长假象所带来的代价将是相当高昂的。

d e q u e deque deque 是由一段一段的定量连续空间构成。一旦有必要在 d e q u e deque deque 的前端或尾端增加新空间,便配置一些定量连续空间串联在整个 d e q u e deque deque 的头端或尾端, d e q u e deque deque 的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口,避开了"重新配置、复制、释放"的轮回,代价则是复杂迭代器架构

受到分段连续线性空间的字面影响,我们可能以为 d e q u e deque deque 的实现复杂度和 v e c t o r vector vector 相比虽然不一样但也差不多,实则不然。主要因为,既然有分段连续线性空间,就必须有中央控制,而为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都不得不设计的非常繁琐。因此, d e q u e deque deque 的实现代码远比 v e c t o r vector vector l i s t list list 加起来还多得多。

template<class T, class Alloc = alloc, size_t BuffSize = 0>
class deque
{
public:							// Basic types
	typedef T value_type;
	typedef value_type* pointer;
	···
protected:						// Internal typedefs
	// 元素的指针的指针(pointer of pointer of T)
	typedef pointer* map_pointer;

protected:						// Data members
	map_pointer map;	// 指向 map,map 是块连续空间,其内的每个元素
						// 都是一个指针(称为结点),指向一块缓冲区
	size_type map_size;	// map 内可容纳多少指针
};

d e q u e deque deque 采用一块所谓的 m a p map map(注意:不是 S T L STL STL m a p map map 容器)作为主控。这里所谓 m a p map map一小块连续空间。其中每个元素(此处称为一个结点, n o d e node node都是指针,指向另一端(较大的)连续线性空间,称为缓冲区 b u f f buff buff)。缓冲区才是 d e q u e deque deque 的储存空间主体。 S G I SGI SGI S T L STL STL 允许我们指定缓冲区大小,默认值 0 0 0 表示将使用 512   b y t e s 512\ bytes 512 bytes 缓冲区)

把各种类型定义整理一下,我们便可发现, m a p map map 其实是一个 T ∗ ∗ T^*{^*} T,也就它是一个指针,所指的内容又是一个指针,指向类型为 T T T 的一块空间,即缓冲区( b u f f buff buff

在这里插入图片描述

【总结】 d e q u e deque deque 并不是真正连续的空间,而是由一段段连续的小空间(缓冲区)由 m a p map map 容器作为主控拼接而成的,实际 d e q u e deque deque 类似于一个动态的二维数组,上图其实就是其底层结构

3. deque 的迭代器

d e q u e deque deque分段连续空间。维持其"整体连续"假象的任务,落在了迭代器operator++operator-- l两个运算子身上。

让我们思考一下, d e q u e deque deque 迭代器应该具备什么结构:

  1. 首先,它必须能够指出分段连续空间(即缓冲区)在哪里。

  2. 其次,它必须能够判断自己是够已经处于所在缓冲区的边缘,如果是,一旦前进或后退时就必须跳跃到下一个或上一个缓冲区。

为了能够正确跳跃, d e q u e deque deque 必须随时掌握管控中心( m a p map map)。下面这种实现方式符合要求:

template<class T, class Ref, class Ptr, size_t BuffSize>
struct __deque_iterator	//未继承 std::iterator
{
	typedef __deque_iterator<T, T&, T*, BuffSize> 				iterator;
	typedef __deque_iterator<T, const T&, const T*, BuffSize>	const_iterator;
	static size_t buffer_size()
	{
		return __deque_buf_size(BuffSize, sizeof(T));
	}
	
	// 未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应类型
	typedef random_access_iterator_tag 	iterator_category;	// (1)
	typedef T 							value_type;			// (2)
	typedef Ptr 						pointer;			// (3)
	typedef Ref 						reference;			// (4)
	typedef size_t 						size_type;
	typedef ptrdiff_t 					difference_type;	// (5)
	typedef T** 						map_pointer;
	
	typedef __deque_iterator 			self;

	// 保持与容器的联结
	T* cur;				// 此迭代器所指:缓冲区中的现行(current)元素
	T* first;			// 此迭代器所指:缓冲区的头
	T* last;			// 此迭代器所指:缓冲区的尾(含备用空间)
	map_pointer node;	// 指向管控中心
};

其中用来决定缓冲区大小的函数 buffer_size(),调用 __deque_buf_size(),后者是一个全局函数,定义如下:

// 1.如果 n 不为 0,传回 n,表示 buffer size 由用户自定义
// 2.如果 n 为 0,表示 buffer size 使用默认值,那么:
// 	(1)如果 sz(元素大小,sizeof(value_type))小于 512,传回 512/sz
// 	(2)如果 sz 不小于512,传回 1

inline size_t __deque_buf_size(size_t n, size_t, sz)
{
	return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
}

总结一下, d e q u e deque deque中控器缓冲区迭代器相互关系如下图所示:

在这里插入图片描述

其中,迭代器的核心结构就是这四个指针:T* cur;T* first;T* last;T** node;

  1. cur:指向遍历容器时遍历到的元素的下一个位置。

  2. first:指向 n o d e node node 结点指向的 b u f f buff buff 数组的第一个元素的位置。

  3. last:指向 n o d e node node 结点指向的 b u f f buff buff 数组的最后一个元素的位置。

  4. node:指向 m a p map map 中控器中第几个 b u f f buff buff 数组(结点 n o d e node node)。

假设现在我们产生一个元素类型为 int,缓冲区大小为 8(个元素)的 d e q u e deque dequedeque<int, alloc, 8>)。经过某些操作后, d e q u e deque deque 拥有 20 个元素,那么其 begin()end() 所传回的两个迭代器应该如下图所示:(这两个迭代器事实上一直保持在 d e q u e deque deque 内,名为 startfinish

在这里插入图片描述

20 个元素需要 20 / 8 = 3 个缓冲区,所以 m a p map map 之内运用了三个结点。迭代器 start 内的 cur 指针当然指向缓冲区的第一个元素,迭代器 finish 内的 cur 指针当然指向缓冲区的最后一个元素(的下一个位置)。

注意:最后一个缓冲区尚有备用空间。稍后如果有新元素要插入尾端,可直接拿此备用空间来使用。

通过以上介绍,我们已经大体了解了 d e q u e deque deque底层结构,因此就可以分析出, d e q u e deque deque 如果想要用下标方括号来访问其元素所在位置,要满足这样一个运算(假设查找 d q [ i ] dq[i] dq[i]):

  1. x x x 来表示下标 i i i 在中控器 m a p map map 中的第几个结点中: x   =   i   /   N x\ =\ i\ /\ N x = i / N 其中, N N N 为缓冲区 b u f f buff buff 数组的大小。

  2. y y y 来表示下标 i i i n o d e node node 结点(第 x x x 个结点)的第几个位置: y   =   i   %   N y\ =\ i\ \%\ N y = i % N其中, N N N 为缓冲区 b u f f buff buff 数组的大小。

由于其缓冲区 b u f f buff buff 数组都是定长的(长度为 N N N),所以我们这样就能像,堆这个数据结构寻找其各个子孙结点一样,通过下标运算,找到其元素具体位置,也就是做到了能够通过下标访问元素 n o d e [ x ] [ y ]   =   d q [ i ] node[x][y]\ =\ dq[i] node[x][y] = dq[i]

【总结】

  1. d e q u e deque deque 头插尾插效率很高,更甚于 v e c t o r vector vector l i s t list list

  2. 下标随机访问效率也不错,相比 v e c t o r vector vector 略逊一筹。

  3. 中间插入删除效率很低,需要挪动数据,是 O ( N ) O(N) O(N)


二、deque 的缺陷

通过上面的介绍我们发现, d e q u e deque deque 简直是 v e c t o r vector vector l i s t list list缝合怪:和 v e c t o r vector vector 同样是底层采用连续空间存储,但其其头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素;又像 l i s t list list 一样,将每一个缓冲区 b u f f buff buff 数组看作一个大结点,然后像 l i s t list list 的每个结点通过指针连接一样, d e q u e deque deque 通过中控器 m a p map map 将其连接起来,但由于其连接的是一块块连续的空间,因此空间利用率高

这么说 d e q u e deque deque 简直是集百家之长,那为什么平时在使用中很少见到 d e q u e deque deque 直接使用呢?难道说仅仅是因为其结构复杂而不用它吗?

d e q u e deque deque 一定是有一个致命缺陷:不适合遍历

因为在遍历时, d e q u e deque deque迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑 v e c t o r vector vector l i s t list list d e q u e deque deque 的应用并不多,而目前能看到的一个应用就是:

S T L STL STL 用其作为 s t a c k stack stack q u e u e queue queue 的底层数据结构


三、deque 作为 stack 和 queue 的底层默认容器的原因

stack 是一种后进先出 L I F O LIFO LIFO)的特殊线性数据结构,因此只要具有 push_back()pop_back()
操作的线性结构,都可以作为 stack 的底层容器,比如 vectorlist 都可以;

queue 是一种先进先出 F I F O FIFO FIFO)的特殊线性数据结构,只要具有 push_back()pop_front()
操作的线性结构,都可以作为 queue 的底层容器,比如 list

但是 S T L STL STL 中对 s t a c k stack stack q u e u e queue queue 默认选择 d e q u e deque deque 作为其底层容器,主要是因为:

  1. stackqueue 不需要遍历(因此 stackqueue 没有迭代器),只需要在固定的一端或者两端进行操作(恰好符合双端队列在两端操作效率高的优点)。

  2. stack 中元素增长时,dequevector效率高(扩容时不需要搬移大量数据);queue 中的元素增长时,deque 不仅效率高,而且内存使用率高

因此,其需求结合了 d e q u e deque deque 的优点,而完美的避开了其缺陷。


总结

d e q u e deque deque 可以说是集合了 v e c t o r vector vector l i s t list list 的各种特点,搞得结构相比 v e c t o r vector vector l i s t list list 复杂了非常多,但还好我们使用的是其接口,复杂的结构标准库里都帮我们实现好了且进行了封装。

但其底层结构本身决定了应用场景不如 v e c t o r vector vector l i s t list list 广泛,但是在特殊的地方有着非常好的效率,如:作为 s t a c k stack stack q u e u e queue queue 的底层容器。

因此,这部分内容仅供了解即可,平时基本上不会使用 d e q u e deque deque 作为容器来使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值