Что такое deque в STL?

Я смотрел на контейнеры STL и пытался понять, что они на самом деле (т. е. используемая структура данных), и deque остановил меня: сначала я подумал, что это двойной связанный список, который позволит вставлять и удалять с обоих концов в постоянное время, но меня беспокоит обещание оператором [], который будет выполнен в постоянное время. В связанном списке произвольный доступ должен быть O (n), верно?

и если это динамический массив, как может это добавить элементы в постоянное время? Следует отметить, что перераспределение может произойти, и что O(1) является амортизированной стоимости, как для вектора.

поэтому мне интересно, что это за структура, которая позволяет произвольный доступ в постоянное время, и в то же время никогда не нужно перемещать в новое большее место.

7 ответов


deque несколько рекурсивно определен: внутренне он поддерживает двустороннюю очередь блоки ("блоки" на рисунке ниже) фиксированного размера. Каждый кусок является вектором, а очередь ("карта" на рисунке ниже) самих Кусков также является вектором.

schematic of the memory layout of a deque

существует отличный анализ характеристик производительности и того, как он сравнивается с vector на CodeProject.

GCC стандартная реализация библиотеки внутренне использует T** для представления карты. Каждый блок данных T*, который выделяется с некоторым фиксированным размером __deque_buf_size (которого зависит sizeof(T)).


deque = двойная очередь

контейнер, который может расти в любом направлении.

Дека обычно реализован как vector of vectors (список векторов не может дать постоянная времени случайного доступа). Хотя размер вторичных векторов зависит от реализации, общий алгоритм должен использовать постоянный размер в байтах.


представьте себе, что это вектор векторов. Только они не стандартные!--0-->s.

внешний вектор содержит указатели на внутренние векторы. Когда его емкость изменяется путем перераспределения, а не выделения всего пустого пространства до конца как std::vector делает, он разбивает пустое пространство на равные части в начале и конце вектора. Это позволяет push_front и push_back на этом векторе оба происходят в амортизированное O (1) Время.

поведение внутреннего вектора необходимо изменить в зависимости от того, находится ли он спереди или сзади deque. Сзади он может вести себя как стандартный std::vector, где он растет в конце, и push_back происходит в O (1) времени. На фронте нужно делать наоборот, растя в начале с каждым push_front. На практике это легко достигается путем добавления указателя на передний элемент и направление роста вместе с размером. С помощью этой простой модификации push_front также может быть O(1) времени.

доступ к любой элемент требует смещения и деления на соответствующий внешний векторный индекс, который встречается в O(1), и индексирования на внутренний вектор, который также является O (1). Это предполагает, что все внутренние векторы имеют фиксированный размер, за исключением тех, которые находятся в начале или конце deque.


(это ответ, который я дал в другое нить. По сути, я утверждаю, что даже довольно наивные реализации, используя один vector, соответствуют требованиям " постоянн non-амортизированного push_{фронта, задней части}". Вы можете быть удивлены, и думаю, что это невозможно, но я нашел другие соответствующие цитаты в стандарте, которые определяют контекст удивительным образом. Пожалуйста, потерпите меня; если я допустил ошибку в этом ответе, было бы очень полезно идентифицировать какие вещи я сказал правильно и где моя логика сломалась. )

в этом ответе, я не пытаюсь определить хороший реализация, я просто пытаюсь помочь нам интерпретировать требования к сложности в стандарте C++. Я цитирую из N3242, который, согласно Википедия, самый последний свободно доступный документ стандартизации к++11. (Он, по-видимому, организован иначе, чем окончательный стандарт, и, следовательно, я не буду указывать точные номера страниц. Конечно, эти правила могли измениться в конечном стандарте, но я не думаю, что это произошло.)

A deque<T> может быть реализовано правильно с помощью vector<T*>. Все элементы копируются в кучу, а указатели хранятся в векторе. (Подробнее о векторе позже).

почему T* вместо T? Потому что стандарт требует этого

"вставка на обоих концах деки делает недействительными все итераторы к Деку, но имеет отсутствие влияния на действительность ссылок на элементы дека."

(Курсив мой). The T* помогает удовлетворить это. Это также помогает нам удовлетворить это:

"вставка одного элемента либо в начале, либо в конце deque всегда ..... причины a одиночный вызов конструктора T."

теперь для (спорного) бита. Почему? используйте vector для хранения T*? Это дает нам случайный доступ, что является хорошим началом. Давайте на мгновение забудем о сложности вектора и построим до этого тщательно:

в стандарте говорится о " количестве операций над содержащимися объектами.". Для deque::push_front это очевидно, потому что ровно 1


хотя стандарт не требует какой-либо конкретной реализации (только случайный доступ с постоянным временем), deque обычно реализуется как коллекция непрерывных "страниц"памяти. Новые страницы выделяются по мере необходимости, но у вас все еще есть случайный доступ. В отличие от std::vector, вам не обещают, что данные хранятся смежно, но, как и вектор, вставки посередине требуют большого перемещения.


из обзора, вы можете думать deque как double-ended queue

deque overview

данные в deque хранятся чунками вектора фиксированного размера, которые являются

pointered по map(который также является куском вектора, но его размер может измениться)

deque internal structure

основная часть кода 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)

основная проблема итератора-когда++, -- iterator, он может перейти к другому чанку (если он указывает на край чанка). Например, существует три блока данных: chunk 1,chunk 2,chunk 3.

на pointer1 указатели на начало chunk 2, когда оператор --pointer это будет указатель на конец chunk 1, так как pointer2.

enter image description here

ниже я дам основную функцию __deque_iterator:

во-первых, перейти к любой фрагмент:

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

отметим, что chunk_size() функция, которая вычисляет размер куска, вы можете подумать, что она возвращает 8 для упрощения здесь.

operator* получить данные в блок

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


template<typename T, size_t buff_size>
deque<T, buff_size>::deque(size_t n, const value_type& value){
    fill_initialize(n, value);
}

template<typename T, size_t buff_size>
void deque<T, buff_size>::fill_initialize(size_t n, const value_type& value){
    // allocate memory for map and chunk
    // initialize pointer
    create_map_and_nodes(n);

    // initialize value for the chunks
    for (map_pointer cur = start.node; cur < finish.node; ++cur) {
        initialized_fill_n(*cur, chunk_size(), value);
    }

    // the end chunk may have space node, which don't need have initialize value
    initialized_fill_n(finish.first, finish.cur - finish.first, value);
}

template<typename T, size_t buff_size>
void deque<T, buff_size>::create_map_and_nodes(size_t num_elements){
    // the needed map node = (elements nums / chunk length) + 1
    size_type num_nodes = num_elements / chunk_size() + 1;

    // map node num。min num is  8 ,max num is "needed size + 2"
    map_size = std::max(8, num_nodes + 2);
    // allocate map array
    map = mapAllocator::allocate(map_size);

    // tmp_start,tmp_finish poniters to the center range of map
    map_pointer tmp_start  = map + (map_size - num_nodes) / 2;
    map_pointer tmp_finish = tmp_start + num_nodes - 1;

    // allocate memory for the chunk pointered by map node
    for (map_pointer cur = tmp_start; cur <= tmp_finish; ++cur) {
        *cur = dataAllocator::allocate(chunk_size());
    }

    // set start and end iterator
    start.set_node(tmp_start);
    start.cur = start.first;

    finish.set_node(tmp_finish);
    finish.cur = finish.first + num_elements % chunk_size();
}

предположим i_deque имеет 20 элементов int 0~19 чей размер куска равен 8, а теперь push_back 3 элемента для i_deque:

i_deque.push_back(1);
i_deque.push_back(2);
i_deque.push_back(3);

это внутренняя структура, как показано ниже:

enter image description here

затем push_back снова, он вызовет выделить новый кусок:

push_back(3)

enter image description here

если мы push_front, он выделит новый кусок перед prev start

enter image description here

обратите внимание, когда push_back элемент в deque, если все карты и куски заполнены, это приведет к выделению новой карты и корректировке фрагментов.Но вышеуказанного кода может быть достаточно, чтобы вы поняли deque.


Я читал "структуры данных и алгоритмы в C++" Адам Drozdek, и нашел, что это полезно. HTH.

очень интересным аспектом STL deque является его реализация. STL deque реализуется не как связанный список, а как массив указателей на блоки или массивы данных. Количество блоков изменяется динамически в зависимости от потребностей хранения, и соответственно изменяется размер массива указателей.

вы можете заметить в середине массив указателей на данные (куски справа), а также вы можете заметить, что массив посередине динамически меняется.

изображение стоит тысячи слов.

enter image description here