介绍
deque是一个双端队列, 是一个顺序容器,允许在开头和结尾快速插入和删除.此外,在deque的两端插入和删除永远不会使指向其余元素的指针或引用失效.
deque的存储会根据需要自动扩展和收缩.扩展deque要比std::vector扩容快速,因为它不涉及将现有元素复制到新的内存位置.
deque常见操作的时间复杂度如下:
- 随机存储 - 常数O(1)
- 在首尾插入或删除元素 - 常数O(1)
- 在其他位置插入或删除元素 - 线性O(n)
这次使用一行一行代码讲解,会比较啰嗦.但是对不熟悉的人来说比较友好一点.也是为了给未来自己复习一下
1. deque的核心原理图
deque的底层要比vector复杂那么一丢丢
文字描述
- 创建队列的时候预先创建一个主控map.它是一个二级指针,每个元素是一个指针类型.在源码中被称为node(也叫缓冲区).在创建主控map的时候,也会创建node(缓冲区),队列实际操作就是这个node(缓冲区)
- 底层几个核心成员变量分别是:
- _M_map: 二级指针,指向主控map
- _M_map_size: 主控map的大小
- _M_start: 队头
- _M_finish: 队尾
iterator的核心成员变量为:
- _M_cur: 指向缓冲区位置,也是当前队头或队尾在缓冲区的位置
- _M_first: 队头、队尾所在缓冲区的起始地址
- _M_last: 队头、队尾所在缓冲区的终止地址
- _M_node: 缓冲区所在主控map的地址
2. deque类图
文字说明
- _Deque_base提供了几个成员函数,用来操作底层的主控map和node.
- _Deque_base内部还有个枚举: S_initial_map_size.定义默认的主控map的大小
- 设置node指向主控map的存储地址由_Deque_iterator提供
3. deque原理剖析
3.1 构造、析构函数
deque提供了如下几个构造、析构函数
因为deque继承自_Deque_base,在deque构造的时候也会调用基类_Deque_base的构造、析构函数.下面是_Deque_base定义的几种构造、析构函数
下面逐个分析每个函数
3.1.1 deque() = default
当我们如下定义deque的时候会调用该构造函数
deque<int> d;
由此调用基类_Deque_base的_Deque_base()版本构造函数,该版本细节如下
_Deque_base()
: _M_impl()
{ _M_initialize_map(0); }
底层调用_M_initialize_map(0) 来初始化主控map和分配缓冲区(node)._M_initialize_map(0)是_Deque_base的成员函数,下面来分析这个函数做了什么. 记住,调用这个函数传入的参数是0
_M_initialize_map(size_t num_elemtns)
这是加上了注释的代码细节
下面一行一行并画个图解释一下这个函数在做什么
第一行调用了__deque_buf_size()函数,通过函数名得知,这个函数用来获取缓冲区的大小
这个函数的参数都是传入deque元素大小.如果元素类型小于 512 的话,那么返回 512 / sizeof(元素类型) ;如果元素类型比512还要大,那么则直接返回 1.
回到_M_initialize_map的第一行来.获取了缓冲区的大小后,根据传入的参数(元素的个数) 除以缓冲区大小,表示需要几个这样的缓冲区,然后再加1,一次性多分配一个缓冲区.毕竟重复分配内存也是挺耗时间的.
第二行则计算主控map的大小.这里用到了一个枚举_S_initial_map_size,这个值为 8.
主控map默认大小为 8.如果你要分配的元素个数比较大,至少需要 7个缓冲区才能装下.那么主控map的大小为缓冲区个数 + 2(多分配两个结点)
第三行分配一个主控map,实际就是一个数组
第四、五行设置两个临时变量,用于指向主控map哪个位置存储的缓冲区是队头和队尾.
第六行在这个范围内创建缓冲区,代码细节如下:
程序执行到这里,就变为了如下图所示的情况
函数的最后几行就是设置队头队尾的指针了,指向合适的位置.这里调用了 _Deque_iterator 的 _M_set_node函数来设置指向的位置
设置完相应的指针后,如下所示这样:
现在队头和队尾都指向同一个位置
3.1.2 deque(size_type n, const allocator_type& a = allocator_type())
我们定义如下deque代码
deque<int> d(10);
deque构造函数代码细节如下:
调用了基类 _Deque_base的 _Deque_base(size_t num_elements) 构造函数,代码细节如下:
和上面一样,只不过这次指定了容器的初始元素个数,细节就不再重新赘述一遍了.
把焦点重新回到deque构造函数的函数体里,这里调用了成员函数 _M_default_initialize().代码细节如下:
这个函数就是将缓冲区初始化为 元素类型 的默认值.
3.1.3 deque(size_type n, const value_type& value, const allocator_type& a = allocator_type())
我们定义如下代码:
deque<int> d(10, 3);
定义一个deque容器,初始元素个数为10,值为3
deque构造函数代码细节如下:
还是调用了基类的构造,和 3.1.2一样,也都调用了基类的 _M_default_initialize(). 先把主控map和缓冲区初始化出来.随后deque构造函数体里调用了 _M_fill_initialize执行初始化.代码细节如下:
3.1.4 deque(initializer_list<value_type> __l, const allocator_type& __a = allocator_type())
定义如下代码:
deque<int> d({1, 2, 3});
初始化列表方式来定义一个deque容器
构造函数代码细节如下:
这个版本的构造函数调用了基类的allocator_type&版本构造函数
基类只是初始化了成员而已,没有创建主控map和缓冲区,那么这个工作就应该是由deque构造函数体的 _M_range_initialize 来做
注意,调用 _M_range_initialize的第三个参数传入的是 random_access_iterator_tag,这个是迭代器类别,表示可以随机访问.
random_access_iterator_tag细节定义在 bits/stl_iterator_base_types.h 头文件中
_M_range_initialize一共有两个版本:
所以两个版本,第二个版本更加匹配.
3.1.5 deque(_InputIterator first, InputIterator last, const allocator_type& a = allocator_type())
3.2 修饰符
3.2.1 clear
3.2.2 insert
版本1iterator insert(const_iterator __position, const value_type& __x)
版本2 iterator insert(const_iterator __position, value_type&& __x)
版本3 iterator insert(const_iterator __p, initializer_list<value_type> __l)
版本4 iterator insert(const_iterator __position, size_type __n, const value_type& __x)
3.2.3 emplace
3.2.4 erase
版本1 iterator erase(const_iterator __position)
版本2 iterator erase(const_iterator __first, const_iterator __last)
3.2.5 push_back
版本1 void push_back(const value_type& __x)
版本2 void push_back(value_type&& __x)
3.2.6 emplace_back
3.2.7 pop_back
3.2.8 push_front
版本1 void push_front(const value_type& __x)
版本2 void push_front(value_type&& __x)
3.2.9 emplace_front
3.2.10 pop_front
3.2.11 resize
版本1 void resize(size_type __new_size)
版本2 void resize(size_type __new_size, const value_type& __x)