0
点赞
收藏
分享

微信扫一扫

deque

钵仔糕的波波仔 2022-02-24 阅读 116

deque

首先我们还是要讲讲deque与vector的不同,我们称deque为双端队列,理由是他可以同时在容器的结尾和开头进行O(1)的插入删除操作。

正是这一差别导致deque在内部于vector有着很大的不同。vector在扩增的时候时常不停的申请内存复制移动,但是deque却不存在这样的操作。这一行为让deque在某方面又和list有着惊人的相似,所以deque没有扩容的概念,因为他们内部实现也是向list一样,各个元素是连接起来的。

但是,deque却又允许O(1)的访问这又deque和list之间有了差别,也就是说deque内部的迭代器是随机访问迭代器。

为了让deque又如上的特性,STL内部不得不让deque的实现变得很复杂而且运行起来又相当耗费时间,所以deque的效率有时候会不如vector。什么时候deque是程序员们值得考虑的一件事情。


内部结构

依旧,我们打开deque的容器看看他包含着什么,于是我们再次发现了我们的老朋友

 _Compressed_pair<_Alty, _Scary_val> _Mypair;

这东西我们见过好多次了,都知道他里面存着对控制容器至关重要的数据。而这些数据,存储在_Deque_val中,也是_Scary_val别名过去的东西。

_Deque_val内部:

private:
    static constexpr size_t _Bytes = sizeof(value_type);
public:
    // elements per block (a power of 2)
    static constexpr int _Block_size = _Bytes <= 1 ? 16
                                     : _Bytes <= 2 ? 8
                                     : _Bytes <= 4 ? 4
                                     : _Bytes <= 8 ? 2
                                                   : 1; 
    _Mapptr _Map; // pointer to array of pointers to blocks
    size_type _Mapsize; // size of map array, zero or 2^N
    size_type _Myoff; // offset of initial element
    size_type _Mysize; // current length of sequence

deque使用一个指针数组用来掌控他的所有元素,这个数组,就是_Map。注意,这个_Map是一个二级指针,因为它是指向指针数组的指针,在_Deque_simple_types中,我们能之间看到它的定义:

template <class _Ty>
struct _Deque_simple_types : _Simple_types<_Ty> {
    using _Mapptr = _Ty**;
};

这个数组的用途是以下图示所指出的。

在这里插入图片描述

map中的每个指针指向一个缓冲区,那里存着deque中真正的数据。想必你已经明白为什么deque的可以如此方便在开头和结尾的插入,同时他又像list一样不需要容量的概念。

下面是_Deque_val中各元素的解释:

  • _Map:指向map指针数组。
  • _Mapsize:指出了_Map数组的大小,它固定是2^n的数量。
  • _Myoff:指出了deque中第一个元素的偏移量。
  • _Mysize:deque容器目前的大小,即元素的数量。

之后我会详细解释map数组的扩容。

有了大致的实现我们就可以想想怎么去访问了。deque要求随机访问,就像数组的行为一样。这个责任就落在deque的iterator上。

iterator重载了一系列运算符,我挑一些重点的来说。

_NODISCARD reference operator*() const noexcept {
    _Size_type _Block = _Mycont->_Getblock(_Myoff);
    _Size_type _Off   = _Myoff % _Block_size;
    return _Mycont->_Map[_Block][_Off];
}

这个是iterator重载的*运算符,可以看到它最终是通过_Map[_Block][_Off]来获取我们需要的元素的,这个_Block通过_Myoff算出,_Off通过求余得到。

注意:每一个iterator里面都有一个_Myoff,指出了该迭代器的偏移量。

下面是_Getblock的内部实现:

return (_Off / _Block_size) & (_Mapsize - 1);

是的,它只有一行代码,但是却有着相当大的作用,之后在deque扩增的时候我们会再次提起它。

重载了operator*()之后,只需要一个_Myoff就能够返回我们需要的元素,这样,其他迭代器增加减少的操作只需要控制迭代器内部的_Myoff即可。比如operator++(int)

_Deque_unchecked_const_iterator operator++(int) noexcept {
    _Deque_unchecked_const_iterator _Tmp = *this;
    ++_Myoff;
    return _Tmp;
}

接下来我们从一个比较特殊的头部插入开始,深入了解deque内部的运作机制。

现在假设,map的size为8,即8个缓冲区指针,假设我们用该deque存储char类型的数据,那么内部每个缓冲区能存16个char数据。

map一开始前后可能会留有空间以供用户插入使用。现在我们不停地在前面插入元素,map会移动自己_Myoff同时向前申请新的缓冲区。但是这样地操作总会有个头,假如_Myoff==_Map怎么办,前面没有空间可以插入了。

这种情况就是我们要需要的特殊地头部插入,我们也会就这种情况深入讨论。

首先我们来看看push_front()

void push_front(const _Ty& _Val) {
    emplace_front(_Val);
}

调用emplace_front()

template <class... _Valty>
decltype(auto) emplace_front(_Valty&&... _Val) {
    _Orphan_all();

    if (_Myoff() % _Block_size == 0 && _Mapsize() <= (_Mysize() + _Block_size) / _Block_size) {
        _Growmap(1);
    }
    _Myoff() &= _Mapsize() * _Block_size - 1;
    size_type _Newoff = _Myoff() != 0 ? _Myoff() : _Mapsize() * _Block_size;
    size_type _Block  = _Getblock(--_Newoff);
    if (_Map()[_Block] == nullptr) {
        _Map()[_Block] = _Getal().allocate(_Block_size);
    }

    _Alty_traits::construct(
        _Getal(), _Unfancy(_Map()[_Block] + _Newoff % _Block_size), _STD forward<_Valty>(_Val)...);

    _Myoff() = _Newoff;
    ++_Mysize();

#if _HAS_CXX17
        return front();
#endif // _HAS_CXX17
}

我们的第一步就是研究这个:

if (_Myoff() % _Block_size == 0 && _Mapsize() <= (_Mysize() + _Block_size) / _Block_size) {
    _Growmap(1);
}

_Myoff()_Deque_val::_Myoff的引用,它指出了第一个元素的位置。对于第一个判断条件:

_Myoff() % _Block_size == 0

代表着第一个元素的位置正好处在某一个缓冲区的开头,而第二个条件则是查看deque中元素是否还留有最后一个空闲的位置。
之所以这样做……后面的步骤给我们了答案。

假设我们8个缓冲区占了6个多,比如目前deque中存在100个char,而前面第一个元素已经到头了。

那么根据判断(_Mysize() + _Block_size) / _Block_size,得(100+16)/16 = 7 <= 8
所以在map的结尾,目前还有一个缓冲区未被使用,于是程序决定跳过_Growmap(1),暂时不分配新的map空间。
于是程序来到下面

//_Myoff在这一步不会改变
_Myoff() &= _Mapsize() * _Block_size - 1;
//_Myoff如果不等于0,那么在该缓冲区内前面一定还有空间,于是让_Newoff暂时等于_Myoff
//_Myoff如果等于0,那么第一个元素就在整个map的开头,前面没有任何空间,于是_Newoff被分配到map后面的空间
size_type _Newoff = _Myoff() != 0 ? _Myoff() : _Mapsize() * _Block_size;
//获得_Newoff对应的map_block,在我们例子中,它目前是7。
size_type _Block  = _Getblock(--_Newoff);

因为_Newoff跑到了后面,所以整个map在被填满之前能够充分的利用。
这个时候你可能会想,第一个元素跑到了map中第7块缓冲区第15个char空间,假如我们要访问deque[1]、deque[2]该怎么办?

模拟一下,假如我们访问deque[2]

_NODISCARD reference operator[](size_type _Pos) noexcept /* strengthened */ {
    return *(_Unchecked_begin() + static_cast<difference_type>(_Pos));
}

operator[]使用begin()+pos的方式找到我们需要的元素,现在begin()应该返回_Newoff位置的迭代器,然后+2,这样想的话,相加后的迭代器应该超出范围了才是。
我们看看重载的operator+()

_NODISCARD _Deque_unchecked_const_iterator operator+(const difference_type _Off) const noexcept {
    _Deque_unchecked_const_iterator _Tmp = *this;
    _Tmp += _Off;
    return _Tmp;
}

好吧,他又调用了operator+=(),我省略之间各种调用,只展示最终调用的函数。

_Deque_unchecked_const_iterator& operator+=(const difference_type _Off) noexcept {
    _Myoff += _Off;
    return *this;
}

可以看到,迭代器相加最终代表着是迭代器内部的_Myoff相加,deque[2]我们相加后的迭代器里面的_Myoff就应该是16*8-1+2 = 129,还是超出了范围。
于是来到最后一步——*this
我们再次回到了operator*(),还记得吗?它内部是通过_Getblock()获得_Myoff对应的map block,我再次展示出他的实现:

return (_Off / _Block_size) & (_Mapsize - 1);

我们手动计算一下
目前_Off是129,_Off/_Block_size = 129/16=8,8&(_Mapsize-1)=8&7
也就是1000&0111 = 0
于是我们得到了_Off = 129对应的block——0,也就是第一块,然后operator*()后面还有个求余的操作
129%16 = 1
于是我们最终得到了deque[2]的位置——_Map[0][1],答案是正确的。
可以看到,虽然我们第一个元素跑到了map的最后面,但是对于用户来说,deque的行为是正常的。

我们回到emplace_front()

...
//查看插入的_Block是否为空,我们的例子中,最后一个map block没被分配,所以是空的
if (_Map()[_Block] == nullptr) {
    //如果是空的,分配缓冲区空间
    _Map()[_Block] = _Getal().allocate(_Block_size);
}
//在缓冲区上构造插入的元素
_Alty_traits::construct(
    _Getal(), _Unfancy(_Map()[_Block] + _Newoff % _Block_size), _STD forward<_Valty>(_Val)...);
//更新必要的东西
_Myoff() = _Newoff;
++_Mysize();

整个插入的过程就是这样,如果map的前面还存在空间,就直接在前面申请缓冲区然后插入,如果没有,就看map后面是否还存在空间,如果有,就在后面空间插入。

那假如……前后都没有呢?比如说现在,我们的例子第一个元素已经跑到map的最后面了,如果继续插入,那么头尾早晚会向连在一起,这样就再也没有空间让我们去插入了。

而这个时候,那个超长的if语句就会生效,执行_Growmap(1)

接下来,我们看看map是如何扩增的?当然,例子还是这个例子。

重新说一下,目前我们的map有8个block,现在里面前7个block已经满,最后一个block的最后面存储的整个deque的第一个元素。

当我们再次执行头插入操作,会导致map扩增。

void _Growmap(size_type _Count) { // grow map by at least _Count pointers, _Mapsize() a power of 2
    static_assert(1 < _Minimum_map_size, "The _Xlen() test should always be performed.");

    _Alpty _Almap(_Getal());
    size_type _Newsize = 0 < _Mapsize() ? _Mapsize() : 1;
    while (_Newsize - _Mapsize() < _Count || _Newsize < _Minimum_map_size) {
        // scale _Newsize to 2^N >= _Mapsize() + _Count
        if (max_size() / _Block_size - _Newsize < _Newsize) {
            _Xlen(); // result too long
        }
        //每次map称2倍扩增
        _Newsize *= 2;
    }
    _Count = _Newsize - _Mapsize();
    //_Myboff获取第一个元素对应的block偏移量,我们例子中,它是7
    size_type _Myboff = _Myoff() / _Block_size;
    _Mapptr _Newmap   = _Almap.allocate(_Mapsize() + _Count);
    //_Myptr指出了new map中我们第一个元素的位置
    _Mapptr _Myptr    = _Newmap + _Myboff;
    //这里是old map复制到new map的步骤

    //根据参数,这一次的复制时从我们的第一个元素到old map的结尾,在我们的例子中,他只复制一个元素deque[0]
    _Myptr = _STD uninitialized_copy(_Map() + _Myboff, _Map() + _Mapsize(), _Myptr); 
    if (_Myboff <= _Count) { // increment greater than offset of initial block
        //这一步是将Old map中从_Map开头一直到第一个元素位置,在我们的例子中为deque[1]-deque[111]
        //注意到它是从_Myptr之后开始复制的,所以复制完毕后我们的deque序列在map内部排序又变回了正常
        _Myptr = _STD uninitialized_copy(_Map(), _Map() + _Myboff, _Myptr); 
        //把后面的空间清理
        _Uninitialized_value_construct_n_unchecked1(_Myptr, _Count - _Myboff); // clear suffix of new
        //清理前面的空间
        _Uninitialized_value_construct_n_unchecked1(_Newmap, _Myboff); // clear prefix of new
    } else { // increment not greater than offset of initial block
        _STD uninitialized_copy(_Map(), _Map() + _Count, _Myptr); // copy more old
        _Myptr = _STD uninitialized_copy(_Map() + _Count, _Map() + _Myboff, _Newmap); // copy rest of old
        _Uninitialized_value_construct_n_unchecked1(_Myptr, _Count); // clear rest to initial block
    }
    
    _Destroy_range(_Map() + _Myboff, _Map() + _Mapsize());
    if (_Map() != _Mapptr()) {
        _Almap.deallocate(_Map(), _Mapsize()); // free storage for old
    }

    _Map() = _Newmap; // point at new
    _Mapsize() += _Count;
}

到这里,我们对deque有了大致的了解,熟悉了内部map的运作方式,但是deque内部实现还有许许多多的细节。之后等我搞明白了再说吧……

举报

相关推荐

0 条评论