deque的底层实现
vector是单向开口的连续线性空间,deque则是一种双向开口的由一段一段的定量连续空间组成,即可以在头尾两端分别做元素的插入和删除操作。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间而我们也知道vector的头部操作效率非常差。
deque采用一块map(不是STL中的map)作为主控。这里的map是一小块连续空间,其中每个元素(此处称为一个结点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。
map其实是一个二级指针T**,它本身是一个指针,所指之物又是一个指针,指向类别为T的一块空间。
如下图所示
那么一个map要管理几个结点呢?最少8个,最多是”所需节点数加2”
从图中我们可以看到deque的内存不是整体连续的,而是由一段一段的定量连续空间构成(分段连续)。它给我们的假象是整体连续,并提供了随机存取的接口。所以为了维持这种整体连续的假象,它的数据结构的设计以及迭代器的前进后退等操作都颇为繁琐。(实现代码可想而知比其他两个顺序性容器多得多)
deque的类:
template <class T,class Alloc=alloc,size_t BufSiz=0>//BufSiz指每个缓冲区可以容纳的元素个数
class deque{
public:
typedef T value_type;
typedef _deque _iterator<T,T&,T*,BufSiz>iterator;
protected:
//二级指针
typedef pointer*map_pointer;//T**
protected:
map_pointer map;//指向上图中的map,相当于指针数组
size_type map_size;//记录map中可以容纳多少指针
iterator start;//指向第一个结点
iterator finish;//指向最后一个结点
public:
iterator begin(){return start;}
iterator end(){return finish;}
size_type size() const {return finish-start;}
...
};
从上面deque的数据结构分析,除了上面说的map二级指针外,它也维护start,finish两个迭代器。这两个迭代器分别指向第一缓冲区的第一个元素和最后缓冲区的最后元素(的下一位置)。当然也得记录中控器的大小(map_size),因为一旦map所提供的节点不足,就必须重新配置更大的一块map。
来看下加入start、finish迭代器后的deque的内存管理方式图:
当1结点(缓冲区)中的空间不足(到达1的最左端),则map的node指针会向左指向一个新的空间,分配一个新的buffer出来。当4的空间不足,则map会的node指针会向右指向一个新的空间,分配一个新的buffer。这样就实现了,deque可以双向扩充的特性,也形成了其空间连续的假象。
node:指向map中的结点
first\last:指向node所指的buffer的头和尾,标示出缓冲区的边界,当iterator++或者iterator--到边界时,为了保持其连续性,需要跳到下一个缓冲区。(由node向左或者向右移动)
set_node(),是跳到下一个缓冲区的函数
void set_node(map_pointer new_node)
{
node=new_node;
first=*new_node;
last=first+difference_type(buffer_size());
}
cur:表示的是迭代器当前指向的元素
因此deque中一个迭代器的大小为16
而一个deque对象本身大小是16(start迭代器)+16(finish迭代器)+4(map二级指针)+4(整型map_size)=40.不管其内部有多少元素,这些元素所占的空间是动态获得的,与对象本身没有关系。
deque与vector的区别
- deque可以实现在常数时间内对起头端进行元素的插入或移除操作
2. deque没有容量概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。
注意:deque的迭代器不是普通指针,因此如非必要,我们应尽可能选择使用vector而非deque
如:对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector上,利用STL算法排好序后,再复制回deque。
deque的重载运算符,前自增:
self& operator++()//前++
{
++cur;//切换至下一个元素
if(cur==last)//如果已经达到所在缓冲区的尾端
{
set_node(node+1);//则切换到管控中心的下一个缓冲区
cur=first;
}
return *this;
}
self& operator--()//前--
{
if(cur==first)
set_node(node-1);
cur=last;}
--cur;//切换至前一个元素
return *this;}
self operator++(int)//后++
{
self tmp=*this;
++*this;
return tmp;
}
self operator--(int)//后--
{
self tmp=*this;
--*this;
return tmp;}
deque的迭代器失效情况:
1、在deque容器首部或者尾部插入元素不会使得任何迭代器失效。//但通过vs2012测试不管前端插入还是后端插入,都会使迭代器 失效
2、在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
3、在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。
deque插入元素时,内存管理分析:push_front
如果在头部插入一个元素,则会先比较start迭代器的内容,即start.cur 和start.first是否相等,相等说明第一缓冲区没有备用空间,所以会调用push_front_axu()函数申请一块缓冲区,在中控器中对应的指针会指向它,当然也会改变start迭代器node项的指向。假设插入的是99,即ideq.push_front(99),如下图所示
删除元素:
删除元素和插入元素类似,在次不再赘述。值得注意的是清除中间位置的元素时,erase()函数内部也会先判断清除位置之前的元素和清除位置之后的元素的大小。会取元素少的方向进行元素的移动。
deque的元素操作:pop_back、pop_front、clear、erase、insert与以上list和vector用法相同
注意:deque的最初状态(无任何元素时)有一个缓冲区、
因此在用clear()清除了整个deque后,也会保留一个缓冲区
最后附上deque的类函数表