文章目录
1. 关于deque
1.1 deque概述
- deque,即我们常说的队列,是一个双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除操作
- deque由一段段的定量连续空间组成,稍后再介绍
1.2 deque与vector区别
1. deque允许于常数时间内对起头端进行元素的插入或移除操作
2. deque没有所谓的容量观念
3. 因为deque需要维持其整体连续的假象,导致其实现起来比较繁琐,为了提高效率,尽可能选择vector而不是deque
2. deque构成
– 刚刚说到,deque是由一段段的定量连续空间组成,那么怎样设计才能使得deque看起来整体连续呢?
答案是采用一块map(此map非STLmap)作为主控。map是一小块连续空间,其中每个元素都是指针,指向缓冲区,此缓冲区负责存储deque的元素
3. deque的迭代器
- deque迭代器必须要能够指出缓冲区所在位置,其次要能够判断是否在缓冲区的边缘,还须时刻掌握管控中心map:
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
//...
static size_t buffer_size() { return __deque_buf_size(BufSize, sizeof(T)); } //定义缓冲区大小
//...
typedef T** map_pointer; //管控中心
T* cur; //此迭代器所指缓冲区中现行元素
T* first; //此迭代器所指缓冲区的头
T* last; //此迭代器所指缓冲区的尾
map_pointer node; //指向管控中心
//...
};
//如果n不为0,表示由用户自己设定缓冲区大小,就返回n
//如果n为0,表示采用默认值:
// 如果sz(元素大小)小于512,则缓冲区大小为512 / sz
// 如果sz不小于512,则返回1
inline size_t __deque_buf_size(size_t n, size_t sz)
{
return n != 0 ? n : (sz < 512 ? szie_t(512 / sz) : size_t(1));
}
- 如下图所示,一个迭代器的构造:
- deque迭代器设计即,将所支持的操作运算符重载,比较简单,就不详细分析了
4. deque构造与内存管理
deque,除了维护指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素,当map所提供的节点不足时,就必须重新分配更大的一块map
- 以实际例子来解析deque的构造与内存管理:
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
int main(int argc, char** argv) {
deque<int> ideq(20,9); //1
cout << "size = " << ideq.size() << endl; //size = 20
//为每个元素设定初值
for (int i = 0;i < ideq.size(); ++i)
ideq[i] = i;
//输出每个元素
for (int i = 0; i < ideq.size(); ++i)
cout << ideq[i] << ' '; //0 1 2 ... 19
cout << endl;
//在尾端添加3个元素
for (int i = 0;i < 3; ++i)
ideq.push_back(i);
//输出所有元素
for (int i = 0; i < ideq.size(); ++i)
cout << ideq[i] << ' '; //0 1 2 ...19 0 1 2
cout << endl;
cout << "size = " << ideq.size() << endl; //size = 23
//在头部增加一个元素
ideq.push_front(99);
for (int i = 0; i < ideq.size(); ++i)
cout << ideq[i] << ' '; //99 0 1 2 ... 19 0 1 2
cout << endl;
cout << "size = " << ideq.size() << endl; //size = 24
return 0;
}
4.1 deque构造
当创建一个deque时,调用deque的构造函数
:
deque(int n, const value_type& value)
: start(), finsh(), map(0), map_size(0)
{
fill_initialize(n, value); //负责安排deque的数据结构,并设定元素初值
}
//fill_initialize函数实现
template <class T, class Alloc, size_t BufSzie>
void deque<T,Alloc, BufSize>::fill_initialize(size_type n, const value_type& value) {
create_map_and_nodes(n); //负责安排deque的结构
map_pointer = cur;
__STL_TRY {
//为每个节点设定初始值
for (cur = start.node; cur < finish.node; ++cur){
uninitialized_fill(*cur, *cur + buffer_size(), value);
}
//最后一个节点的尾端可能有备用空间,不必设初值
uninitialized_fill(finish.first, finish.cur, value);
}
catch(...) {
//...
}
}
//create_map_and_nodes函数实现
template <class T, class Alloc, size_t BufSize>
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);
// 调用deque专属空间配置器,配置map空间
map = map_allocator::allocate(map_size);
// 将[nstart, nfinish)区间设置在map的中间,
// 这样就能保证前后增长而尽可能减少map的重新分配次数
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
// 分配结点空间
map_pointer cur;
__STL_TRY {
for (cur = nstart; cur <= nfinish; ++cur)
// 为每一个map指针指向的缓冲区的每一个元素分配内存空间
*cur = allocate_node();
}
// 维护指针状态,为deque的两个迭代器start和finish赋初值
start.set_node(nstart);
finish.set_node(nfinish);
start.cur = start.first;
finish.cur = finish.first + num_elements % buffer_size();
}
4.2 push_back元素操作
先来简短分析一下push_back的步骤:
- 调用push_back,如若最后一个缓冲区尚有两个以上的备用空间,则直接在备用空间构造新元素
- 只剩一个元素备用空间时,调用push_back_aux配置新缓冲区
- 调用push_back_aux,先行调用reserve_map_at_back()判断是否需要更换map,如果需要则调用reallocate_map()进行map的更换,不需要则什么也不做
- 在新缓冲区中构造元素并更改finish状态
- 以下是push_back函数实现:
public:
void push_back(const value_type& t) {
if (finish.cur != finish.last - 1) { //如果最后一个缓冲区还有两个以上的元素备用空间
construct(finish.cur, t); //则直接在备用空间构造元素
++finish.cur; //调整最后缓冲区使用状态
}
else //即只有一个元素备用空间调用该函数
push_back_aux(t); //配置一块新缓冲区
}
//只剩一个元素备用空间
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t)
{
value_type t_copy = t;
// 判断是否需要调整map空间
reserve_map_at_back();
*(finish.node + 1) = allocate_node(); // 配置一块新的缓冲区
__STL_TRY {
construct(finish.cur, t_copy); // 构造新加入的元素
finish.set_node(finish.node + 1); // 调整finish
finish.cur = finish.first; //令cur指向新缓冲区
}
__STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
//reverse_map_at_back()函数实现:
void reverse_map_at_back(size_type nodes_to_add = 1)
{
if (nodes_to_add + 1 > map_size - (finish.node - map) //map尾端备用空间节点不足,则调用下面函数更换map
reallocate_map(nodes_to_add, false); //稍后解析
}
4.3 push_front元素操作
先来简短分析一下push_front的步骤:
- 调用push_front,如若第一缓冲区尚有备用空间,则直接在备用空间构造新元素
- 没有元素备用空间时,调用push_front_aux配置新缓冲区
- 调用push_front_aux,先行调用reserve_map_at_front()判断是否需要更换map,如果需要则调用reallocate_map()进行map的更换,不需要则什么也不做
- 在新缓冲区中构造元素并更改start状态
public:
void push_front(const value_type& t) {
if (start.cur != start.first) { //尚有备用空间
construct(start.cur - 1, t); //直接构造元素
--start.cur; //调整缓冲区使用状态
}
else
push_front_aux(t); //第一缓冲区无备用空间则调用
}
//没有备用空间
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t)
{
value_type t_copy = t;
reserve_map_at_front(); // 同push_back(),检查是否需要调整map
*(start.node - 1) = allocate_node(); // 配置一块新的缓冲区
__STL_TRY {
start.set_node(start.node - 1); // 调整start
start.cur = start.last - 1;
construct(start.cur, t_copy);
}
catch (...) {
start.set_node(start.node + 1);
start.cur = start.first;
deallocate_node(*(start.node - 1));
throw;
}
}
//reserve_map_at_front函数实现
void reverse_map_at_front(size_type nodes_to_add = 1) {
if (nodes_to_add > start.node - map) //map前端节点不足则调用下面函数进行更换map
reallocate_map(nodes_to_add, true);
}
4.4 reallocate_map函数实现
主要分两种情况进行分析:
1. 如果只是一端的节点使用完了,而另一端还剩余很多空间,则在原map进行调整
2. 实在没有空间可供调整,那就配置一块新map,将原map的内容拷贝过来,释放掉原来map的空间
- 下面是函数实现:
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
bool add_at_front)
{
size_type old_num_nodes = finish.node - start.node + 1; //原节点数
size_type new_num_nodes = old_num_nodes + nodes_to_add; //新节点数
map_pointer new_nstart;
// 此处为了防止出现一端已经用完,另一端却还有很多剩余的情况
if (map_size > 2 * new_num_nodes) {
// 调整新的map中的起始点
new_nstart = map + (map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0); //根据传入的bool值进行调整
// 如果前端剩余很多
if (new_nstart < start.node)
copy(start.node, finish.node + 1, new_nstart);
else // 尾端剩余很多
copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes);
}
else { // map不够用了,就需要配置一块更大的map
size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
// 配置一块大的map
map_pointer new_map = map_allocator::allocate(new_map_size);
// 始终要使start和finish处在map空间的中间
new_nstart = new_map + (new_map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
// 拷贝到新的map空间中去
copy(start.node, finish.node + 1, new_nstart);
// 释放旧的空间
map_allocator::deallocate(map, map_size);
// 设定map的起始地址和大小
map = new_map;
map_size = new_map_size;
}
// 调整新的start和finish
start.set_node(new_nstart);
finish.set_node(new_nstart + old_num_nodes - 1);
}
5. deque的元素操作
- 以下是一些元素操作,push_back与push_front上述已经解析了,所以在这就不再重复提及:
操作 | 功能 |
---|---|
pop_back | 将尾端元素去掉,若最后缓冲区没有元素则会调用pop_back_aux进行缓冲区的释放 |
pop_front | 将首部元素去掉,若第一缓冲区只有一个元素则调用pop_front_aux进行缓冲区的释放 |
clear | 用来清除整个deque,会保留一个缓冲区 |
erase | 1.清除某个元素,返回被删元素位置的迭代器 2.用来清除[first,last)之间的元素 |
insert | 1.若插入点是deque的最前端,交给push_front去做 2.如果插入点是最尾端,则交给push_back去做 3. 否则交给insert_aux |
5.1 关于erase
首先理清以下erase的思路:
erase单个元素:
1. 如果清除点之前的元素较少,则向后移动清除点之前的元素
2. 如果清除点之后的元素较少,则向前移动清除点之后的元素
erase[first,last)区间元素:
1. 清除的是整个deque,则调用clear
2. 清除的是部分空间:
2.1 如果清除点之前的元素较少,则向后移动清除点之前的元素
2.2 如果清除点之后的元素较少,则向前移动清除点之后的元素
- 以下是函数实现:
//清除单个元素
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;
}
//清除一个区间
template <class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::erase(iterator first, iterator last)
{
if (first == start && last == finish) { // 需要擦除整个deque
clear();
return finish;
}
else {
difference_type n = last - first; // 清除区间的长度
difference_type elems_before = first - start; // 待清除区间前方的元素个数
if (elems_before < (size() - n) / 2) { // 如果前方的元素个数较少
copy_backward(start, first, last); // 向后移动前方元素
iterator new_start = start + n; // 调整新的起始点
destroy(start, new_start); // 全局函数,析构节点元素
for (map_pointer cur = start.node; cur < new_start.node; ++cur)
data_allocator::deallocate(*cur, buffer_size()); // 释放缓冲区空间
start = new_start;
}
else { // 后方元素比较少的情况
copy(last, finish, first); // 向前移动后方元素
iterator new_finish = finish - n; // 调整新的finish迭代器
destroy(new_finish, finish); // 全局函数,析构节点元素
for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
data_allocator::deallocate(*cur, buffer_size()); // 释放缓冲区空间
finish = new_finish;
}
return start + elems_before;
}
}
5.2 关于insert_aux
实现原理:
1. 判断插入点之前的元素个数
2. 如插入点之前的元素个数较少,则将插入点之前的元素(包括插入点的元素)向后移动一位,将新元素x插入到插入点
3. 如插入点之后的元素个数较少,则将插入点之后的元素(包括插入点的元素)向前移动一位,将新元素x插入到插入点
- 以下是函数实现:
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x)
{
difference_type index = pos - start; // 插入元素前面的元素个数
value_type x_copy = x;
if (index < size() / 2) { // 如果前端的元素比较少
push_front(front()); // 在最前面插入一个与第一个元素一样的数
iterator front1 = start; // 记录起始点
++front1;
iterator front2 = front1;
++front2;
pos = start + index;
iterator pos1 = pos;
++pos1;
copy(front2, pos1, front1); // 拷贝空间,将[front2,pos1)拷贝到front1
}
else { // 后端的元素比较少,原理同上
push_back(back());
iterator back1 = finish;
--back1;
iterator back2 = back1;
--back2;
pos = start + index;
copy_backward(pos, back2, back1); //拷贝空间,将[pos, back2]拷贝到back1
}
*pos = x_copy;
return pos;
}