【问题标题】:STL deque accessing by index is O(1)?通过索引访问 STL 双端队列是 O(1)?
【发布时间】:2011-01-18 19:45:44
【问题描述】:

我已经读过,可以在 STL 双端队列中通过位置索引访问元素在恒定时间内完成。据我所知,双端队列中的元素可能存储在几个不连续的位置,从而消除了通过指针算法进行的安全访问。例如:

abc->defghi->jkl->mnop

上述双端队列的元素由单个字符组成。一组中的字符集表明它分配在连续的内存中(例如 abc 位于单个内存块中,defhi 位于另一个内存块中,等等)。谁能解释如何通过位置索引访问可以在恒定时间内完成,特别是如果要访问的元素位于第二个块中?还是双端队列有指向块组的指针?

更新:或者是否有任何其他常见的双端队列实现?

【问题讨论】:

    标签: c++ stl deque random-access


    【解决方案1】:

    我从Wikipedia 找到了这个双端队列实现:

    将内容存储在多个较小的数组中,分配额外的 根据需要将数组放在开头或结尾。索引由以下方式实现 保持一个包含指向每个较小的指针的动态数组 数组。

    我想它回答了我的问题。

    【讨论】:

    • 索引已实现”并没有真正解释它是如何工作的。
    【解决方案2】:

    deque 中的数据由固定大小的向量块存储,分别是

    map指向(这也是一个向量块,但它的大小可能会改变)

    deque iterator的主要部分代码如下:

    /*
    buff_size is the length of the chunk
    */
    template <class T, size_t buff_size>
    struct __deque_iterator{
        typedef __deque_iterator<T, buff_size>              iterator;
        typedef T**                                         map_pointer;
    
        // pointer to the chunk
        T* cur;       
        T* first;     // the begin of the chunk
        T* last;      // the end of the chunk
    
        //because the pointer may skip to other chunk
        //so this pointer to the map
        map_pointer node;    // pointer to the map
    }
    

    deque的主要部分代码如下:

    /*
    buff_size is the length of the chunk
    */
    template<typename T, size_t buff_size = 0>
    class deque{
        public:
            typedef T              value_type;
            typedef T&            reference;
            typedef T*            pointer;
            typedef __deque_iterator<T, buff_size> iterator;
    
            typedef size_t        size_type;
            typedef ptrdiff_t     difference_type;
    
        protected:
            typedef pointer*      map_pointer;
    
            // allocate memory for the chunk 
            typedef allocator<value_type> dataAllocator;
    
            // allocate memory for map 
            typedef allocator<pointer>    mapAllocator;
    
        private:
            //data members
    
            iterator start;
            iterator finish;
    
            map_pointer map;
            size_type   map_size;
    }
    

    下面我给大家deque的核心代码,主要大概两部分:

    1. 迭代器

    2. 如何随机访问deque实现

    1。迭代器(__deque_iterator)

    迭代器的主要问题是,当++,--迭代器时,它可能会跳到其他块(如果它指向块的边缘)。比如有三个数据块:chunk 1,chunk 2,chunk 3

    pointer1指向chunk 2的开头,当操作符--pointer指向chunk 1的结尾,从而指向pointer2

    下面我将给出__deque_iterator的主要功能:

    首先,跳到任意块:

    void set_node(map_pointer new_node){
        node = new_node;
        first = *new_node;
        last = first + chunk_size();
    }
    

    注意,chunk_size() 计算块大小的函数,你可以认为它返回 8 来简化这里。

    operator*获取chunk中的数据

    reference operator*()const{
        return *cur;
    }
    

    operator++, --

    //增量的前缀形式

    self& operator++(){
        ++cur;
        if (cur == last){      //if it reach the end of the chunk
            set_node(node + 1);//skip to the next chunk
            cur = first;
        }
        return *this;
    }
    
    // postfix forms of increment
    self operator++(int){
        self tmp = *this;
        ++*this;//invoke prefix ++
        return tmp;
    }
    self& operator--(){
        if(cur == first){      // if it pointer to the begin of the chunk
            set_node(node - 1);//skip to the prev chunk
            cur = last;
        }
        --cur;
        return *this;
    }
    
    self operator--(int){
        self tmp = *this;
        --*this;
        return tmp;
    }
    
    迭代器跳过 n 步/随机访问
    self& operator+=(difference_type n){ // n can be postive or negative
        difference_type offset = n + (cur - first);
        if(offset >=0 && offset < difference_type(buffer_size())){
            // in the same chunk
            cur += n;
        }else{//not in the same chunk
            difference_type node_offset;
            if (offset > 0){
                node_offset = offset / difference_type(chunk_size());
            }else{
                node_offset = -((-offset - 1) / difference_type(chunk_size())) - 1 ;
            }
            // skip to the new chunk
            set_node(node + node_offset);
            // set new cur
            cur = first + (offset - node_offset * chunk_size());
        }
    
        return *this;
    }
    
    // skip n steps
    self operator+(difference_type n)const{
        self tmp = *this;
        return tmp+= n; //reuse  operator +=
    }
    
    self& operator-=(difference_type n){
        return *this += -n; //reuse operator +=
    }
    
    self operator-(difference_type n)const{
        self tmp = *this;
        return tmp -= n; //reuse operator +=
    }
    
    // random access (iterator can skip n steps)
    // invoke operator + ,operator *
    reference operator[](difference_type n)const{
        return *(*this + n);
    }
    

    2。随机访问deque元素

    deque的常用功能

    iterator begin(){return start;}
    iterator end(){return finish;}
    
    reference front(){
        //invoke __deque_iterator operator*
        // return start's member *cur
        return *start;
    }
    
    reference back(){
        // cna't use *finish
        iterator tmp = finish;
        --tmp; 
        return *tmp; //return finish's  *cur
    }
    
    reference operator[](size_type n){
        //random access, use __deque_iterator operator[]
        return start[n];
    }
    

    你也看到了这个问题,它给出了deque的主要代码

    https://stackoverflow.com/a/50959796/6329006

    【讨论】:

      【解决方案3】:

      其中一种可能的实现可以是 const 大小数组的动态数组;当需要更多空间时,可以将这种 const 大小的数组添加到任一端。除了可能部分为空的第一个和最后一个数组之外,所有数组都是满的。这意味着知道每个数组的大小和第一个数组中使用的元素数量,就可以通过索引轻松找到元素的位置。
      http://cpp-tip-of-the-day.blogspot.ru/2013/11/how-is-stddeque-implemented.html

      【讨论】:

        【解决方案4】:

        如果 deque 在 std::vector 之上实现为 ring buffer,当它的大小增长时会重新分配自己,那么通过索引访问确实是 O(1)。

        该标准提供了证据表明这种实现是有意义的——至少它在复杂性估计方面符合标准。第 23.2.1.3/4 和 23.2.1.3/5 条要求

        • 插入到双端队列的开头或结尾需要恒定时间,而插入到中间需要双端队列大小的线性时间

        • 当从双端队列中擦除元素时,它可能会调用尽可能多的赋值运算符,就像从被擦除的元素到双端队列末尾的距离一样。

        当然,您应该依靠标准要求,而不是您自己对如何实现容器和算法的设想。

        注意该标准要求的不仅仅是上面列出的这两点。它还要求对元素的引用必须在插入到双端队列的后面或前面之间保持有效。如果环形缓冲区包含指向实际元素而不是元素本身的指针,则可以满足此要求。无论如何,我的回答只是证明了多个实现可能满足某些要求的想法。

        【讨论】:

        • 在这种情况下,元素是连续正确分配的......但我想知道如果它不作为循环缓冲区实现会怎样。
        • @jasonline,无论它是如何实现的,它的行为都受 C++ 标准文档的约束。它包含随机访问双端队列的要求,因此无法按照您建议的方式实现 STL 实现。我只是概述了它可以实施以符合标准的方式。
        • @jasonline:环形缓冲区不能保证连续分配的元素。它们被排序的内存是连续的,但如果元素从位置 987 开始并在位置 5 结束(并且环形缓冲区有空间容纳 1000 个东西),那么它们就不连续。
        • 你错了。它不能在 std::vector 之上实现,因为 std::deque 永远不会使对元素的引用无效。但是当没有可用空间时,std::vector 必须。
        • "如果 deque 实现为环形缓冲区" deque 实现为环形缓冲区。
        猜你喜欢
        • 1970-01-01
        • 2020-08-10
        • 2018-12-02
        • 2020-06-18
        • 2016-10-31
        • 1970-01-01
        • 2021-10-12
        相关资源
        最近更新 更多