STL-deque

简介

deque,底层是双端队列,他要求

  1. 在开头或末尾插入、删除元素的时间复杂度为 O(1). 这是它和 vector 的主要区别
  2. 随机访问的时间复杂度为 O(1). 这是它和 list 的主要区别

deque是序列式容器

类模板

1
2
3
4
template<
class T, //数据类型
class Allocator = std::allocator<T> //分配器
> class deque;

成员函数

迭代器相关

  1. begin() - 返回指向 deque 第一个元素的迭代器
  2. end() - 返回指向 deque 中最后一个元素之后的理论元素的迭代器
  3. rbegin() - 返回指向 deque 中最后一个元素的反向迭代器(反向开始)
  4. rend() – 返回一个反向迭代器,指向 deque 中第一个元素之前的理论元素
  5. cbegin() - 返回指向 deque 中第一个元素的const迭代器 (C++11)
  6. cend() - 返回一个const迭代器,指向 deque 中最后一个元素之后的理论元素 (C++11)
  7. crbegin() – 返回指向 deque 中最后一个元素的const反向迭代器(反向开始) (C++11)
  8. crend() - 返回指向 deque 中第一个元素之前的理论元素的const反向迭代器 (C++11)

容量相关

  1. size() - 返回 deque 中元素数 O(1)
  2. max_size() - 返回 deque 可以容纳的最大元素数 O(1)
  3. resize() - 调整 deque 可存储数量
  4. empty() - 当前 deque 是否为空 O(1)
  5. shrink_to_fit() - 减少 deque 的容量,将占用内存大小减少到与size()相等 (C++11)

元素访问相关

  1. [g] - 返回对 deque 位置 g 处的引用 O(1)
  2. at(g) - 返回对 deque 位置 g 处的引用,同时进行越界检查 O(1)
  3. front() - 返回对 deque 中第一个元素的引用 O(1)
  4. back() - 返回对 deque 中最后一个元素的引用 O(1)
  5. data() - 返回 deque 底层数组的指针 O(1)

修改器相关

  1. assgin() - 用新的容器替换此容器
  2. clear() - 清空所有元素,size()置 0
  3. insert() - 插入一个或多个元素到指定位置
  4. erase() - 删除指定位置元素
  5. push_front() - 头部插入元素 O(1)
  6. pop_front() - 删除头部元素 O(1)
  7. push_back() - 尾部插入元素 O(1)
  8. pop_back() - 删除尾部元素 O(1)
  9. swap() - 交换元素
  10. emplace() - 插入一个元素到指定位置,调用构造函数 (C++11)
  11. emplace_front() - 插入一个元素到头部,调用构造函数 (C++11)
  12. emplace_back() - 插入一个元素到尾部,调用构造函数 (C++11)

deque迭代器详解

deque 的所有迭代器都是随机访问迭代器

1. 正向迭代器

此迭代器是由 begin(), end() 所返回的,其对其的操作为正向,且当 deque 元素不为 const 时可以更改其指向的值,即:

1
2
3
4
deque<int> v{ 1,2,3,4,5,6 };
auto i = v.begin(); //*i为1
i += 2; //*i为3
*i = 100; //*i为100

2. 反向迭代器

此迭代器由 rbegin(), rend() 所返回,对其的操作为反向,且也满足当 deque 元素不为 const时可以更改其指向的值,即:

1
2
3
4
deque<int> v{ 1,2,3,4,5,6 };
auto i = v.rbegin();//*i为6
i += 2; //*i为4
*i = 100; //*i为100

3. const迭代器

此迭代器由 cbegin(), cend(), crbegin(), crend() 返回,只能对其操作,不能更改其指向的值,即:

1
2
3
4
deque<int> v{ 1,2,3,4,5,6 };
auto i = v.cbegin();//*i为1
i += 2; //*i为3
*i = 100; //error!

deque的特性

  1. 从头部或尾部添加元素,现有元素的内存地址不会改变,因此指针和引用不失效
  2. 从头部或尾部添加元素,可能需要分配新的内存块, 这会使迭代器失效
  3. 从中间添加元素,需要移动其它元素,这会使迭代器、指针和引用失效

回到开头将deque的两个特点,是将vector可随机访问和list容易添加元素的优点,但是凡事都是有代价的,在一些从一端添加删除元素的情况下,vector的性能可能比deque要好(比如用作stack时),deque适合用作队列(queue),即一端入另一端出的情况,但具体还是应该根据实际情况而论,详情参见STL-stack

随机访问O(1)

具体而言,deque的具体实现是 map of vectors :用一个大的 map ,管理很多小的 vector ,小的 vector 里存的才是element。因此它能够实现随机访问( map 和 vector 都可以实现随机访问)

deque底层探究

下文转自deque容器底层实现原理,有所删改

前面说到,deque 的实现为 map of vectors,即 deque 存储数据的空间是一段一段等长的连续空间构成,各段空间不一定是连续的,可以位于内存的不同区域

为了管理这些不同的区域,deque 用数组(数组名假定为map)存储着各个连续空间的首地址,也就是说 map数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间

通过建立map数组,deque 申请的这些分段的内存就能实现”整体连续“的效果,换句话说,当 deque 需要在头部或者尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部

当 map 满了会怎样?很简单,再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间

deque 容器的分段存储结构,提高了在序列两端添加或删除元素的效率,但也使该容器迭代器的底层实现变得更复杂

deque迭代器的底层实现

由于 deque 底层将序列中的元素分别存储到了不同段的连续空间中,因此要想实现迭代器的功能,必须先解决如下 2 个问题:

  1. 迭代器在遍历 deque 时,必须能够确认各个连续空间在 map 数组中的位置
  2. 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中

为了实现遍历 deque 的功能,其迭代器定义如下的结构:

1
2
3
4
5
6
7
8
template<class T,...>
struct __deque_iterator{
...
T* cur;
T* first;
T* last;
map_pointer node;//map_pointer 等价于 T**
}

四个指针的作用

  1. cur:指向当前正在遍历的元素
  2. first:指向当前连续空间的首地址
  3. last:指向当前连续空间的末尾地址
  4. node:它是一个二级指针,用于指向 map 数组中存储的指向当前连续空间的指针

借助这 4 个指针,deque 迭代器对随机访问迭代器支持的各种运算符进行了重载,能够对 deque 分段连续空间中存储的元素进行遍历,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//当迭代器处于当前连续空间边缘的位置时,如果继续遍历,就需要跳跃到其它的连续空间中,该函数可用来实现此功能
void set_node(map_pointer new_node){
node = new_node;//记录新的连续空间在 map 数组中的位置
first = *new_node; //更新 first 指针
//更新 last 指针,difference_type(buffer_size())表示每段连续空间的长度
last = first + difference_type(buffer_size());
}
//重载 * 运算符
reference operator*() const{return *cur;}
pointer operator->() const{return &(operator *());}
//重载前置 ++ 运算符
self & operator++(){
++cur;
//处理 cur 处于连续空间边缘的特殊情况
if(cur == last){
//调用该函数,将迭代器跳跃到下一个连续空间中
set_node(node+1);
//对 cur 重新赋值
cur = first;
}
return *this;
}
//重载前置 -- 运算符
self& operator--(){
//如果 cur 位于连续空间边缘,则先将迭代器跳跃到前一个连续空间中
if(cur == first){
set_node(node-1);
cur == last;
}
--cur;
return *this;
}

deque容器的底层实现

了解了 deque 容器底层存储序列的结构,以及 deque 容器迭代器的内部结构之后,接下来看看 deque 容器究竟是如何实现的

deque 容器除了维护先前讲过的 map 数组,还需要维护 start、finish 这 2 个 deque 迭代器,以下为 deque 容器的定义:

1
2
3
4
5
6
7
8
9
10
11
//_Alloc为内存分配器
template<class _Ty,
class _Alloc = allocator<_Ty>>
class deque{
...
protected:
iterator start;
iterator finish;
map_pointer map;
...
}

其中,start 迭代器记录着 map 数组中首个连续空间的信息,finish 迭代器记录着 map 数组中最后一个连续空间的信息

另外需要注意的是,和普通 deque 迭代器不同,start 迭代器中的 cur 指针指向的是连续空间中首个元素,而 finish 迭代器中的 cur 指针指向的是连续空间最后一个元素的下一个位置

deque

借助 start 和 finsh,以及 deque 迭代器中重载的诸多运算符,就可以实现 deque 容器提供的大部分成员函数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//begin() 成员函数
iterator begin() {return start;}
//end() 成员函数
iterator end() { return finish;}
//front() 成员函数
reference front(){return *start;}
//back() 成员函数
reference back(){
iterator tmp = finish;
--tmp;
return *tmp;
}
//size() 成员函数
size_type size() const{return finish - start;}//deque迭代器重载了 - 运算符
//enpty() 成员函数
bool empty() const{return finish == start;}

我查看vs库中实现与此方法不同,但思路基本相同