Персистентная очередь — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
Строка 1: Строка 1:
После того, как мы получили [[Очередь#Реализация на шести стеках|очередь в реальном времени]] с <tex>O(1)=6</tex> обычными [[Стек|стеками]], ее можно легко превратить в персистентную, сделав все стеки [[Персистентный стек|персистентными]], но реально можно ограничиться всего пятью персистентными стеками.  
+
После того, как мы получили [[Очередь#Реализация на шести стеках|очередь в реальном времени]] с <tex>O(1)=6</tex> обычными [[Стек|стеками]], ее можно легко превратить в персистентную, сделав все стеки [[Персистентный стек|персистентными]], но на самом деле персистентность позволяет не создавать явной копии стека, так что достаточно всего пяти стеков.
  
 
== Эффективная реализация ==
 
== Эффективная реализация ==
  
По сравнению с реализацией на шести стеках, теперь у нас все стеки персистентные, а значит у нас нет необходимости хранить отдельно копию стека <tex>R</tex>, так как все его версии теперь доступны по его персистентности. Это позволяет нам заменить два стека <tex>Rc_1, Rc_2</tex> на один стек <tex>R_2</tex>, в который мы и будем записывать перекопированный кусок.
+
Будем отталкиваться от реализации на [[Очередь#Реализация на двух стеках|двух стеках]]. Пусть у нас есть стек <tex>L</tex> для операций <tex>push</tex>, стек <tex>R</tex> для операций <tex>pop</tex>.
 +
К моменту опустошения стека <tex>R</tex> нам нужно получить стек <tex>R'</tex>, содержащий все элементы стека <tex>L</tex> в правильном для извлечения порядке. Этого можно добиться, если переместить все элементы стека <tex>L</tex>, в стек <tex>R'</tex>, назовем такой процесс перекопированием, а очередь будет в это время находится в режиме перекопирования (''recopy mode''). Режим перекопирования активируется, когда во время очередной операции мы уже можем не успеть переместить нужные элементы, а именно <tex>L.size>R.size</tex>. Состояние очереди будет фиксировать логическая переменная <tex>recopy</tex>.
  
Персистентная очередь будет состоять из пяти персистентных стеков: <tex>L_1, L_2, R_1, R_2, T</tex>, причем стеки <tex>L_1, L_2</tex> используются для операций <tex>push</tex>, стеки <tex>R_1, R_2</tex> используются для операций <tex>pop</tex>, а стек <tex>T</tex> используется для временного хранения содержимого стека <tex>R</tex>.
+
Теперь рассмотрим как обрабатывать операции во время перекопирования. В режиме перекопирования операция <tex>empty=false</tex>, так как мы не можем извлекать элементы, находившиеся в не пустом стеке <tex>L</tex>, значит очередь всегда не пуста.
  
В каждый момент времени определено, какой стек <tex>L</tex> из стеков <tex>L_1, L_2</tex> сейчас используется для операций <tex>push</tex>, какой стек <tex>R</tex> из стеков <tex>R_1, R_2</tex> сейчас используется для операций <tex>pop</tex>, а также находится ли очередь в режиме перекопирования (''recopy mode'').
+
Поскольку стек <tex>L</tex> во время перемещения содержимого теряет свою структуру, мы не сможем положить туда элементы, пришедшие с <tex>push</tex>. Для таких элементов заведем специальный стек <tex>L'</tex> в который будем складывать поступающие элементы, а после перекопирования обменяем его ролями с <tex>L</tex>.
  
В режиме перекопирования операции <tex>push</tex> перенаправляются в парный стек <tex>L'</tex>; операции <tex>pop</tex> выполняются над последней персистентной версией <tex>R</tex>, тогда операция <tex>empty</tex> всегда возвращает <tex>false</tex>, так как стек <tex>L</tex> содержит <tex>n+1>0</tex> элементов, а в режиме перекопирования мы не можем извлечь их операцией <tex>pop</tex>.
+
Этот алгоритм почти работает, но есть проблема: никто не гарантирует, что стек <tex>R</tex> опустошится за время перекопирования, то есть мы получим в итоге два стека с выходными данными, а значит во время следующего перекопирования у нас может не быть свободного стека (например, если все операции были <tex>push</tex>). Избавимся от этой проблемы следующим образом: мы принудительно извлечем все элементы стека <tex>R</tex> во вспомогательный стек <tex>T</tex>, затем переместим все элементы стека <tex>L</tex> в стек <tex>R</tex>, затем обратно переместим все элементы <tex>T</tex> в стек <tex>R</tex>. Легко показать, что такая последовательность действий получит в стеке <tex>R</tex> все элементы в правильном порядке.
  
Для хранения последней персистентной версии <tex>R</tex> мы используем второй имеющийся стек <tex>R'</tex>, а также переменную <tex>toCopy</tex> {{---}} количество элементов, оставшихся в <tex>R'</tex>, оно уменьшается на <tex>1</tex> с каждой следующей операцией <tex>pop</tex>.
+
Теперь разберемся с операциями <tex>pop</tex>. Теперь у нас теряет свою структуру стек <tex>R</tex>, значит нужна его копия для извлечения элементов. Без персистентности нам бы пришлось создавать такую копию параллельно с самим стеком <tex>R</tex>, но теперь мы можем просто записать в <tex>R'</tex> последнюю версию стека <tex>R</tex> и дальше извлекать элементы уже из нее. Чтобы учесть извлеченные из копии элементы, будем использовать переменную <tex>toCopy=R'.size</tex>, будем уменьшать ее на <tex>1</tex> при каждом извлечении из <tex>T</tex> и операциях <tex>pop</tex>. Так как извлеченные элементы будут нарастать со дна стека <tex>T</tex>, мы никогда не извлечем некорректный элемент, если <tex>toCopy>0</tex>.
  
Режим перекопирования активируется, когда в стеке <tex>L</tex> становится больше элементов, чем в стеке <tex>R</tex>. Пусть в стеке <tex>R</tex> в этот момент находилось <tex>n</tex> элементов, тогда в стеке <tex>L</tex> находилось <tex>n+1</tex> элементов. Заметим, что мы можем корректно обработать только <tex>n</tex> запросов <tex>pop</tex>, операции <tex>push</tex> и <tex>empty</tex> мы обрабатываем корректно всегда, следовательно мы должны закончить перекопирование за <tex>n+1</tex> операцию, включая активирующую.
 
 
Для завершения перекопирования нужно:
 
# Переместить все содержимое стека <tex>R</tex> в <tex>T</tex>, <tex>n</tex> операций.
 
# Переместить все содержимое стека <tex>L</tex> в <tex>R</tex>, <tex>n + 1</tex> операция.
 
# Переместить первые <tex>toCopy</tex> элементов стека <tex>T</tex> в <tex>R</tex>, а оставшиеся элементы выкинуть, <tex>n</tex> операций.
 
# Обменять местами стеки <tex>L</tex> и <tex>L'</tex>, <tex>2</tex> операции.
 
 
Итого, мы должны сделать <tex>3 \cdot n + 3</tex> дополнительных действия за <tex>n + 1</tex> операций, то есть выполняя <tex>O(1)=3</tex> дополнительных действия на операцию, мы сможем завершить перекопирование и вернуться в обычный режим до исчерпания стека <tex>R'</tex>, получив <tex>O(1)</tex> реального времени и <tex>O(1)</tex> памяти на персистентность на операцию.
 
 
Теперь рассмотрим, какие изменения произошли за время перекопирования. Пусть среди <tex>n</tex> следующих за активацией операций у нас <tex>x</tex> операций <tex>pop</tex> и <tex>n-x</tex> операций <tex>push</tex>. Тогда в стеке <tex>R</tex> оказалось <tex>2 \cdot n + 1 - x</tex> элементов, а в новом стеке <tex>L</tex> оказалось <tex>n - x</tex> элементов. Тогда в стеке <tex>R</tex> на <tex>n+1</tex> больше элементов, чем в стеке <tex>L</tex>, а это значит, что до следующего режима перекопирования <tex>n + 2</tex> операции, и за это время мы успеем очистить старый стек <tex>Rc</tex>, в котором находится максимум <tex>n</tex> ненужных элементов, просто удаляя при каждой операции в обычном режиме один элемент из <tex>Rc</tex>, если он не пустой.
 
 
Заметим, что вышеприведенный алгоритм гарантирует нам, что в обычном режиме в стеке <tex>L</tex> находится не больше элементов, чем в <tex>R</tex>, так что проверка на пустоту очереди при обычном режиме сводится к проверке на пустоту стека <tex>R</tex>.
 
  
  
 
[[Категория: Дискретная математика и алгоритмы]]
 
[[Категория: Дискретная математика и алгоритмы]]
 
[[Категория: Амортизационный анализ]]
 
[[Категория: Амортизационный анализ]]

Версия 10:57, 8 июня 2013

После того, как мы получили очередь в реальном времени с [math]O(1)=6[/math] обычными стеками, ее можно легко превратить в персистентную, сделав все стеки персистентными, но на самом деле персистентность позволяет не создавать явной копии стека, так что достаточно всего пяти стеков.

Эффективная реализация

Будем отталкиваться от реализации на двух стеках. Пусть у нас есть стек [math]L[/math] для операций [math]push[/math], стек [math]R[/math] для операций [math]pop[/math]. К моменту опустошения стека [math]R[/math] нам нужно получить стек [math]R'[/math], содержащий все элементы стека [math]L[/math] в правильном для извлечения порядке. Этого можно добиться, если переместить все элементы стека [math]L[/math], в стек [math]R'[/math], назовем такой процесс перекопированием, а очередь будет в это время находится в режиме перекопирования (recopy mode). Режим перекопирования активируется, когда во время очередной операции мы уже можем не успеть переместить нужные элементы, а именно [math]L.size\gt R.size[/math]. Состояние очереди будет фиксировать логическая переменная [math]recopy[/math].

Теперь рассмотрим как обрабатывать операции во время перекопирования. В режиме перекопирования операция [math]empty=false[/math], так как мы не можем извлекать элементы, находившиеся в не пустом стеке [math]L[/math], значит очередь всегда не пуста.

Поскольку стек [math]L[/math] во время перемещения содержимого теряет свою структуру, мы не сможем положить туда элементы, пришедшие с [math]push[/math]. Для таких элементов заведем специальный стек [math]L'[/math] в который будем складывать поступающие элементы, а после перекопирования обменяем его ролями с [math]L[/math].

Этот алгоритм почти работает, но есть проблема: никто не гарантирует, что стек [math]R[/math] опустошится за время перекопирования, то есть мы получим в итоге два стека с выходными данными, а значит во время следующего перекопирования у нас может не быть свободного стека (например, если все операции были [math]push[/math]). Избавимся от этой проблемы следующим образом: мы принудительно извлечем все элементы стека [math]R[/math] во вспомогательный стек [math]T[/math], затем переместим все элементы стека [math]L[/math] в стек [math]R[/math], затем обратно переместим все элементы [math]T[/math] в стек [math]R[/math]. Легко показать, что такая последовательность действий получит в стеке [math]R[/math] все элементы в правильном порядке.

Теперь разберемся с операциями [math]pop[/math]. Теперь у нас теряет свою структуру стек [math]R[/math], значит нужна его копия для извлечения элементов. Без персистентности нам бы пришлось создавать такую копию параллельно с самим стеком [math]R[/math], но теперь мы можем просто записать в [math]R'[/math] последнюю версию стека [math]R[/math] и дальше извлекать элементы уже из нее. Чтобы учесть извлеченные из копии элементы, будем использовать переменную [math]toCopy=R'.size[/math], будем уменьшать ее на [math]1[/math] при каждом извлечении из [math]T[/math] и операциях [math]pop[/math]. Так как извлеченные элементы будут нарастать со дна стека [math]T[/math], мы никогда не извлечем некорректный элемент, если [math]toCopy\gt 0[/math].