Я смотрел на STL контейнеры и пытаясь понять , что они на самом деле (то есть структура данных , используемая), а Deque остановил меня: я сначала подумал , что это был двойной связанный список, который позволил бы вставку и удаление с обоих концов в постоянное время, но меня беспокоит обещание , данное оператором [], которое должно быть выполнено в постоянное время. В связанном списке произвольный доступ должен быть O (n), верно?
И если это динамический массив, как он может добавлять элементы в постоянное время? Следует отметить, что может произойти перераспределение, и что O (1) является амортизированной стоимостью, как для вектора .
Поэтому мне интересно, что это за структура, которая обеспечивает произвольный доступ в постоянное время, и в то же время никогда не нужно перемещать ее в новое более крупное место.
deque
обозначает двустороннюю очередь , хотя очевидно, что строгое требование O (1) доступа к средним элементам является специфическим для C ++Ответы:
Deque несколько рекурсивно определен: внутренне он поддерживает двустороннюю очередь кусков фиксированного размера. Каждый чанк является вектором, и очередь («карта» на рисунке ниже) самих чанков также является вектором.
Там отличный анализ характеристик производительности и , как он сравнивает с ,
vector
по меньшей CodeProject .Реализация стандартной библиотеки GCC внутренне использует
T**
для представления карты. Каждый блок данных имеетT*
определенный фиксированный размер__deque_buf_size
(который зависит отsizeof(T)
).источник
Вообразите это как вектор векторов. Только они не стандартные
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
.источник
Контейнер, который может расти в любом направлении.
Deque обычно реализуется как
vector
оvectors
(список векторов не может дать произвольный доступ с постоянным временем). В то время как размер вторичных векторов зависит от реализации, общий алгоритм должен использовать постоянный размер в байтах.источник
array
ни о чем илиvector
о чем-то, что может обещать амортизированныйO(1)
push_front. Внутренняя часть двух структур, по крайней мере, должна иметьO(1)
push_front, что ни гарантия,array
ниvector
гарантия.vector
этого не делает, но это достаточно простая модификация, чтобы сделать это.(Это ответ, который я дал в другой ветке . По сути, я утверждаю, что даже довольно наивные реализации, использующие одну
vector
, соответствуют требованиям «постоянного не амортизированного push_ {front, back}». Вы можете быть удивлены и думаю, что это невозможно, но я нашел в стандарте другие релевантные цитаты, которые удивительным образом определяют контекст. Пожалуйста, потерпите меня, если я допустил ошибку в этом ответе, было бы очень полезно определить, какие вещи Я правильно сказал, и где моя логика сломалась.)В этом ответе я не пытаюсь определить хорошую реализацию, я просто пытаюсь помочь нам интерпретировать требования сложности в стандарте C ++. Я цитирую N3242 , который, согласно Википедии , является последним свободно доступным документом по стандартизации C ++ 11. (Похоже, что он организован не так, как в окончательном стандарте, и поэтому я не буду указывать точные номера страниц. Конечно, эти правила могли измениться в окончательном стандарте, но я не думаю, что это произошло.)
А
deque<T>
может быть правильно реализован с помощьюvector<T*>
. Все элементы копируются в кучу, а указатели хранятся в векторе. (Подробнее о векторе позже).Почему
T*
вместоT
? Поскольку стандарт требует, чтобы(мой акцент). Это
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*
, что мы можем игнорировать любую операцию, которая копирует (или перераспределяет) aT*
.Я не сказал много о том, как вектор будет вести себя в deque. Возможно, мы интерпретируем его как кольцевой буфер (вектор всегда занимает максимум
capacity()
, а затем перераспределяем все в больший буфер, когда вектор заполнен. Детали не имеют значения.В последних нескольких абзацах мы проанализировали
deque::push_front
и взаимосвязь между количеством объектов в уже существующей deque и количеством операций, выполняемых push_front над содержащимисяT
-объектами. И мы обнаружили, что они были независимы друг от друга. Поскольку стандарт предписывает, что сложность заключается в операциях над операциямиT
, мы можем сказать, что это имеет постоянную сложность.Да, сложность Operations-On-T *-Амортизируется (из-за
vector
), но нас интересует только сложность Operations-On-T, и она постоянна (не амортизируется).Сложность vector :: push_back или vector :: push_front не имеет значения в этой реализации; эти соображения связаны с операциями
T*
и, следовательно, не имеют значения. Если бы стандарт ссылался на «условное» теоретическое понятие сложности, то они бы не стали явно ограничивать себя «количеством операций над содержащимися объектами». Я переоценил это предложение?источник
list
независимо от текущего размера списка; Если список слишком велик, распределение будет медленным или не удастся. Следовательно, насколько я вижу, комитет принял решение указать только те операции, которые можно объективно подсчитать и измерить. (PS: у меня есть другая теория на этотO(n)
что число операций асимптотически пропорционально количеству элементов. IE, количество метаопераций. В противном случае не имеет смысла ограничивать поискO(1)
. Ergo, связанные списки не имеют права.list
может быть реализован какvector
указатель (вставка в середину приведет к вызову одного конструктора копирования, независимо от размера списка, иO(N)
перетасовку указателей можно игнорировать, поскольку они не операции на Т).deque
этот способ и (2) «обманывают» таким образом (даже если это разрешено стандартом), когда вычисление алгоритмической сложности не помогает при написании эффективных программ ,Из обзора вы можете думать
deque
какdouble-ended queue
Данные в
deque
хранятся чанками вектора фиксированного размера, которыеуказывается с помощью
map
(который также является частью вектора, но его размер может измениться)Основная часть кода
deque iterator
, как показано ниже:Основная часть кода
deque
, как показано ниже:Ниже я дам вам основной код
deque
, в основном из трех частей:итератор
Как построить
deque
1. iterator (
__deque_iterator
)Основная проблема итератора заключается в том, что когда ++, - итератор, он может перейти к другому чанку (если указатель на край чанка). Например, есть три данные ломтей:
chunk 1
,chunk 2
,chunk 3
.В
pointer1
указатели на началоchunk 2
, когда оператор--pointer
будет указатель на конецchunk 1
, чтобы кpointer2
.Ниже я приведу основные функции
__deque_iterator
:Во-первых, перейдите к любому фрагменту:
Обратите внимание, что
chunk_size()
функция, которая вычисляет размер чанка, вы можете подумать, что возвращает 8 для упрощения здесь.operator*
получить данные в чанкеoperator++, --
// префикс формы приращения
итератор пропустить n шагов / произвольный доступ2. Как построить
deque
общая функция
deque
Предположим,
i_deque
есть 20 элементов int,0~19
чей размер равен 8, и теперь push_back 3 элемента (0, 1, 2) дляi_deque
:Это внутренняя структура, как показано ниже:
Затем push_back снова вызовет выделение нового чанка:
Если мы
push_front
, он будет выделять новый кусок доstart
Обратите внимание, когда
push_back
элемент в deque, если все карты и чанки заполнены, это приведет к выделению новой карты и настройке чанков. Но приведенного выше кода может быть достаточно для пониманияdeque
.источник
Я читал «Структуры данных и алгоритмы в C ++» Адама Дроздека и нашел это полезным. НТН.
Вы можете заметить, что в середине находится массив указателей на данные (фрагменты справа), а также вы можете заметить, что массив в середине динамически изменяется.
Изображение стоит тысячи слов.
источник
deque
часть, и это довольно хорошо.В то время как стандарт не предписывает какой-либо конкретной реализации (только произвольный доступ с постоянным временем), deque обычно реализуется как совокупность непрерывных «страниц» памяти. Новые страницы распределяются по мере необходимости, но у вас все еще есть произвольный доступ. В отличие от этого
std::vector
, вам не обещают, что данные хранятся непрерывно, но, как и в случае с вектором, вставки в середину требуют большого перемещения.источник
insert
требуется много переселения , как это эксперимент- здесь показывает ошеломляющие разницу междуvector::insert()
иdeque::insert()
?