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 double−ended 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 最大差异:
-
在于 d e q u e deque deque 允许于常数时间 O ( 1 ) O(1) O(1) 内对其头端进行元素的插入或删除操作。
-
在于 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 虽可变长,却只能向尾端变长,而且其所谓变长是个假象,事实上是:
-
另觅更大的空间。
-
将原数据复制过去。
-
释放原空间。
扩容三部曲。如果不是 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 迭代器应该具备什么结构:
-
首先,它必须能够指出分段连续空间(即缓冲区)在哪里。
-
其次,它必须能够判断自己是够已经处于所在缓冲区的边缘,如果是,一旦前进或后退时就必须跳跃到下一个或上一个缓冲区。
为了能够正确跳跃, 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;
:
-
cur
:指向遍历容器时遍历到的元素的下一个位置。 -
first
:指向 n o d e node node 结点指向的 b u f f buff buff 数组的第一个元素的位置。 -
last
:指向 n o d e node node 结点指向的 b u f f buff buff 数组的最后一个元素的位置。 -
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
deque(deque<int, alloc, 8>
)。经过某些操作后,
d
e
q
u
e
deque
deque 拥有 20
个元素,那么其 begin()
和 end()
所传回的两个迭代器应该如下图所示:(这两个迭代器事实上一直保持在
d
e
q
u
e
deque
deque 内,名为 start
和 finish
)
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]):
-
用 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 数组的大小。
-
用 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]
【总结】
-
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 略逊一筹。
-
中间插入删除效率很低,需要挪动数据,是 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
的底层容器,比如vector
和list
都可以;
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 作为其底层容器,主要是因为:
-
stack
和queue
不需要遍历(因此stack
和queue
没有迭代器),只需要在固定的一端或者两端进行操作(恰好符合双端队列在两端操作效率高的优点)。 -
在
stack
中元素增长时,deque
比vector
的效率高(扩容时不需要搬移大量数据);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 作为容器来使用。