1、简介

双端队列deque,与vector的最大差异在于:

一、deque运行常数时间对头端或尾端进行元素的插入和删除操作。

二、deque没有所谓的容器概念,因为它是动态地以分段连续空间组合而成随时可以增加一块新的内存空间并拼接起来。

虽然deque也提供随机访问的迭代器,但它的迭代器与list和vector的不一样,其设计相当复杂而精妙。因此,会对各种运算产生一定影响,厨房必要,尽可能的选择使用vetor而非deque。队列示意图如下图所示:

2、deque的中控器

deque在逻辑上看起来是连续的空间,内部确实是一段一段的定量连续空间构成。

一旦有必要在deque的前端或尾端增加新空间,deque会配置一段定量的连续空间,串联在整个deque的头部或尾部。

deque的设计大师最大的调整应该就是如何在这段分段的定量连续空间上还能维护其整体连续的假象,并提供其随机存取的接口,从而避开了像vector那样的“重新配置-复制-释放”开销三部曲。———这样一来,虽然开销降低,却提高了复杂的迭代器架构。

因此,deque数据结构的设计和迭代前进或后退等操作都非常复杂。

deque采用一块所谓的map(注意,不是stl里面的map容器)作为中控器,其实就是一小块连续空间,其中的每一个元素都是指针,指向另外一段较大的连续线性空间,称之为缓冲区。在后面我们将看到,缓冲区才是deque的存储空间主体。

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
typedef pointer** map_pointer;
map_pointer map;//指向 map,map 是连续空间,其内的每个元素都是一个指针。
size_type map_size;
...
};

deque的结构设计中,map和node-buffer的关系如下:

3、deque的迭代器

deque是分段连续空间,维持其“整体连续”假象的任务,就靠它的迭代器来实现,也就是operator++和operator--两个算子上面。在开发者看来,设计deque迭代器应该具备如下三个特征的结构和功能:

一、既然deque存储空间是分段的连续空间,迭代器应该能够指出当前的连续空间在哪里。

二、因为缓冲区有边界,迭代器还应该能判断当前是否处于缓冲区的边缘,如果是,一旦前进或后退,就必须跳转到下一个或上一个缓冲区。

三、也就是实现前面两种情况的前提,迭代器必须随时控制中控器。

有了这些分析之后,在分析源码时,就显得容易理解了。

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
typedef pointer** map_pointer;
map_pointer map;//指向 map,map 是连续空间,其内的每个元素都是一个指针。
size_type map_size;
...
};

deque的每一个缓冲区设计了三个迭代器:

struct __deque_iterator {
...
typedef T value_type;
T* cur;
T* first;
T* last;
typedef T** map_pointer;
map_pointer node;
...
};

为什么这么设计呢?因为deque是分段连续的空间,下图描绘了deque的中控器、缓冲区、迭代器之间的相互关系:

map的每一段都指向一个缓冲区buffre,而缓冲区是需要知道每个元素的位置的,所以需要这些迭代器去访问。其中:

  • cur表示当前所指向的位置;
  • first表示当前数组中头的位置;
  • last表示当前数组中尾的位置。

这样设计显然是为了方便管理,需要注意的是deque的空间由map管理的。它是一个指向指针的指针。所以三个参数都是指向当前的数组,但这样的数组可能有多个,只是每个数组都管理这3个变量。

最后,deque缓冲区的大小由一个全局函数来决定:

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));
}
//如果 n 不为0,则返回 n,表示缓冲区大小由用户自定义
//如果 n == 0,表示 缓冲区大小默认值
//如果 sz = (元素大小 sizeof(value_type)) 小于 512 则返回 521/sz
//如果 sz 不小于 512 则返回 1

用例分析:

假设现在构造一个int类型的deque,设置缓冲区大小等于32,这样一来,每个缓冲区可以容纳32/sizeof(int)=8个元素。经过一番操作之后,deuqe现在有20个元素来,那么成员函数begin()和end()返回的两个迭代器应该是什么样的呢?如下图所示:

20个元素需要20/8≈3个缓冲区。

所以map运用的三个节点,迭代器start内的cur指针指向缓冲区的第一个元素,迭代器finish内的cur指针指向缓冲区的最后一个元素(的下一个位置)。

注意:最后一个缓冲区尚有备用空间,如果之后还有新元素插入,则直接插入到备用空间。

4、迭代器的操作

迭代器操作包括两种:前进和后退。

operator++操作代表需要切换到下一个元素,这里需要先切后再判断是否已经到达缓冲区到末尾。

self& operator++() {
++cur; //切换至下一个元素
if (cur == last) { //如果已经到达所在缓冲区的末尾
set_node(node+1); //切换下一个节点
cur = first;
}
return *this;
}

operator--操作代表切换到上一个元素所在的位置,需要先判断是否到达缓冲区的头部再后退。

self& operator--() {
if (cur == first) { //如果已经到达所在缓冲区的头部
set_node(node - 1); //切换前一个节点的最后一个元素
cur = last;
}
--cur; //切换前一个元素
return *this;
} //结合前面的分段连续空间,你在想一想这样的设计是不是合理呢?

5、deque的构造和析构函数

deque的构造函数有多个重载函数,接受大部分不同的参数类型,基本上每一个构造函数都会调用create_map_and_nodes,这就是构造函数的核心,后面我们来分析这个函数的实现。

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
public: // Basic types
deque() : start(), finish(), map(0), map_size(0){
create_map_and_nodes(0);
} // 默认构造函数
deque(const deque& x) : start(), finish(), map(0), map_size(0) {
create_map_and_nodes(x.size());
__STL_TRY {
uninitialized_copy(x.begin(), x.end(), start);
}
__STL_UNWIND(destroy_map_and_nodes());
}
// 接受 n:初始化大小, value:初始化的值
deque(size_type n, const value_type& value) : start(), finish(), map(0), map_size(0) {
fill_initialize(n, value);
}
deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {
fill_initialize(n, value);
}
deque(long n, const value_type& value) : start(), finish(), map(0), map_size(0){
fill_initialize(n, value);
}
...

下面我们来看下deque的中控器如配置:

void deque<T,Alloc,BufSize>::create_map_and_nodes(size_type_num_elements) {
//需要节点数= (每个元素/每个缓冲区可容纳的元素个数+1)
//如果刚好整除,多配一个节点
size_type num_nodes = num_elements / buffer_size() + 1;
//一个 map 要管理几个节点,最少 8 个,最多是需要节点数+2
map_size = max(initial_map_size(), num_nodes + 2);
map = map_allocator::allocate(map_size);
// 计算出数组的头前面留出来的位置保存并在nstart.
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
map_pointer cur;//指向所拥有的节点的最中央位置
...
}

注意,分析源码之后发现:deque的begin和end不是一开始就指向map中控器的开通和结尾的,而是指向所拥有的节点的最中央位置。

这样带来的好处是可以使得头尾两边扩充的可能性一样大,换句话说,因为deque是头尾插入都是O(1),所以deque在头和尾都留有空间方便头尾插入。

那么,什么时候map中控器本身需要调整大小呢?触发条件在于reserve_map_at_back和reserve_map_at_font这两个函数来判断,实际操作由reallocate_map来执行。

// 如果 map 尾端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_back (size_type nodes_to_add = 1) {
if (nodes_to_add + 1 > map_size - (finish.node - map))
reallocate_map(nodes_to_add, false);
}
// 如果 map 前端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
if (nodes_to_add > start.node - map)
reallocate_map(nodes_to_add, true);
}

6、deque的插入元素和删除元素

因为deque是能够双向操作,所以其push和pop操作都类似于list,都可以直接有对应的操作,需要注意的是list是链表,并不会涉及到界线的判断,而deque是由数组来存储的,所以需要随时对界限进行判断。

push的实现:

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
public: // push_* and pop_*
// 对尾进行插入
// 判断函数是否达到了数组尾部. 没有达到就直接进行插入
void push_back(const value_type& t) {
if (finish.cur != finish.last - 1) {
construct(finish.cur, t);
++finish.cur;
}
else
push_back_aux(t);
}
// 对头进行插入
// 判断函数是否达到了数组头部. 没有达到就直接进行插入
void push_front(const value_type& t) {
if (start.cur != start.first) {
construct(start.cur - 1, t);
--start.cur;
}
else
push_front_aux(t);
}
...
};

pop的实现:

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
public:
// 对尾部进行操作
// 判断是否达到数组的头部. 没有到达就直接释放
void pop_back() {
if (finish.cur != finish.first) {
--finish.cur;
destroy(finish.cur);
}
else
pop_back_aux();
}
// 对头部进行操作
// 判断是否达到数组的尾部. 没有到达就直接释放
void pop_front() {
if (start.cur != start.last - 1) {
destroy(start.cur);
++start.cur;
}
else
pop_front_aux();
}
...
};

pop和push都先调用了reserve_map_at_XX函数,这些函数主要为了判断前后空间是否足够。

删除操作

构造函数都会调用create_map_and_nodes函数,考虑到deque实现前后插入时间复杂度为O(1),保证了在前后留出了空间,所以push和pop都可以在前面的数组进行操作。

现在来分析erase,因为deque是由数组构成,所以地址空间是连续的,删除也就像vector一样,需要移动所有的元素。

deque为了保证效率尽可能高,就判断删除的位置上中间偏后还是中间偏前来进行移动。

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
public: // erase
iterator erase(iterator pos) {
iterator next = pos;
++next;
difference_type index = pos - start;
// 删除的地方是中间偏前, 移动前面的元素
if (index < (size() >> 1)) {
copy_backward(start, pos, next);
pop_front();
}
// 删除的地方是中间偏后, 移动后面的元素
else {
copy(next, finish, pos);
pop_back();
}
return start + index;
}
// 范围删除, 实际也是调用上面的erase函数.
iterator erase(iterator first, iterator last);
void clear();
...
};

最后说一下insert函数。

deque源码,基本每一个insert重载函数都会调用insert_auto判断插入的位置离头还是尾比较近。

如果离头近,则先将头往前移动,调整将要移动的距离,用copy进行调整。

如果离尾近,则将往前移动,调整将要移动的距离,用copy进行调整。

注意:

push_back则先执行构造再移动node,而push_front是先移动node再进行构造,实现的差异主要是finish是指向最后一个元素的后一个地址,而first指向的是第一个元素的地址,下面pop也是一样的。

deque源码里还有一些其它的成员函数:

reallocate_map:判断中考的容量是否够用,如果不够用,申请更大的空间,拷贝元素过去,修改map和start,finish的指向。

fill_initialize:申请空间,对每个空间进行初始化,最后一个数组单独处理。毕竟最后一个数组一般不会全部填满。

clear:删除所有的元素,分两步执行:

首先,从第二个数组开始到倒数第二个数组一次性全部删除,这样做是考虑到中间的数组肯定都是满的,前后两个数组则不一定是满的,最后删除前后两个数组元素。

deque的swap操作:只是交换了start,finish,map,并没有交换所有的元素。

resize:重新将deque进行调整,实现方式与list一样。

析构函数:分步释放内存。

7、deque总结

deque实际上是在功能上合并了vector和list。

优点:

  • 随机访问方便,即支持[]操作和vector.at();
  • 在内部方便的进行插入和删除操作;
  • 可在两端进行push、pop。

缺点:

  • 因为涉及数据结构的维护比较复杂,采用分段连续空间,所以占有内存相对多。

使用区别:

  • 如果需要高效的随机存储,而不在乎插入和删除的效率,则使用vector。
  • 如果需要大量的插入和删除,而不关心随机存取,则应使用list。
  • 如果需要随机存取,且关心亮度数据的插入和删除,则应使用deque。

deque概述的更多相关文章

  1. deque源码1(deque概述、deque中的控制器)

    deque源码1(deque概述.deque中的控制器) deque源码2(deque迭代器.deque的数据结构) deque源码3(deque的构造与内存.ctor.push_back.push_ ...

  2. STL deque详解

    英文原文:http://www.codeproject.com/Articles/5425/An-In-Depth-Study-of-the-STL-Deque-Container 绪言 这篇文章深入 ...

  3. 带你深入理解STL之Deque容器

    在介绍STL的deque的容器之前,我们先来总结一下vector和list的优缺点.vector在内存中是分配一段连续的内存空间进行存储,其迭代器采用原生指针即可,因此其支持随机访问和存储,支持下标操 ...

  4. deque源码4(deque元素操作:pop_back、pop_front、clear、erase、insert)

    deque源码1(deque概述.deque中的控制器) deque源码2(deque迭代器.deque的数据结构) deque源码3(deque的构造与内存.ctor.push_back.push_ ...

  5. deque源码3(deque的构造与内存、ctor、push_back、push_front)

    deque源码1(deque概述.deque中的控制器) deque源码2(deque迭代器.deque的数据结构) deque源码3(deque的构造与内存.ctor.push_back.push_ ...

  6. deque源码2(deque迭代器、deque的数据结构)

    deque源码1(deque概述.deque中的控制器) deque源码2(deque迭代器.deque的数据结构) deque源码3(deque的构造与内存.ctor.push_back.push_ ...

  7. Deque(队列)

    目录 Deque 概述 特点 常用方法 双向队列操作 ArrayDeque Deque 概述 一个线性 collection,支持在两端插入和移除元素.名称 deque 是"double e ...

  8. 【c++】标准模板库STL入门简介与常见用法

    一.STL简介 1.什么是STL STL(Standard Template Library)标准模板库,主要由容器.迭代器.算法.函数对象.内存分配器和适配器六大部分组成.STL已是标准C++的一部 ...

  9. STL源码剖析读书笔记--第四章--序列式容器

    1.什么是序列式容器?什么是关联式容器? 书上给出的解释是,序列式容器中的元素是可序的(可理解为可以按序索引,不管这个索引是像数组一样的随机索引,还是像链表一样的顺序索引),但是元素值在索引顺序的方向 ...

随机推荐

  1. 【操作系统】Linux bash常用函数路径配置

    临时方法:export PATH=/usr/bin:/usr/sbin:/bin:/sbin长期方法:1.    vi /etc/profile2.    在最后插入并保存:    export PA ...

  2. Boosting Adversarial Training with Hypersphere Embedding

    目录 概 主要内容 代码 Pang T., Yang X., Dong Y., Xu K., Su H., Zhu J. Boosting Adversarial Training with Hype ...

  3. 编写Java程序,定义士兵类(Soldiers)并初始化5个士兵对象。

    返回本章节 返回作业目录 需求说明: 创建士兵类(Soldiers),定义有一个String类型参数name,代表士兵的姓名,两个int类型变量x和y,分别表示士兵所在的坐标位置,x代表横坐标,y代表 ...

  4. 【MySQL作业】SELECT 数据查询——美和易思MySQL运算符应用习题

    点击打开所使用到的数据库>>> 1.查询指定姓名的客户(如"张晓静")的地址和电话号码. select address 地址, phone 电话号码 from c ...

  5. 关于 Spring-WebFlux 的一些想法

    本文是本人在知乎提问 spring webflux现在看来是否成功? 下的回答,其他回答也很精彩,如果感兴趣可以查看 现在基于 spring web 的同步微服务有一个非常大的缺陷就是:相对于基于 s ...

  6. Ranger架构剖析

    Ranger介绍 2016年,Hadoop迎来了自己十周岁生日.过去的十年,Hadoop雄霸武林盟主之位,号令天下,引领大数据技术生态不断发展壮大,一时间百家争鸣,百花齐放.然而,兄弟多了不好管,为了 ...

  7. Linux登录时,下游回显非常慢

    问题现象 登录linux时,远程连接正常,[root@...]回显非常慢,在执行脚本时,很容易导致命令下发错乱 原因分析 家目录下的.bash_history文件太大,导致每次登陆时读取这个文件耗时太 ...

  8. vim - 显示不可见字符(:set list)

    默认情况下,vim是不会显示space,tabs,newlines,trailing space,wrapped lines等不可见字符的.我们可以使用以下命令打开list选项,来显示非可见字符: : ...

  9. spring security 登出操作 详细说明

    1.前言 这里专门 做 spring security 登出操作 的  详细记录 2.操作 (1)目录结构 (2)在security 拦截规则配置文件添加退出登录支持 源码 package com.e ...

  10. Hackurllib

    是的大部分的http请求库都不够hacking 不过有w8ay师傅的hack-requests 但是我想造一个属于自己的轮子它将会足够简单足够hacking 用这个名字是因为我选择了urllib做为最 ...