从零开始写STL-容器-双端队列

什么是双端队列?在介绍vector源码,我们发现在vector前端插入元素往往会引起大量元素的重新分配,双端队列(deque)就是为了解决这一问题,双端队列中在首端和末端插入元素的时间复杂度都为O(1),也许你会说链表不行吗,但是其实链表存在一定的缺陷,比如每个结点都要多花出两份存储指针的空间,下面我们将通过源码来分析deque的实现。

基本实现模型 链状数组

动态数组中在首端插入元素低效率的根本原因在于不能在首端分配新的内存,结合链表的实现我们可以实现一个链状数组,其中维护一个内存缓存数组,数组中每个元素指向一片固定大小的内存块,当前内存块耗尽(到达最末端或者首端)就会通过分配器来调度迭代器的位置进入下一个或者前一个内存块。

要维护一个双端队列需要实现哪些?

  • 内存缓冲区数组的位置
  • 当前已使用的缓冲区大小和位置(通过 维护数组中两个指针,一个指向被使用的最前面的内存块,一个指向被使用的最后面的内存块。

双端队列迭代器

要维护一个迭代器需要实现哪些东西?

  • 首先需要知道当前在哪个内存块(内存分配器所分配的块状内存)中
  • 还要知道内存块的开始/结束位置 以免访问未初始化的内存

    实现迭代器的随机存取时,注意通过内存块容纳元素数量和内存块位置来决定元素之间的距离。
	template<class T, size_t Bufsize = 0>
struct _deque_iterator : public random_access_iterator<T>
{
typedef _deque_iterator<T> iterator;
// 这里计算的是一个内存块中可以存放多少T类型的实例
static size_t buffer_size()
{
return _deque_buf_size(Bufsize, sizeof(T));
} _deque_iterator()
{
cur = first = last = NULL;
node = NULL;
}
T* cur;//此迭代器所指缓冲区中的元素
T* first;//缓冲区开头元素
T* last;//缓冲区尾部元素
map_pointer node;//缓冲区控制器 void set_node(map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
} reference operator*() const
{
return *cur;
}
pointer operator->() const
{
return &(operator*());
}
difference_type operator-(const self& x)const
{
return difference_type(buffer_size())*(node - x.node - 1) + (cur - first) + (x.last - x.cur);// 间隔内存块*每个内存块个数 + 结点offset
}
self& operator++()
{
++cur;
if (cur == last) //到当前内存块最后一个元素,由链状数组 内存分配器 来指向下一个内存块
{
set_node(node + 1);
cur = first;
}
return *this;
}
self operator++(int)
{
self tmp = *this;
++*this;
return tmp;
}
self& operator--()
{
--cur;
if (cur == first)
{
set_node(node - 1);
cur = last;
}
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
//随机存取
self& operator+=(difference_type n)
{
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < (difference_type)(buffer_size()))
cur += n;//在当前的缓冲块内部
else
{
difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size())
: -difference_type((-offset - 1) / buffer_size()) - 1;
set_node(node + node_offset);// 计算内存块的偏移量
cur = first + (offset - node_offset*difference_type(buffer_size()));
}
return *this;
}
self operator+(difference_type n) const
{
self tmp = *this;
return tmp += n;
} self& operator-=(difference_type n)
{
return *this += -n;
}
self operator-(difference_type n)const
{
self tmp = *this;
return tmp -= n;
}
reference operator[](difference_type n) const
{
return *(*this + n);
} bool operator==(const self &x) { return x.cur == cur; }
bool operator!=(const self &x)
{
return !(*this==x);
}
bool operator<(const self& x)
{
return (node == x.node) ? (cur < x.cur) : (node < x.node);//优先比较缓冲区!
}
};

双端队列源码

deque 除了维护一个先前说过的指向map的指针外,也维护start finish 两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素,同时管理当前map的大小,在节点不足时重新分配内存。

typedef 部分

		typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef size_t size_type;
typedef _deque_iterator<T> iterator;
typedef ptrdiff_t difference_type;
typedef size_t size_type;
typedef pointer* map_pointer;

内部数据

protected:
iterator start, finish;//维护已经使用内存块的头尾
map_pointer map;//指向内存分配器
size_type map_size;//存放数据量
std::allocator<T> data_allocator;//内存分配器
std::allocator<pointer> map_allocator;//注意这个内存分配器是用来分配内存块的

内存分配

		void create_map_and_nodes(size_type num_elements)
{
//分配内存结点数量
size_type num_nodes = num_elements / buffer_size() + 1;
map_size = std::max((size_t)8, num_nodes + 2);
map = map_allocator.allocate(map_size);
//取中间部分来存放数据,这样给头围留下较为稳定均衡的增长空间
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
map_pointer cur;
try
{
//注意这里cur 是 T**
for (cur = nstart; cur <= nfinish; cur++)
{
//分配内存块 这里*cur 是 T*
*cur = data_allocator.allocate(buffer_size());
}
}
catch (...)
{ }
start.set_node(nstart);
finish.set_node(nfinish);
start.cur = start.first;
finish.cur = finish.first + num_elements%buffer_size();
}
//填充值
void fill_initialize(size_t n,const value_type& val)
{
create_map_and_nodes(n);
map_pointer cur;
try
{
for (cur = start.node; cur < finish.node; cur++)
std::uninitialized_fill(*cur, *cur + buffer_size(), val);
std::uninitialized_fill(finish.first, finish.last, val);
}
catch (...)
{
delete this;
throw __uncaught_exception;
}
}
//要增加的内存块数 以及是否在前端添加(便于更加高效移动元素)
void 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_start;
if (map_size > 2 * new_num_nodes)//无需重新分配内存
{
new_start = map + (map_size - new_num_nodes) / 2 + (add_at_front ? nodes_to_add : 0);
if (new_start < start.node)
std::copy(start.node, finish.node + 1, new_start);
else
std::copy_backward(start.node, finish.node + 1, new_start + old_num_nodes);
}
else
{
size_type new_map_size = map_size + std::max(map_size, nodes_to_add) + 2;
map_pointer new_map = map_allocator.allocate(new_map_size);
new_start = new_map + (new_map_size - new_num_nodes) / 2 + (add_at_front ? nodes_to_add : 0);
std::copy(start.node, finish.node + 1, new_start);
map = new_map;
map_size = new_map_size;
}
}
void push_front_aux(const value_type& val)
{
if (start.node - map < 1)
reallocate_map(1, true);
*(start.node - 1) = data_allocator.allocate(buffer_size());
try
{
start.set_node(start.node - 1);
start.cur = start.last - 1;
data_allocator.construct(start.cur, val);
}
catch (...)// commit or rollback!
{
start.set_node(start.node + 1);
start.cur = start.first;
data_allocator.deallocate(*(start.node - 1),buffer_size());
throw;
}
}
void push_back_aux(const value_type& val)
{
if (map_size - (finish.node - map) < 2)
reallocate_map(1, false);
*(finish.node + 1) = data_allocator.allocate(buffer_size());
try
{
data_allocator.construct(finish.cur, val);
finish.set_node(finish.node + 1);
finish.cur = finish.first;
}
catch (...)
{
finish.set_node(finish.node - 1);
finish.cur = finish.last;
data_allocator.deallocate(*(finish.node + 1), buffer_size());
throw;
}
}

数据获取相关

		iterator begin()
{
return start;
}
iterator end()
{
return finish;
}
reference operator[](size_type n)
{
return start[(difference_type)n];
}
reference front()
{
return *start;
}
reference back()
{
return *(finish - 1);
}
size_type size()
{
return finish - start;
}
size_type max_size() { return size_type(-1); }
bool empty() { return finish == start; }

Modifiers

		void push_back(const value_type& val)
{
if (finish.cur != finish.last - 1)
{
//没必要重新分配空间
data_allocator.construct(finish.cur, val);
++finish.cur;
}
else
push_back_aux(val);
}
void push_front(const value_type& val)
{
if (start.cur != start.first)
{
data_allocator.construct(start.cur - 1, val);
--start.cur;
}
else
push_front_aux(val);
}
void clear()
{
for (auto it = start.cur + 1; it < finish.cur; it++)
data_allocator.destroy(it);//销毁内存块中元素
for (auto it = start.node + 1; it <= finish.node; it++)//销毁并回收内存块
map_allocator.destroy(it),map_allocator.deallocate(it,1);
finish = start;
}
void pop_back()
{
if (finish.cur != finish.first)
{
--finish.cur;
data_allocator.destroy(finish.cur);
}
else
{
//回收当前内存块
data_allocator.deallocate(finish.first,buffer_size());
finish.set_node(finish.node - 1);
finish.cur = finish.last - 1;
data_allocator.destroy(finish.cur);
}
}
void pop_front()
{
if (start.cur != start.last - 1)
{
data_allocator.destroy(start.cur);
start.cur++;
}
else
{
destroy(start.cur);
set_node(start.node + 1);
start.cur = start.first;
}
} iterator erase(iterator pos)
{
iterator next = pos++;
difference_type index = pos - start;
if (index < size() / 2)
{
copy_backward(start, pos, next);
pop_front();
}
else
{
copy(next, finish, pos);
pop_back();
}
return start + index;
}
//这里和vector 删除元素的思路类似
//通过前端 后端 元素数量选择最高效的移动数据方式
iterator erase(iterator first, iterator last)
{
if (first == start&&last == finish)
{
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;
for (auto it = start; it < new_start; it++)
data_allocator.destroy(it);
for (auto it = start; it < new_start; it++)
data_allocator.deallocate(it, buffer_size());
start = new_start;
}
else
{
copy(last, finish, first);
iterator new_finish = finish - n;
for (auto it = new_finish; it < finish; it++)
data_allocator.destroy(it);
for (auto it = new_finish; it < finish; it++)
data_allocator.deallocate(it, buffer_size());
finish = new_finish;
}
return start + elems_before; }
} iterator insert(iterator pos, const value_type& x)
{
if (pos.cur == start.cur)
{
push_front(x);
return start;
}
else if (pos.cur == finish.cur)
{
push_back(x);
return finish - 1;
}
else
{
difference_type index = pos - start;
value_type x_copy = x;
//通过Push_front 把头元素复制到前一个内存块,然后将原内存块元素移动到对应位置
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); }
else
{
push_back(back());
iterator back1 = finish;
--back1;
iterator back2 = back1;
--back2;
pos = start + index;
copy_backward(pos, back2, back1);
}
*pos = x_copy;
return pos;
}
}

从零开始写STL-容器-双端队列的更多相关文章

  1. C++STL之双端队列容器

    C++STL之双端队列容器 deque双端队列容器与vector很类似,采用线性表顺序存储结构.但与vector区别,deque采用分块的线性存储结构来存储数据,每块的大小一般为512B,将之称为de ...

  2. 从零开始写STL—容器—vector

    从0开始写STL-容器-vector vector又称为动态数组,那么动态体现在哪里?vector和一般的数组又有什么区别?vector中各个函数的实现原理是怎样的,我们怎样使用会更高效? 以上内容我 ...

  3. STL容器-deque-双端队列

    注明:全部来自转载,供自己学习与复习使用 deque双向开口可进可出的容器 我们知道连续内存的容器不能随意扩充,因为这样容易扩充别人那去 deque却可以,它创造了内存连续的假象. 其实deque由一 ...

  4. [STL] deque 双端队列

  5. STL队列 之FIFO队列(queue)、优先队列(priority_queue)、双端队列(deque)

    1.FIFO队列   std::queue就是普通意思上的FIFO队列在STL中的模版. 1.1主要的方法有: (1)T front():访问队列的对头元素,并不删除对头元素 (2)T back(): ...

  6. c++实现双端队列

    在使用c++容器的时候其底层如何实现  例如  vector 容器  :是一个内存可以二倍扩容的向量容器,使用方便但是对内存要求严格,弊端明显    list  容器  : 双向循环链表    deq ...

  7. 【C++】STL常用容器总结之五:双端队列deque

    6.双端队列deque 所谓的deque是”double ended queue”的缩写,双端队列不论在尾部或头部插入元素,都十分迅速.而在中间插入元素则会比较费时,因为必须移动中间其他的元素.双端队 ...

  8. stl之deque双端队列容器

    deque与vector很相似,不仅能够在尾部插入和删除元素,还能够在头部插入和删除. 只是当考虑到容器元素的内存分配策略和操作性能时.deque相对vector较为有优势. 头文件 #include ...

  9. STL容器:deque双端队列学习

    所谓deque,是"double-ended queue"的缩写; 它是一种动态数组形式,可以向两端发展,在尾部和头部插入元素非常迅速; 在中间插入元素比较费时,因为需要移动其它元 ...

随机推荐

  1. [转]在ASP.NET MVC3中使用EFCodeFirst 1.0

    本文转自:http://kb.cnblogs.com/page/97003/ 作者: NinoFocus  来源: 博客园  发布时间: 2011-04-12 10:41  阅读: 11971 次   ...

  2. xshell常用命令大全

    xshell常用命令大全 (1)命令ls——列出文件 ls -la 给出当前目录下所有文件的一个长列表,包括以句点开头的“隐藏”文件 ls a* 列出当前目录下以字母a开头的所有文件 ls -l *. ...

  3. 【Java】包装类型

    Java中的基本类型功能简单,不具备对象的特性,为了使基本类型具备对象的特性,所以出现了包装类,就可以像操作对象一样操作基本类型数据. 一.基本类型对应的包装类 基本类型                ...

  4. P1433 吃奶酪

    题目描述 房间里放着n块奶酪.一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在(0,0)点处. 输入输出格式 输入格式: 第一行一个数n (n<=15) 接下来每行2个实数,表示第i块 ...

  5. Python3 动手自己写谷歌翻译

    本篇为实现谷歌翻译的功能,在编写的时候以为只是一个接口的问题. 没想到的是每次翻译都会触发一次JS的执行,在请求参数中生成一个tk. 文中tk的实现是复用的网上大神的代码生成tk. 好了,不说了直接看 ...

  6. python学习一 hello world of python

    我使用的环境是 OS:Red Hat Enterprise Linux AS release 4 (Nahant Update 3) PYTHON:2.7.2 如果操作系统没有安装python, 百度 ...

  7. file.seek()

    语法:fileObject.seek(offset,whence) offset -- 开始的偏移量,也就是代表需要移动偏移的字节数 whence:可选,默认值为 0.给offset参数一个定义,表示 ...

  8. 解决webstorm中vue语法没有提示

    首先看看webstrom内置的vue插件,打上勾,没有这个选项就要自己去下载插件了 如果插件还是没有语法提示,可以用下面的方法,自己添加语法进去搜索 unknown HTML tag attribut ...

  9. HTTP的工作原理

    客户机与服务器建立连接后,发送一个请求给服务器,请求格式为:统一资源标识符.协议版本号.服务器收到请求的信息(包括请求行,请求头,请求体).服务器接收到请求后,给予相应的响应信息,格式为一个状态行(包 ...

  10. 06JavaScript函数

    JavaScript函数 3.1系统函数 3.1.1编码函数 功能:将字符串中非文字.数字字符(如&,%,#,^,空格符…)转成相对应的ASCII值. 语法:escape(字符串) 3.1.2 ...