从零开始写STL-容器-双端队列
从零开始写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-容器-双端队列的更多相关文章
- C++STL之双端队列容器
C++STL之双端队列容器 deque双端队列容器与vector很类似,采用线性表顺序存储结构.但与vector区别,deque采用分块的线性存储结构来存储数据,每块的大小一般为512B,将之称为de ...
- 从零开始写STL—容器—vector
从0开始写STL-容器-vector vector又称为动态数组,那么动态体现在哪里?vector和一般的数组又有什么区别?vector中各个函数的实现原理是怎样的,我们怎样使用会更高效? 以上内容我 ...
- STL容器-deque-双端队列
注明:全部来自转载,供自己学习与复习使用 deque双向开口可进可出的容器 我们知道连续内存的容器不能随意扩充,因为这样容易扩充别人那去 deque却可以,它创造了内存连续的假象. 其实deque由一 ...
- [STL] deque 双端队列
- STL队列 之FIFO队列(queue)、优先队列(priority_queue)、双端队列(deque)
1.FIFO队列 std::queue就是普通意思上的FIFO队列在STL中的模版. 1.1主要的方法有: (1)T front():访问队列的对头元素,并不删除对头元素 (2)T back(): ...
- c++实现双端队列
在使用c++容器的时候其底层如何实现 例如 vector 容器 :是一个内存可以二倍扩容的向量容器,使用方便但是对内存要求严格,弊端明显 list 容器 : 双向循环链表 deq ...
- 【C++】STL常用容器总结之五:双端队列deque
6.双端队列deque 所谓的deque是”double ended queue”的缩写,双端队列不论在尾部或头部插入元素,都十分迅速.而在中间插入元素则会比较费时,因为必须移动中间其他的元素.双端队 ...
- stl之deque双端队列容器
deque与vector很相似,不仅能够在尾部插入和删除元素,还能够在头部插入和删除. 只是当考虑到容器元素的内存分配策略和操作性能时.deque相对vector较为有优势. 头文件 #include ...
- STL容器:deque双端队列学习
所谓deque,是"double-ended queue"的缩写; 它是一种动态数组形式,可以向两端发展,在尾部和头部插入元素非常迅速; 在中间插入元素比较费时,因为需要移动其它元 ...
随机推荐
- WindowForm.计算器
设计计算器: 外部变量: 数字键按钮: 运算符按钮事件代码: 清零按钮 等号按钮: 思维导图:
- spark集群启动步骤及web ui查看
集群启动步骤:先启动HDFS系统,在启动spark集群,最后提交jar到spark集群执行. 1.hadoop启动cd /home/***/hadoop-2.7.4/sbinstart-all.sh ...
- InChatter系统之客户端实现原理与阶段小结
InChatter客户端的开发可以说是目前系统的阶段性结尾了.很抱歉的是,这篇文章来的这么晚,迟到了这么久. 在客户端的开发主要针对两个方面: 消息的传输与处理 消息的UI交互处理 一.消息的传输与处 ...
- 【PostgreSQL-9.6.3】表空间
在PostgreSQL中,表空间实际上是为表指定一个存储目录,这样方便我们把不同的表放在不同的存储介质或者文件系统中.在创建数据库.表.索引时都可以指定表空间. 1. 创建表空间 --表空间目录必须是 ...
- C#运用存储过程新增一条记录并返回自动生成的ID
@Hcy黄灿奕:http://blog.sina.com.cn/iihcy 前言: 1.存储过的好处: 存储过程相对于其他的数据库访问方法有以下的优点: (1)重复使用.存储过程可以重复使用,从而可以 ...
- zabbix3.0.4 部署之八 (zabbix3.0.4 报警前端配置)
(如何让报警信息推送----微信.邮件)(邮件与微信一样就不在重复) 创建一个用户 将用户加入administrator组 添加之前设置的报警媒介脚本 设置报警等级 创建动作 配置报警内容 设置报警条 ...
- jvm中的内存溢出与内存泄露
内存溢出: 就是我们通常遇到的OutOfMemoryError异常,它俗理解就是内存不够,通常在运行大型程序时发生,当程序所需要的内存远远超出了JVM内存所承受大小,就会报出OutOfMemoryEr ...
- vue中的组件传值
组件关系可以分为父子组件通信.兄弟组件通信.跨级组件通信. 父传子 - props 子传父 - $emit 跨级可以用bus 父子双向 v-model 父链(this.$parent this.$ch ...
- JVM优化(下)
14.Tomcat8优化之G1牢记收集器的测试结果以及小结: jmeter测试:(1)tomcat未做任何优化:(2)禁用AJP:(3)启用连接池:(4)设置最大线程数1000,初始线程数200:(5 ...
- 启动web项目卡在Initializing Spring root WebApplicationContext不动
这几天在和同学一起做一个电教器材管理系统的Web项目,用SVN互通,在此记录下经常遇到的bug. Bug: 启动项目一直卡在Initializing Spring root WebApplicatio ...