Jump to content

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

В информатике двусторонняя очередь (сокращенно deque , / d ɛ k / DEK) . [1] ) — абстрактный тип данных , обобщающий очередь , элементы которой можно добавлять или удалять либо спереди (голова), либо сзади (хвост). [2] Его также часто называют связным списком «голова-хвост» , хотя на самом деле это относится к конкретной структуры данных реализации двухсторонней очереди (см. ниже).

Соглашения об именах

[ редактировать ]

Deque иногда пишется dequeue , но такое использование обычно не рекомендуется в технической литературе и технических текстах, поскольку dequeue также является глаголом, означающим «удалить из очереди». Тем не менее, несколько библиотек и некоторые авторы, такие как Ахо , Хопкрофт и Ульман в своем учебнике «Структуры данных и алгоритмы» , называют это dequeue . Джон Митчелл , автор книги «Концепции языков программирования», также использует эту терминологию.

Различия и подтипы

[ редактировать ]

Это отличается от абстрактного типа данных очереди или «первым пришел — первым обслужен» списка ( FIFO ), где элементы можно добавлять только с одного конца и удалять с другого. Этот общий класс данных имеет несколько возможных подтипов:

  • Дек с ограничением ввода — это такой запрос, в котором удаление может быть выполнено с обоих концов, а вставка — только с одного конца.
  • Дек с ограничением вывода — это такой, в котором вставка может быть выполнена с обоих концов, а удаление может быть выполнено только с одного конца.

Как базовые, так и наиболее распространенные типы списков в вычислениях, очереди и стеки можно считать специализацией деков и реализовать с их помощью. Дек — это структура данных, которая позволяет пользователям выполнять операции push и pop на обоих концах, обеспечивая гибкость в управлении порядком элементов.

Операции

[ редактировать ]
Диаграмма классов UML двусторонней очереди
UML class diagram of a double-ended queue

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

Имена различаются в зависимости от языка; основные реализации включают в себя:

операция общее имя (имена) Есть С++ Ява Перл PHP Питон Руби Ржавчина JavaScript
вставить элемент сзади вводить, нюхать, толкать Append push_back offerLast push array_push append push push_back push
вставить элемент спереди толчок, минусы Prepend push_front offerFirst unshift array_unshift appendleft unshift push_front unshift
удалить последний элемент выбрасывать Delete_Last pop_back pollLast pop array_pop pop pop pop_back pop
удалить первый элемент поп Delete_First pop_front pollFirst shift array_shift popleft shift pop_front shift
проверить последний элемент заглянуть Last_Element back peekLast $array[-1] end <obj>[-1] last back <obj>.at(-1)
изучить первый элемент First_Element front peekFirst $array[0] reset <obj>[0] first front <obj>[0]

Реализации

[ редактировать ]

Существует как минимум два распространенных способа эффективной реализации двухсторонней очереди: с помощью модифицированного динамического массива или с помощью двусвязного списка .

Подход динамического массива использует вариант динамического массива , который может расти с обоих концов, иногда называемый массивом deques . Эти деки массива обладают всеми свойствами динамического массива, такими как произвольный доступ с постоянным временем , хорошая локальность ссылки и неэффективная вставка/удаление в середине, с добавлением вместо этого амортизированной вставки/удаления с постоянным временем на обоих концах. только с одного конца. Три распространенные реализации включают в себя:

  • Хранение содержимого дека в кольцевом буфере и изменение размера только тогда, когда буфер заполняется. Это уменьшает частоту изменения размеров.
  • Выделение содержимого дека из центра базового массива и изменение размера базового массива при достижении любого конца. Этот подход может потребовать более частого изменения размеров и тратить больше места, особенно когда элементы вставляются только с одного конца.
  • Хранение содержимого в нескольких массивах меньшего размера с выделением дополнительных массивов в начале или конце по мере необходимости. Индексирование реализуется путем хранения динамического массива, содержащего указатели на каждый из меньших массивов.

Чисто функциональная реализация

[ редактировать ]

Двусторонние очереди также можно реализовать как чисто функциональную структуру данных . [3] : 115  Существуют две версии реализации. Первый из них, называемый « deque реального времени », представлен ниже. Это позволяет очереди быть постоянной с операциями за время O (1) в худшем случае, но требует ленивых списков с мемоизацией . Второй, без ленивых списков и мемоизации, представлен в конце разделов. Его амортизированное время равно O (1), если постоянство не используется; но наихудшая сложность операции равна O ( n ) , где n — количество элементов в двусторонней очереди.

Напомним, что для списка l, |l| обозначает его длину, то есть NIL представляет собой пустой список и CONS(h, t) представляет список, голова которого h и чей хвост t. Функции drop(i, l) и take(i, l) вернуть список l без своего первого i элементы и первый i элементы l, соответственно. Или, если |l| < i, они возвращают пустой список и l соответственно.

Деки в реальном времени посредством ленивой перестройки и планирования

[ редактировать ]

Двусторонняя очередь представляется в виде шестерки (len_front, front, tail_front, len_rear, rear, tail_rear) где front представляет собой связанный список , который содержит начало очереди длиной len_front. Сходным образом, rear представляет собой связанный список, который представляет собой обратную сторону задней части очереди длиной len_rear. Более того, уверяют, что |front| ≤ 2|rear|+1 и |rear| ≤ 2|front|+1 - интуитивно это означает, что и спереди, и сзади содержится от трети минус одна до двух третей плюс один из элементов. Окончательно, tail_front и tail_rear являются хвостами front и из rear, они позволяют запланировать момент принудительного выполнения некоторых ленивых операций. Обратите внимание, что когда двусторонняя очередь содержит n элементы в первом списке и n элементы в заднем списке, то инвариант неравенства остается удовлетворенным после i вставки и d удаления, когда (i+d) ≤ n/2. То есть максимум n/2 операции могут происходить между каждой ребалансировкой.

Давайте сначала дадим реализацию различных операций, которые влияют на переднюю часть дека — минусы, голову и хвост. Эти реализации не обязательно учитывают инвариант. Во второй раз мы объясним, как изменить дек, который не удовлетворяет инварианту, в тот, который ему удовлетворяет. Однако они используют инвариант: если передняя часть пуста, то задняя часть содержит не более одного элемента. Операции, влияющие на конец списка, определяются аналогично симметрией.

empty = (0, NIL, NIL, 0, NIL, NIL)
fun insert'(x, (len_front, front, tail_front, len_rear, rear, tail_rear)) =
  (len_front+1, CONS(x, front), drop(2, tail_front), len_rear, rear, drop(2, tail_rear))
fun head((_, CONS(h, _), _, _, _, _)) = h
fun head((_, NIL, _, _, CONS(h, NIL), _)) = h
fun tail'((len_front, CONS(head_front, front), tail_front, len_rear, rear, tail_rear)) =
  (len_front - 1, front, drop(2, tail_front), len_rear, rear, drop(2, tail_rear))
fun tail'((_, NIL, _, _, CONS(h, NIL), _)) = empty

Осталось объяснить, как определить метод balance это перебалансирует дек, если insert' или tail сломал инвариант. Метод insert и tail можно определить, сначала применив insert' и tail' а затем применяя balance.

fun balance(q as (len_front, front, tail_front, len_rear, rear, tail_rear)) =
  let floor_half_len = (len_front + len_rear) / 2 in
  let ceil_half_len = len_front + len_rear - floor_half_len in
  if len_front > 2*len_rear+1 then
    let val front' = take(ceil_half_len, front)
        val rear' = rotateDrop(rear, floor_half_len, front)
    in (ceil_half_len, front', front', floor_half_len, rear', rear')
  else if len_front > 2*len_rear+1 then
    let val rear' = take(floor_half_len, rear)
        val front' = rotateDrop(front, ceil_half_len, rear)
    in (ceil_half_len, front', front', floor_half_len, rear', rear')
  else q

где rotateDrop(front, i, rear)) вернуть конкатенацию front и из drop(i, rear). То есть front' = rotateDrop(front, ceil_half_len, rear) положить в front' содержание front и содержание rear этого еще нет rear'. С момента падения n элементы берут время мы используем ленивость, чтобы гарантировать, что элементы отбрасываются по два, причем два отбрасывания выполняются в течение каждого tail' и каждый insert' операция.

fun rotateDrop(front, i, rear) =
  if i < 2 then rotateRev(front, drop(i, rear), NIL)
  else let CONS(x, front') = front in
    CONS(x, rotateDrop(front', j-2, drop(2, rear)))

где rotateRev(front, middle, rear) — это функция, которая возвращает переднюю часть, за которой следует перевернутую середину, а затем заднюю часть. Эта функция также определена с использованием ленивости, чтобы гарантировать, что ее можно вычислить шаг за шагом, при этом каждый шаг выполняется по одному шагу. insert' и tail' и занимая постоянное время. Эта функция использует инвариант, который |rear|-2|front| это 2 или 3.

fun rotateRev(NIL, rear, a) =
  reverse(rear)++a
fun rotateRev(CONS(x, front), rear, a) =
  CONS(x, rotateRev(front, drop(2, rear), reverse(take(2, rear))++a))

где ++ — это функция, объединяющая два списка.

Реализация без лени

[ редактировать ]

Обратите внимание, что без ленивой части реализации это была бы непостоянная реализация очереди за O (1) амортизированное время . В этом случае списки tail_front и tail_rear могут быть удалены из представления двусторонней очереди.

Языковая поддержка

[ редактировать ]

Контейнеры Ada предоставляют универсальные пакеты. Ada.Containers.Vectors и Ada.Containers.Doubly_Linked_Lists, для реализаций динамического массива и связанного списка соответственно.

C++ Стандартная библиотека шаблонов предоставляет шаблоны классов. std::deque и std::list, для реализации нескольких массивов и связанных списков соответственно.

Начиная с Java 6, Java Collections Framework предоставляет новую Deque интерфейс, который обеспечивает функциональность вставки и удаления на обоих концах. Это реализуется такими классами, как ArrayDeque (также новое в Java 6) и LinkedList, предоставляя реализации динамического массива и связанного списка соответственно. Однако ArrayDeque, вопреки своему названию, не поддерживает произвольный доступ.

в Javascript Прототип Array и массивы Perl имеют встроенную поддержку удаления ( shift и pop ) и добавления ( unshift и push ) элементов на обоих концах.

Python 2.4 представил collections модуль с поддержкой объектов deque . Он реализован с использованием двусвязного списка подмассивов фиксированной длины.

Начиная с PHP 5.3, расширение PHP SPL содержит класс SplDoublyLinkedList, который можно использовать для реализации структур данных Deque. Раньше для создания структуры Deque вместо этого приходилось использовать функции массива array_shift/unshift/pop/push.

GHC Модуль Data.Sequence реализует эффективную, функциональную структуру двухсторонних очередей в Haskell . В реализации используются 2–3-пальцевые деревья , аннотированные размерами. Существуют и другие (быстрые) возможности реализации чисто функциональных (таким образом, также постоянных ) двойных очередей (большинство из которых используют сильно ленивые вычисления ). [3] [4] Каплан и Тарьян были первыми, кто реализовал оптимальные слитно-персистентные цепные деки. [5] Их реализация была строго функциональной в том смысле, что она не использовала ленивые вычисления. Окасаки упростил структуру данных, используя отложенное вычисление с загрузочной структурой данных и ухудшив границы производительности с наихудшего до амортизированного. [6] Каплан, Окасаки и Тарьян создали более простую, не загружаемую, амортизированную версию, которую можно реализовать либо с использованием ленивых вычислений, либо более эффективно с использованием мутации в более широком, но все же ограниченном виде. [7] Михаеску и Тарьян создали более простую (но все же очень сложную) строго чисто функциональную реализацию объединяемых двоичных данных, а также гораздо более простую реализацию строго чисто функциональных несоединяемых деков, обе из которых имеют оптимальные границы для наихудшего случая. [8]

Руста std::collections включает VecDeque , который реализует двустороннюю очередь с использованием расширяемого кольцевого буфера.

Сложность

[ редактировать ]
  • В реализации двусвязного списка и при отсутствии накладных расходов на выделение/освобождение временная сложность всех операций дека равна O(1) . Кроме того, временная сложность вставки или удаления в середине с учетом итератора равна O (1); однако временная сложность произвольного доступа по индексу равна O(n).
  • В растущем массиве амортизированная временная сложность всех операций с деком равна O(1) . Кроме того, временная сложность произвольного доступа по индексу равна O (1); но временная сложность вставки или удаления в середине равна O(n) .

Приложения

[ редактировать ]
можно использовать двустороннюю очередь Для хранения истории посещений : новые веб-сайты добавляются в конец очереди, а самые старые записи будут удаляться, если история слишком велика. Когда пользователь просит очистить историю посещений за последний час, самые последние добавленные записи удаляются.

Одним из примеров использования двухсторонней очереди является алгоритм перехвата работы . [9] Этот алгоритм реализует планирование задач для нескольких процессоров. Для каждого процессора поддерживается отдельная дек с выполняемыми потоками. Для выполнения следующего потока процессор получает первый элемент из дека (используя операцию «удалить первый элемент»). Если текущий поток разветвляется, он возвращается в начало дека («вставить элемент спереди») и выполняется новый поток. Когда один из процессоров завершает выполнение своих потоков (т.е. его дек пуст), он может «украсть» поток у другого процессора: он получает последний элемент из дека другого процессора («удалит последний элемент») и выполняет это. Алгоритм перехвата работы используется библиотекой Intel Threading Building Blocks (TBB) для параллельного программирования.

См. также

[ редактировать ]
  1. ^ Джесси Либерти; Сиддхартха Рао; Брэдли Джонс. C++ за один час в день, Sams Teach Yourself , шестое издание. Издательство Самс, 2009. ISBN   0-672-32941-7 . Урок 18: Классы динамических массивов STL, стр. 486.
  2. ^ Дональд Кнут . Искусство компьютерного программирования , Том 1: Фундаментальные алгоритмы , Третье издание. Аддисон-Уэсли, 1997. ISBN   0-201-89683-4 . Раздел 2.2.1: Стеки, очереди и деки, стр. 238–243.
  3. ^ Jump up to: а б Окасаки, Крис (сентябрь 1996 г.). Чисто функциональные структуры данных (PDF) (кандидатская диссертация). Университет Карнеги-Меллон. КМУ-КС-96-177.
  4. ^ Адам Л. Буксбаум и Роберт Э. Тарьян. Слитно-персистентные деки с помощью структурной загрузки данных. Журнал алгоритмов, 18(3):513–547, май 1995 г. (стр. 58, 101, 125).
  5. ^ Хаим Каплан и Роберт Э. Тарьян. Чисто функциональные представления объединяемых отсортированных списков. На симпозиуме ACM по теории вычислений, страницы 202–211, май 1996 г. (стр. 4, 82, 84, 124).
  6. ^ Крис Окасаки (август 1997 г.), Объединяемые двусторонние очереди , Уведомления ACM SIGPLAN, том 32, выпуск 8
  7. ^ Хаим Каплан, Крис Окасаки и Роберт Э. Тарджан (2000), Простые слитно-персистентные объединяемые списки , SIAM Journal on Computing Vol. 30, вып. 3
  8. Раду Михаеску и Роберт Тарьян (август 2003 г.), Заметки о объединяемых деках в Pure Lisp , Принстаунский университет, COS 528, осень 03 г.
  9. ^ Блюмоф, Роберт Д.; Лейзерсон, Чарльз Э. (1999). «Планирование многопоточных вычислений путем кражи работы» (PDF) . Дж АСМ . 46 (5): 720–748. дои : 10.1145/324133.324234 . S2CID   5428476 .
[ редактировать ]
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: b08f83cbae3ea81dd404b6add135182b__1720314240
URL1:https://arc.ask3.ru/arc/aa/b0/2b/b08f83cbae3ea81dd404b6add135182b.html
Заголовок, (Title) документа по адресу, URL1:
Double-ended queue - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)