Что на самом деле является deque в STL? - PullRequest
160 голосов
/ 09 июня 2011

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

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

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

Ответы [ 7 ]

152 голосов
/ 09 июня 2011

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

schematic of the memory layout of a deque

анализ характеристик производительности и их сравнение с vector в CodeProject .

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

20 голосов
/ 30 июня 2014

Представьте это как вектор векторов. Только они не стандартные std::vector с.

Внешний вектор содержит указатели на внутренние векторы. Когда его емкость изменяется посредством перераспределения, вместо того, чтобы распределять все пустое пространство до конца, как это делает 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.

16 голосов
/ 09 июня 2011

deque = двусторонняя очередь

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

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

11 голосов
/ 26 декабря 2011

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

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

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

Почему T* вместо T?Поскольку стандарт требует, чтобы

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

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

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

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

Стандарт говорит о «количестве операций над содержащимися объектами».Для deque::push_front это явно 1, потому что построен только один T объект, и ноль из существующих T объектов считывается или сканируется любым способом.Это число 1, очевидно, является константой и не зависит от количества объектов, находящихся в данный момент в декеЭто позволяет нам сказать, что:

'Для наших deque::push_front число операций над содержащимися объектами (Ts) является фиксированным и не зависит от количества объектов, уже находящихся в очереди.'

Конечно, количество операций на T* будет не таким хорошим.Когда vector<T*> станет слишком большим, он будет перераспределен, и многие T* будут скопированы.Так что да, количество операций на T* будет сильно отличаться, но на число операций на T это не повлияет.

Почему нас волнует это различие между операциями подсчета на Tа подсчет операций на T*?Это потому, что стандарт гласит:

Все требования к сложности в этом разделе изложены исключительно с точки зрения количества операций над содержащимися объектами.

Для deque, содержащиеся объекты являются T, а не T*, что означает, что мы можем игнорировать любую операцию, которая копирует (или перераспределяет) T*.

Я не сказал много о том, каквектор будет вести себя в деке.Возможно, мы интерпретируем его как кольцевой буфер (вектор всегда занимает максимум capacity(), а затем перераспределяем все в больший буфер, когда вектор заполнен. Детали не имеют значения.

В последних нескольких абзацах мы проанализировали deque::push_front и взаимосвязь между количеством объектов в уже существующей deque и числом операций, выполняемых push_front над содержащимися T -объектами. И мы обнаружили, что они были независимы друг от друга. Поскольку стандарт требует, чтобы сложность выражалась в операциях на T, то мы можем сказать, что это имеет постоянную сложность.

Да, сложность Операции на Т * амортизируется (из-за vector), но нас интересует только Сложность операций на Т и это постоянно (не амортизируется).

Сложность vector :: push_back или vector :: push_front не имеет значения в этой реализации; эти соображения связаны с операциями на T* и, следовательно, не имеют значения. Если бы стандарт ссылался на «обычное» теоретическое понятие сложности, то они бы не ограничивали себя «числом операций над содержащимися объектами». Я переоценил это предложение?

7 голосов
/ 21 июня 2018

Из обзора можно представить deque как double-ended queue

deque overview

Данные в deque хранятся в виде фрагментов фиксированныхразмерный вектор, на который

указывает 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)

Основная проблема итератора заключается в том, что когда ++, - итератор, он может перейти к другому чанку (если он указывает на край чанка).Например, есть три блока данных: 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, он вызовет allocateновый блок:

push_back(3)

enter image description here

Если мы push_front, он будет выделять новый блок до предыдущего start

enter image description here

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

6 голосов
/ 09 июня 2011

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

3 голосов
/ 16 ноября 2017

Я читал «Структуры данных и алгоритмы на С ++» Адама Дроздека и нашел это полезным. НТН.

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

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

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

enter image description here

...