您的位置:首页 > 编程语言 > C语言/C++

C++ 标准模板库(STL)_序列式容器—— deque(模拟连续空间)(侯捷老师)

2020-07-15 04:43 218 查看

STL—— deque

  • 7、push_front()、push_back()源码
  • 8、pop_front() 、pop_back()源码
  • 9、deque::insert()源码
  • 10、deque如何模拟连续空间
  • 11、deque常见操作和应用
  • 参考
  • 双端队列——deque

    #include<deque>

    1、定义


    双端队列:一种双向开口的 “连续” (分段连续线性)数据空间,支持高效地头尾两端分别做元素的插入和删除操作,同时能够快速地随机访问任一个元素。
    注意:
    vector也可以在头尾两端进行操作,但是其头部操作时间复杂度高,需要后继元素往后移动,所以标准库没有为vector提供push_front或pop_front操作

    2、特点

    • 1、支持两端进行push和pop,时间复杂度O(1);
    • 2、内部进行插入insert,时间复杂度O(n);
    • 3、支持随机访问,时间复杂度O(1);

    以下特点后文进行具体说明:

    • 4、双向开口的 分段连续线性内存空间(每次++,–都需要判断当前buffer是否用完,若用完则需要通过控制中心跳到下一个buffer)。
    • 5、deque没有容量(capacity)的概念,因为它是动态地以分段连续空间组合而成的,随时可以增加一段新空间并链接起来。不会像vector那样“因旧空间不够而新配置一个更大空间,然后复制元素,再释放旧空间”
    • 6、控制中心的map是指向vector的,以后也是2倍增长。(map左右两边一般留有剩余空间,用于前后插入元素)
      注意:这里是map不是STL中的map

    3、deque节点结构(数据组织形式)


    参数说明:

    • node 所指内容是一个指针。指向某个缓冲区的起始位置;
    • first=*(node);
    • last=first+node_size(即buffer_size()) ;
    • cur是一个指针。指向[first last)之间;
    • start:起始缓冲区,deque::begin() 传回迭代器start,start内的cur指针指向缓冲区的第一个元素;
    • finish:最后一个缓冲区,deque::end() 传回迭代器finish,finish内的cur指向缓冲区的最后元素(的下一个位置)

    结构说明:

    • 1、deque由一段一段定长的连续空间(buffer)所链接而成,一旦需要在deque的前端或尾端增加新空间,便配置一段定量的连续空间,并将该空间串接在deque的头部或尾部。(上图中五根长条
    • 2、采用一块map作为主控,map是一块小的连续空间,其中每个元素都是指针,指向一块较大的线性连续空间(buffer)。
    • 3、map本身也是一块固定大小的连续空间,当缓冲区数量增多,map容不下更多的指针时,deque会寻找一块新的空间来作为map。

    deque的迭代器必须有这样的能力:它必须能够指出分段连续空间在哪里,判断自己所指的位置是否位于某一个缓冲区的边缘,如果位于边缘,则执行operator-- 或operator++时要能够自动跳到下一个缓冲区。因此deque的迭代器也是Ramdon Access Iterator 迭代器

    4、deque数据结构部分源码定义

    template <class T, class Alloc = alloc, size_t BufSiz = 0>
    class deque {
    public:                         // Basic types
    typedef T value_type;
    typedef value_type* pointer;
    typedef const value_type* const_pointer;
    typedef value_type& reference;
    typedef const value_type& const_reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    typedef __deque_iterator<T, T&, T*, BufSiz>              iterator;
    
    protected:                      // Internal typedefs
    
    typedef pointer* map_pointer;//T**
    
    protected:                      // Data members
    iterator start;               // 起始缓冲区
    iterator finish;              // 最后一个缓冲区
    
    //指向map,map是一个T**,也就是一个指针,其所指之物又是指针,
    //指向型别为T的一块空间。map是块连续空间,其内的每个元素
    //都是一个指针(称为节点),指向一个缓冲区
    map_pointer map;
    size_type map_size;   // map容量,有多少个指针
    
    public:                         // Basic accessors
    iterator begin() { return start; }
    iterator end() { return finish; }
    size_type size() const { return finish - start;; }
    ...

    5、初始化map部分源码

    // 每次配置一个元素大小的配置器
    typedef simple_alloc<value_type, Alloc> data_allocator;
    // 每次配置一个指针大小的配置器
    typedef simple_alloc<pointer, Alloc> map_allocator;
    
    template <class T, class Alloc, size_t BufSize>
    void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements) {
    // 需要分配的结点数  如果为能整除 则多分配多一个结点
    size_type num_nodes = num_elements / buffer_size() + 1;
    
    // 分配结点内存 (前后预留一个 用于扩充)
    map_size = max(initial_map_size(), num_nodes + 2);
    map = map_allocator::allocate(map_size);
    
    // 将需要分配缓冲区的结点放在map的中间
    map_pointer nstart = map + (map_size - num_nodes) / 2;
    map_pointer nfinish = nstart + num_nodes - 1;
    
    map_pointer cur;
    // 为了简化 去掉了异常处理的代码
    for (cur = nstart; cur <= nfinish; ++cur)
    *cur = allocate_node(); // 为每个结点分配缓冲区
    }
    
    // 设置start、finish指针
    start.set_node(nstart);
    finish.set_node(nfinish);
    start.cur = start.first;
    finish.cur = finish.first + num_elements % buffer_size();
    }

    6、__deque_iterator 源码定义

    template <class T, class Ref, class Ptr, size_t BufSiz>
    struct __deque_iterator {
    // 基本型别的定义
    typedef __deque_iterator<T, T&, T*, BufSiz>             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;
    
    // 缓冲区的大小
    tatic size_t buffer_size() { ... }
    
    // 主要维护的三个指针
    T* cur;    // 指向当前元素
    T* first;   // 指向当前缓冲区的头
    T* last;   // 指向当前缓冲区的尾
    
    map_pointer node; // 指向当前缓冲区在map中的位置
    // ...
    };

    deque的迭代器应该具备什么结构:

    • 首先,能够指出分段连续空间(亦即缓冲区)在哪里;
    • 其次,够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退就必须跳跃至下一个或上一个缓冲区;
    • 最后,为了能够正确跳跃,deque必须随时掌握管控中心(map)。所以需要以下几个指针:
    // 保持与容器的联结
    T* cur;       // 此迭代器所指之缓冲区中的现行元素
    T* first;     // 此迭代器所指之缓冲区的头
    T* last;      // 此迭代器所指之缓冲区的尾(含备用空间)
    map_pointer node; //指向管控中心

    6.1、迭代器失效问题

    • 1、在deque容器首部或者尾部插入元素不会使得任何迭代器失效。
    • 2、在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
    • 3、在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。

    7、push_front()、push_back()源码

    void push_front(const value_type& t) {
    if (start.cur != start.first) {    // 第一缓冲区还有容量
    construct(start.cur - 1, t);
    --start.cur;
    }
    else
    push_front_aux(t);
    }
    
    // 如果第一缓冲区容量不足会调用这个函数来配置新的缓冲区
    template <class T, class Alloc, size_t BufSize>
    void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t) {
    value_type t_copy = t;
    reserve_map_at_front();   // 可能导致map的重新整治
    *(start.node - 1) = allocate_node();
    start.set_node(start.node - 1);
    start.cur = start.last - 1;
    construct(start.cur, t_copy);
    }
    
    // 根据map前面为分配的结点数量来判断是否需要重新整治
    void reserve_map_at_front (size_type nodes_to_add = 1) {
    if (nodes_to_add > start.node - map)
    reallocate_map(nodes_to_add, true);
    }
    void push_back(const value_type& t)
    {
    // STL使用前闭后开的区间, 所以如果缓冲区还有一个以上的备用空间,
    // 则直接在finish.cur上构造对象即可, 然后更新迭代器
    if (finish.cur != finish.last - 1) {
    construct(finish.cur, t);
    ++finish.cur;
    }
    // 容量已满就要新申请内存了
    else
    push_back_aux(t);
    }
    
    ```csharp
    void pop_front() {
    if (start.cur != start.last - 1)
    {
    destroy(start.cur);
    ++start.cur;
    }
    else
    pop_front_aux();
    }

    8、pop_front() 、pop_back()源码

    void pop_front() {
    if (start.cur != start.last - 1) {
    destroy(start.cur);
    ++start.cur;
    }
    else
    pop_front_aux();
    }
    
    // 当前缓冲区只剩一个元素
    template <class T, class Alloc, size_t BufSize>
    void deque<T, Alloc, BufSize>::pop_front_aux() {
    destroy(start.cur);
    deallocate_node(start.first);  // 释放该缓冲区
    start.set_node(start.node + 1);
    start.cur = start.first;
    }
    void pop_back()
    {
    if (finish.cur != finish.first) {
    //如果最后一个缓冲区有一个或多个元素
    --finish.cur; //调整指针,相当于排除最后元素
    destroy(finish.cur); //将最后元素析构
    }
    else //最后缓冲区没元素
    pop_back_aux();  //进行缓冲区的释放
    }

    9、deque::insert()源码

    iterator insert(iterator position, const value_type& x) {
    if (position.cur == start.cur) {
    // 插入位置为begin()
    push_front(x);
    return start;
    }
    else if (position.cur == finish.cur) {
    // 插入位置为end()
    push_back(x);
    iterator tmp = finish;
    --tmp;
    return tmp;
    }
    else {
    // 如果插入位置是在(begin(), end())
    return insert_aux(position, x);
    }
    }
    
    // insert_aux()跟erase()实现类似
    // 调用copy()或者copy_backward()将元素前移或者后移
    // 然后修改原来位置的值
    
    //指定位置插入一个元素
    template <class T, class Alloc, size_t BufSize>
    typename deque<T, Alloc, BufSize>::iterator
    deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x) {
    //计算出插入点之前的元素个数
    difference_type index = pos - start;
    value_type x_copy = x;
    /* 要在非头/尾位置插入元素,对于线性空间而言,势必涉及到移动元素
    * 为了使得效率达到最优,需要尽量少移动元素
    * 所以接下来我们比较插入点前的元素个数和插入点之后的元素个数
    * 然后来选择移动哪一边的元素
    */
    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;
    }
    ...

    10、deque如何模拟连续空间



    deque是如何模拟连续空间的呢?全都是deque iterators的功劳,源代码做了大量的操作符重载,尤其是遇到buffer边界时如何跳到控制中心,使得deque的iterator能够在buffer之间模拟出连续空间。

    reference operator[] (size_type n)
    {
    return start[difference_type(n)];
    }
    reference front(){return *start;}
    reference back()
    {
    iterator tmp = finish;
    --tmp;
    return *tmp;
    }
    size_type size() const {return finish - start;}
    bool empty() const {return finish == start;}
    
    reference operator*() const {return *cur;}
    pointer operator->() const {return & (operator*());}
    
    // 判断两个迭代器间的距离,两根iterators之间的距离相当于
    //(1)两根iterators间的buffers的总长度+
    //(2)itr至其buffer末尾的长度+
    //(3)x至其buffer起头的长度
    difference_type operator-(const self& x) const
    {
    return difference_type(buffer_size())*(node-x.node-1)+
    (cur-first)+(x.last-x.cur);
    //首尾buffers之间的buffers数量
    //(cur-first)末尾(当前)buffer的元素量,(x.last-x.cur)起始buffer的元素量
    }
    
    //从外界看上去维护的是一段连续空间的关键
    ////////////////////////////////
    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;
    }
    
    self operator+(difference_type n) const
    {
    self tmp = *this;
    
    // 这里调用了operator +=()可以自动调整指针状态
    return tmp += n;
    }
    
    //  将n变为-n就可以使用operator +=()了,
    // 初等数学是神奇的, 还记得我们刚学编程时求绝对值是怎么写的吗? :P
    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) const { return cur == x.cur; }
    bool operator!=(const self& x) const { return !(*this == x); }
    bool operator<(const self& x) const {
    return (node == x.node) ? (cur < x.cur) : (node < x.node);
    }
    
    /*
    迭代器内对各种指针运算都进行了重载,
    所以各种指针运算如加、减、前进、后退等都不能直观视之。
    最关键的是:一旦遇到缓冲区边缘,要特别小心,
    视前进或后退而定,可能需要调用set_node()跳一个缓冲区
    */
    void set_node(map_pointer new_node)
    {
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
    }
    };

    11、deque常见操作和应用

    11.1 构造和赋值

    deque<Elem> c 创建一个空的deque
      deque<Elem> c1(c2) 复制一个deque。
      deque<Elem> c(n) 创建一个deque,含有n个数据,数据均已缺省构造产生。
      deque<Elem> c(n, elem) 创建一个含有n个elem拷贝的deque。
      deque<Elem> c(beg,end) 创建一个以[beg;end)区间的deque。
      
    operator=赋值运算符重载
    c.assign(n,num)将n个num拷贝复制到容器c
    c.assign(beg,end)将[beg,end)区间的数据拷贝复制到容器c

    案例:

    deque<int> d1 {1,2,3,4,5},d2;
    d2.assign(2, 8);
    deque<int>::iterator it;
    cout << "d2.assign(n,num):";
    for(it=d2.begin();it!=d2.end();it++){
    cout << *it << " ";
    }
    d2.assign(d1.begin(), d1.begin()+3);
    cout << "d2.assign(beg,end):";
    for(it=d2.begin();it!=d2.end();it++){
    cout << *it << " ";
    }
    cout << endl;

    11.2 定位和索引

    c.at(pos)返回索引为pos的位置的元素,会执行边界检查,如果越界抛出out_of_range异常
    
    c.operator[]下标运算符重载

    案例:

    deque<int> d {1,2,3,4,5};
    cout << "d.at(pos):" << d.at(2);
    cout << "d[2]:" << d[2];
    return 0;

    11.3 empty()、size()、front()、back()

    c.empty()判断c容器是否为空
    c.size()返回c容器中实际拥有的元素个数
    c.front()返回c容器的第一个元素
    c.back()返回c容器的最后一个元素
    deque<int> d {1,2,3,4,5};
    if(!d.empty()){
    cout << "d.size():" << d.size() << endl;
    cout << "d.front():" << d.front() << endl;
    cout << "d.back(): " << d.back() << endl;
    }

    11.4 插入和删除

    c.insert(pos,num)在pos位置插入元素num
    c.insert(pos,n,num)在pos位置插入n个元素num
    c.insert(pos,beg,end)在pos位置插入区间为[beg,end)的元素
    
    c.push_back(num)在末尾位置插入元素
    c.pop_back()删除末尾位置的元素
    c.push_front(num)在开头位置插入元素
    c.pop_front()删除开头位置的元素
    
    c.erase(pos)删除pos位置的元素c.erase(beg,end)删除区间为[beg,end)的元素
    c.erase(beg,end)删除区间为[beg,end)之间的元素
    c.clear()清除c容器中拥有的所有元素
    deque<int> d {1,2,3,4,5};
    deque<int>::iterator it;
    cout << "insert before:" ;
    for(it=d.begin();it!=d.end();it++){
    cout << *it << " ";
    }
    cout << endl;
    d.insert(d.end(),22);
    d.insert(d.end(), 3,88);
    int a[5] = {1,2,3,4,5};
    d.insert(d.begin(),a,a+3);
    cout << "insert after:" ;
    for(it=d.begin();it!=d.end();it++){
    cout << *it << " ";
    }
    cout << endl;
    
    d.push_back(10);
    d.pop_back();
    d.push_front(10);
    d.pop_front();
    
    d.erase(d.begin());
    deque<int>::iterator it;
    cout << "erase(pos) after:" ;
    for(it=d.begin();it!=d.end();it++){
    cout << *it << " ";
    }
    cout << endl;
    d.erase(d.begin(), d.begin()+3);
    cout << "erase(beg,end) after:" ;
    for(it=d.begin();it!=d.end();it++){
    cout << *it << " ";
    }
    cout << endl;
    
    d.clear();

    deque具体实现参考

    参考

    1、https://blog.csdn.net/m0_37264397/article/details/78671062
    2、https://www.cnblogs.com/runnyu/p/6000368.html
    3、https://blog.csdn.net/u013074465/article/details/44617629
    4、《STL的源码剖析》

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: