chapter4 序列式容器:deque

1 deque概述

vector是单向开口的连续线性空间,deque则是一种双向开口连续线性空间。所谓双向开口,意思是可以再头尾两端分别做元素的插入和删除操作。

与vector的区别:

  1. deque允许于常数时间内对其头端进行元素的插入或移除操作
  2. deque没有所谓容量概念,它是以动态地分段连续空间组合而成的,随时可以增加一段新的空间并链接起来。

虽然deque也提供Random Access Iterator,但它的迭代器并不是普通指针。除非必要,尽可能选择vector而非deque。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector上,将vector排序后(利用STL sort算法),再复制回deque。

2 deque的中控器

deque由一段一段的定量连续空间组成。deque的最大任务便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置,复制,释放”的轮回,代价则是复杂的迭代器架构。

分段连续线性空间,必须要有中央控制,为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为复杂!!

deque采用一块所谓的map(并非STL map容器)作为主控,这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区.

template<class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public:	//basic types
    typedef T value_type;
    typedef value_type* pointer;
    ...
protected:	//internal typedefs
    //元素的指针的指针(pointer of pointer of T)
    typedef pointer* map_pointer;
    
protected:	//data members
    map_pointer map;	//指向map,map是块连续空间,其内的每个元素都是一个指针(称为节点),指向一块缓冲区
    size_type map_size; //map内科可容纳多少指针
	...
}

在这里插入图片描述

3 deque的迭代器

3.1 迭代器结构

deque是分段连续空间,维持其“整体连续”假象的任务,落在了迭代器的operator++operator--两个运算子上。

deque迭代器应具备的结构:

  1. 能够指出分段连续空间(缓冲区)在哪里
  2. 能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退就必须跳跃至下一个或上一个缓冲区
template<class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {	//未继承std::iterator
    typedef __deque_iterator<T, T&, T*, BufSize> iterator;
    typedef __deque_iterator<T, const T&, const T*, BufSize> const_iterator;
    staticsize_tbuffer_size() { return __deque_buf_size(BufSiz, sizeof(T)); }
    
    //未继承std::iterator,所以必须自行撰写五个必要的迭代器相应型别
    typedef random_access_iterator_tag iterator_category;
    typedef T value_type;
    typedef ptr pointer;
    typedef Ref reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
	typedef T** map_pointer;
    
    typedef __deque_iterator self;
    
    //保持与容器的联结
    T* cur; //此迭代器所指之缓冲区中的现行元素
    T* first;	//此迭代器所指之缓冲区的头
    T* last;	//此迭代器所指之缓冲区的尾(含备用空间)
    map_pointer node;	//指向管控中心
	...
}

其中用来决定缓冲区大小的函数buffer_size(),调用__deque_buf_size(),后者是一个全局函数,定义如下:

//如果n不为0,传回n,表示buffer size由用户自定义
//如果n为0,表示buffer size使用默认值,那么
//	如果sz(元素大小,sizeof(value_type))小于512,传回512/sz
//	如果sz不小于512,传回1
inline size_t __deque_buf_size(size_t n, size_t sz) {
    return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
}

在这里插入图片描述
假设产生一个元素型态为int,缓冲区大小为8(个元素)的deque,语法形式为deque<int, alloc, 8>

经过某些操作后,deque拥有20个元素,那么其begin()和end()所传回的迭代器如图所示:

在这里插入图片描述

3.2 迭代器操作

迭代器指针运算需要尤其小心,一旦行进遇到缓冲区边缘,则视前进或后退而定,可能需要跳一个缓冲区:

void set_node(map_pointer new_node) {
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
}
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--() {
    if(cur == first) {	//如果已达所在缓冲区的头端
    	set_node(node - 1);	//就切换值前一节点(亦即缓冲区)
        cur = last;		//的最后一个元素(的下一位置)
    }
    --cur;
    return *this;
}
self operator--(int) {
	self tmp = *this;
    --*this;
    return tmp;
}

除此之外,在实现随机存取(迭代器可以直接跳跃n个距离),也需要判断目标位置是否在同一个缓冲区内:

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;
}

4 deque的数据结构

deque需要维护:

  1. 一个指向map的指针
  2. start,finish两个迭代器
  3. 目前的map大小
template<class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public:	//basic types
    typedef T value_type;
    typedef value_tpe* pointer;
    typedef size_t size_type;
    
public:	//iterators
    typedef __deque_iterator<T, T& T*, BufSiz> iterator;
    
protected:
    //元素的指针的指针
    typedef pointer* map_pointer;
    
protected:	//Data members
    iterator start;	//第一个节点
    iterator finish;//最后一个节点
    
    map_pointer map;	//指向map,map是块连续空间,其每个元素都是个指针,指向一个节点(缓冲区)
    
    size_type map_size; //map内有多少指针
...
}

5 deque的构造与内存管理

5.1 deque的构造

deque<int, alloc, 8> ideq(20, 9);

其缓冲区大小为8(个元素),并令其保留20个元素空间,每个元素初值为9,为了指定deque的第三个template参数(缓冲区大小),根据C++语法规则,必须将前两个参数都指明出来,因此必须明确指定alloc为空间配置器。

deque自行定义了两个专属的空间配置器:

protected:
	//专属空间配置器,每次配置一个元素大小
	typedef simple_alloc<value_type, Alloc> data_allocator;
    //专属空间配置器,每次配置一个指针大小
    typedef simple_alloc<pointer, Alloc> map_allocator;

并提供有一个constructor:

deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {
    fill_initialize(n, value);	//负责产生并安排好deque的结构,并将元素的初值设定妥当
}

fill_initialize()负责产生并安排好deque的结构,并将元素的初值设定妥当

template<class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::fill_initialize(size_type n, constvalue_type& value) {
    create_map_and_node(n);		//把deque的结构都产生安排好
    map_pointer cur;
    __STL_TRY {
    	//为每个节点的缓冲区设定初值,最后一个节点的设定稍有不同
        ...
    }
    catch(...) {
        ...
    }
}

create_map_and_node()负责产生并安排好deque的结构:

template<class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::create_map_and_node(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);
    map = map_allocator::allocator(map_size);
    //以上配置出一个“具有map_size个节点”的map
    
    //以下令nstart和nfinish指向map所拥有之全部节点的最中央区段
    //保持在最中央,可使头尾两端的扩充能量一样大。每个节点可对应一个缓冲区
    map_pointer nstart = map + (map_size - num_nodes) / 2;
    map_pointer nfinish = nstart + num_nodes - 1;
    
    map_point cur;
    __STL_TRY {
        //为map内的每个现有节点配置缓冲区。所有缓冲区加起来就是deque的可用空间(最后一个缓冲区可能有一些余裕)
        for(cur = nstart; cur <= nfinish; ++cur) 
            *cur = allocate_node();
    }
    catch(...) {
    	//commit or rollback 语义:若非全部成功,则一个不留
        ...
    }
    
    //为deque内的两个迭代器start和end设定正确内容
    start.set_node(nstart);
    finish.set_node(nfinish);
    start.cur = start.first;
    finish.cur = finish.first + num_elements % buffer_size();
    //如果刚好整除,会多分配一个节点,即令cur指向这多分配的一个节点(所对应之缓冲区)的起始处
}

5.2 push_back()与push_front()

1.在尾端新增元素时,如果最后缓冲区尚有两个(含)以上的元素备用空间,则直接在备用空间上构造元素;若尾端备用元素空间不足,则push_back()会调用push_back_aux(),先配置一整块新的缓冲区,再设妥新元素内容,然后更改迭代器finish的状态。

在这里插入图片描述2. 在最前端新增元素时,若第一缓冲区尚有备用空间,则之间在备用空间上construct();若第一缓冲区已无备用空间,则需要调用push_front_aux()
在这里插入图片描述

5.3 map的重新整治

push_back_aux()push_front_aux()函数的定义中都提到若符合某种条件必须重换一个map。这是因为当map的尾端或前端的节点备用空间不足时,需要配置更大的map以进行替代。这个问题的判断由reserve_map_at_back()reverse_map_at_front(),实际操作由reallocate_map()执行:

void reverse_map_at_back(size_type nodes_to_add = 1) {
    if(nodes_to_add + 1 > map_size - (finish.node - map))
        //如果map尾端的节点备用空间不足
        //符合以上条件则必须重新换一个map(配置更大的,拷贝原来的,释放原来的)
        reallocate(nodes_to_add, false);
}

void reverse_map_at_front(size_type nodes_to_add = 1) {
    if(nodes_to_add > start.node - map)
        //如果map尾端的节点备用空间不足
        //符合以上条件则必须重新换一个map(配置更大的,拷贝原来的,释放原来的)
        reallocate(nodes_to_add, true);
}

6 deque的元素操作

6.1 pop_back()和pop_front()

所谓pop,是将元素拿掉。无论从deque的最前端或最尾端取元素,都需考虑在某种条件下,将缓冲区释放掉

pop_back()为例:

void pop_back() {
    if(finish.cur != finish.first) {
        //最后缓冲区有一个(或更多)元素
        --finish.cur;	//调整指针,相当于排除了最后元素
        destory(finish.cur);	//将最后元素析构
    }
    else
        //最后缓冲区没有任何元素
        pop_back_aux();	//此处将进行缓冲区的释放工作
}

//只有当finish.cur == finish.first时才会被调用
template<class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::pop_back_aux() {
    deallocate_node(finish.first);	//释放最后一个缓冲区
    finish.set_node(finish.node - 1); //调整finish的状态,使指向上一个缓冲区的最后一个元素
    finish.cur = finish.last - 1;
    destory(finish.cur); 	//将该元素析构
}

6.2 clear()

clear()用于清楚整个deque。需要注意的是,deque的最初状态(无任何元素时)保有一个缓冲区,因此,clear()完成之后恢复初始状态,也一样要保留一个缓冲区。

具体的实现方法(p164):

  1. 针对头尾以外的每个缓冲区,将此类缓冲区的所有元素析构并释放缓冲区内存
  2. 对于头尾两个缓冲区,将头尾缓冲区的所有元素析构,并释放尾缓冲区,但是保留头缓冲区
  3. 若只有一个缓冲区,则析构该缓冲区内所有元素,但不释放
  4. 最后,调整迭代器状态

6.3 erase()

两种版本,一种用于清除某个元素,一种用于清除[first, last)区间内的所有元素:

//清除pos所指的元素
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;
}

//清除[first, last)区间内的所有元素
template<class T, class Alloc, size_t BufSiize>
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) >> 1) {  		//如果前方的元素比较少
            copy_backward(start, first, last); 			//向后移动前方元素(覆盖清除区间)
            iterator new_start = start + n; 			//标记deque的新起点
            destroy(start, new_start);
            //以下将冗余的缓冲区释放
            for (map_pointer cur = start.node; cur < new_start.node; ++cur)
                    dara_allocator::deallocate(*cur, buffer_size());
            start = new_start;      //设定deque的新起点
        }
        else {  							//如果清除区间后方的元素比较少
            copy(last, finish, first); 		//向前移动后方元素(覆盖清除区间)
            iterator new_finish = finish - n;  //标记deque的新尾点
            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;    		//设定deque的新尾点
        }
        return start + elems_before;
    }
}

6.4 insert()

以下insert()函数允许在某个点(之前)插入一个元素,并设定其值:

//在position处插入一个元素,其值为x
iterator insert(iterator position, const value_type& x) {
    if(position.cur == start.cur) {	//如果插入点是deque最前端
    	push_front(x);				//交给push_front()
        return start;
    }
    else if(position.cur == finish.cur) { //如果插入点是deque最尾端
        push_back(x);					  //交给push_back()
        iterator tmp = finish;
        --tmp;
        return tmp;
    }
    else {
        return insert_aux(position, x);
    }
}

辅助插入函数insert_aux()

iterator insert_aux(iterator pos, const value_type& x) {
    difference_type index = pos - start;	//插入点之前的元素个数
    value_type x_opy = x;
    if(index < (size() >> 1)) {	//如果插入点之前的元素较少
        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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值