Изменения

Перейти к: навигация, поиск

Персистентная очередь

809 байт добавлено, 23:31, 5 сентября 2019
м
Правка орфографии
'''Персистентная очередь''' (англ. ''persistent queue'') {{---}} это [[Очередь|очередь]], реализующая [[Персистентные структуры данных|персистентность]], то есть позволяющая получить доступ ко всем своим предыдущим версиям. Как будет показано далее, можно реализовать функциональную персистентность, то есть каждая ячейка памяти в такой структуре будет инициализирована один раз и в дальнейшем не изменится.
== Основная идея ==
Понятно, что во время перекопирования могут поступить операции <tex>\mathtt{push}</tex>, а стек <tex>L</tex> в это время потеряет свою структуру, сложить элементы туда мы уже не сможем, значит нужно завести еще один стек <tex>L'</tex>, в который мы и будем складывать новые элементы. После окончания перекопирования мы поменяем ролями <tex>L,L'</tex> и <tex>R,R'</tex>, на первый взгляд, все станет хорошо.
Однако, если реализовать этот алгоритм, мы получим неприятную вещь: старый стек <tex>R</tex> может и не опустошиться за это время, то есть мы получили два стека с выходными данными, а значит, возможен случай (например, если все поступающие операции {{---}} <tex>\mathtt{push}</tex>), когда при следующем перекопировании у нас не будет свободного стека для копировании копирования туда элементов <tex>L</tex>. Для преодоления этой проблемы мы принудительно будем извлекать все элементы из стека <tex>R</tex> во вспомогательный стек <tex>TS</tex>, затем копировать элементы из стека <tex>L</tex> в <tex>R</tex>, а затем обратно копировать элементы из стека <tex>TS</tex> в <tex>R</tex>. Легко показать, что приведенный алгоритм как раз получает на выходе в <tex>R</tex> все элементы стеков <tex>L,R</tex> в правильном порядке.
Но этого еще недостаточно. Если мы принудительно извлекаем элементы из стека <tex>R</tex>, появляются следующие проблемы:
# Что вернуть при операции <tex>\mathtt{pop}</tex>? Для этого заведем себе стек <tex>Rc</tex> {{---}} копию стека <tex>R</tex>, из которого мы и будем извлекать требуемые элементы.
# Как поддерживать корректность такой копии? Поскольку этот стек нужен только для перекопирования, а во время него он занят, нужна запасная копия <tex>Rc'</tex> для копирования всех элементов, которые мы копируем в <tex>R</tex>, а по окончании перекопирования поменяем ролями стеки <tex>Rc, Rc'</tex>, как мы делали со стеками <tex>L, L'</tex>.
# Как учесть, что во время перекопирования часть элементов была извлечена из <tex>Rc</tex>? Для этого заведем специальную переменную <tex>\mathtt{toCopy}</tex>, которая показывает, сколько корректных элементов находится в стеке <tex>TS</tex>, и уменьшается при каждом извлечении из <tex>TS</tex> или операции <tex>\mathtt{pop}</tex>. К счастью, все некорректные элементы будут нарастать со дна стека, так что мы никогда не извлечем некорректный элемент, если <tex>\mathtt{toCopy}>0</tex>. Если во время операции <tex>\mathtt{pop}</tex> у нас <tex>\mathtt{toCopy} = 0</tex>, это означает, что теперь в стеке <tex>R</tex> находится весь правый кусок очереди, так что нам придется извлечь элемент из него.
Теперь может возникнуть проблема с непустым <tex>Rc</tex> после завершения перекопирования. Покажем, что мы всегда успеем его опустошить, если будем использовать дополнительное извлечение из него при каждой операции в обычном режиме, для этого полностью проанализируем алгоритм.
Посмотрим на дополнительные действия, которые нам предстоят:
# Переместить содержимое <tex>R</tex> в <tex>TS</tex>, <tex>n</tex> действий.
# Переместить содержимое <tex>L</tex> в стеки <tex>R, Rc'</tex>, <tex>n + 1</tex> действий.
# Переместить первые <tex>\mathtt{toCopy}</tex> элементов из <tex>TS</tex> в <tex>R, Rc'</tex>, остальные выкинуть, <tex>n</tex> действий.
# Поменять ролями стеки <tex>Rc, Rc'</tex>, <tex>L, L'</tex>, <tex>2</tex> действия.
Теперь рассмотрим, как изменились наши стеки за весь период перекопирования. Договоримся, что операция <tex>\mathtt{empty}</tex> не меняет очередь, то есть никакие дополнительные действия не совершаются. Пусть за <tex>n</tex> следующих за активацией меняющих операций (<tex>\mathtt{push}, \mathtt{pop}</tex>) поступило <tex>x</tex> операций <tex>\mathtt{pop}</tex>, <tex>n - x</tex> операций <tex>\mathtt{push}</tex>. Очевидно, что после перекопирования в новых стеках окажется: <tex>n-x</tex> элементов в <tex>L</tex>, <tex>2 \cdot n + 1 - x = (n - x) + (n + 1)</tex> элементов в <tex>R</tex>, то есть до следующего перекопирования еще <tex>n + 2</tex> операции. С другой стороны, стек <tex>Rc</tex> содержал всего <tex>n</tex> элементов, так что мы можем очистить его, просто удаляя по одному элементу при каждой операции в обычном режиме.
Итак, очередь <tex>Q</tex> будет состоять из шести стеков <tex>L,L',R,Rc,Rc',TS</tex>, а также двух внутренних переменных <tex>\mathtt{recopy}, \mathtt{toCopy}</tex>, которые нужны для корректности перекопирования + дополнительная переменная <tex>\mathtt{copied}</tex>, показывающая, перемещали ли мы элементы из стека <tex>L</tex> в стек <tex>R</tex>, чтобы не начать перемещать эти элементы в стек <tex>TS</tex>.
Инвариант очереди (обычный режим):
# <tex>Rc</tex> {{---}} копия <tex>R</tex>
# <tex>Rc'.size < R.size - L.size</tex>
# <tex>L'.size = 0, TS.size = 0</tex>
Тогда к следующему перекопированию (<tex>L.size=R.size+1</tex>) мы гарантированно будем иметь пустые стеки <tex>L',TS,Rc'</tex>, которые нам понадобятся.
Инвариант очереди (режим перекопирования):
# <tex>Rc.size = \mathtt{toCopy}</tex>
# Если <tex>L.size = 0</tex>, то:
## При <tex>\mathtt{toCopy} > 0</tex> первые <tex>\mathtt{toCopy}</tex> элементов <tex>TS</tex> {{---}} корректны, то есть действительно содержатся в очереди.
## При <tex>\mathtt{toCopy} \leqslant 0</tex> стек <tex>R</tex> содержит весь правый кусок очереди в правильном порядке.
Также после операции в обычном режиме следует проверка на активацию перекопирования (<tex>\mathtt{recopy} = (L.size > R.size)</tex>), если это так, <tex>\mathtt{toCopy} = R.size, \mathtt{recopy} = true, \mathtt{copied} = false</tex>, совершается первый набор дополнительных действий.
После операции в режиме перекопирования следует проверка на завершение перекопирования (<tex>\mathtt{recopy} = (TS.size == 0)</tex>), а при завершении меняются ролями стеки <tex>Rc, Rc'</tex>, <tex>L, L'</tex>.
Следующий псевдокод выполняет требуемые операции:
'''function''' checkNormal():
additionalOperations()
<span style="color:#008000">// Если мы не все перекопировали, то у нас не пуст стек TS</span> recopy = TS.size <tex> \ne </tex> 0
</code>
=== additionalOperations ===
<code>
'''function''' additionalOperations():
<span style="color:#008000">// Нам достаточно 3 операций на вызов</span>
'''int''' toDo = 3
<span style="color:#008000">// Пытаемся перекопировать R в TS</span>
'''while''' '''not''' copied '''and''' toDo > 0 '''and''' R.size > 0
TS.push(R.pop())
toDo = toDo - 1
<span style="color:#008000">// Пытаемся перекопировать L в R и Rc'</span>
'''while''' toDo > 0 '''and''' L.size > 0
copied = true
Rc'.push(x)
toDo = toDo - 1
<span style="color:#008000">// Пытаемся перекопировать T S в R и Rc' с учетом toCopy</span> '''while''' toDo > 0 '''and''' TS.size > 0 '''T''' x = TS.pop()
'''if''' toCopy > 0
R.push(x)
toCopy = toCopy - 1
toDo = toDo - 1
<span style="color:#008000">// Если все скопировано, то меняем роли L, L' и Rc, Rc'</span> '''if''' TS.size == 0
swap(L, L')
swap(Rc, Rc')
Также нам нет необходимости опустошать стек <tex>R'</tex> к моменту перекопирования, так как скопировать туда новую версию <tex>R</tex> мы можем за <tex>O(1)</tex>, а освобождение ячеек памяти бессмысленно, так как они используются в других версиях персистентной очереди.
В качестве версии очереди мы будем использовать запись <tex>Q= \langle L, L', R, R', TS, \mathtt{recopy}, \mathtt{toCopy}, \mathtt{copied}\rangle</tex>, содержащую пять версий персистентных стеков и три переменных.
Пусть персистентный стек возвращает вместе с обычным результатом работы стека новую версию, то есть операция <tex>S.pop</tex> возвращает <tex>\langle Sn, x\rangle</tex>, а операция <tex>S.push(x)</tex> возвращает <tex>Sn</tex>.
'''if''' !recopy
'''stack<T>''' Ln = L.push(x)
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> Q' = <Ln, L', R, R', TS, recopy, toCopy, copied>
'''return''' Q'.checkRecopy()
'''else'''
'''stack<T>''' Ln' = L'.push(x)
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> Q' = <L, Ln', R, R', TS, recopy, toCopy, copied>
'''return''' Q'.checkNormal()
</code>
'''if''' !recopy
<Rn, x> = R.pop()
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> Q' = <L, L', Rn, R', TS, recopy, toCopy, copied>
'''return''' <Q'.checkRecopy(), x>
'''else'''
'''else'''
<Rn, x> = Rn.pop()
Q' = <L, L', Rn, Rn', TS, recopy, curCopy, copied>
'''return''' <Q'.checkNormal(), x>
</code>
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> checkRecopy():
'''if''' L.size > R.size
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> Q' = <L, L', R, R', TS, true, R.size, false>
'''return''' Q'.checkNormal()
'''else'''
'''return''' <L, L', R, R', TS, false, toCopy, copied>
</code>
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> checkNormal():
Q' = Q.additionalOperations()
<span style="color:#008000">// Если мы не все перекопировали, то у нас не пуст стек TS</span> return <Q'.L, Q'.L', Q'.R, Q'.R', Q'.TS, Q'.TS.size <tex> \ne </tex> 0, Q'.toCopy, Q'.copied>
</code>
=== additionalOperations ===
<code>
<'''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''stack<T>''', '''boolean''', '''int''', '''boolean'''> additionalOperations():
<span style="color:#008000">// Нам достаточно 3 операций на вызов</span>
'''int''' toDo = 3
<span style="color:#008000">// Пытаемся перекопировать R в TS</span>
'''stack<T>''' Rn = R
'''stack<T>''' Tn Sn = TS
'''boolean''' curCopied = copied
'''while''' '''not''' curCopied '''and''' toDo > 0 '''and''' Rn.size > 0
<Rn, x> = Rn.pop()
Tn Sn = TnSn.push(x)
toDo = toDo - 1
Ln = L
<span style="color:#008000">// Пытаемся перекопировать L в R</span>
'''while''' toDo > 0 '''and''' Ln.size > 0
curCopied = true
toDo = toDo - 1
curCopy = toCopy
<span style="color:#008000">// Пытаемся перекопировать T S в R с учетом toCopy</span> '''while''' toDo > 0 '''and''' TnSn.size > 0 <TnSn, x> = TnSn.pop()
'''if''' curCopy > 0
Rn = Rn.push(x)
toDo = toDo - 1
'''stack<T>''' Ln' = L'
<span style="color:#008000">// Если все скопировано, то меняем роли L1, L2</span> '''if''' TS.size == 0
swap(Ln, Ln')
'''return''' <Ln, Ln', Rn, R', TnSn, recopy, curCopy, curCopied>
</code>
<br clear = "all"/>
Сделаем операцию <tex> \mathtt{push(3)} </tex>, у нас обычный режим, поэтому элемент пойдет в стек <tex>L</tex>, активируется перекопирование, <tex>R' = R</tex>, за три операции мы успеваем перекопировать элемент стека <tex>R</tex> в стек <tex>TS</tex>, а также перекопировать два элемента стека <tex>L</tex> в стек <tex>R</tex>.
[[Файл:PersistentQueue_state3.png|300px|nothumb|left|]]
<br clear = "all"/>
Сделаем операцию <tex> \mathtt{push(4)} </tex>, мы в режиме перекопирования, поэтому элемент пойдет в стек <tex>L'</tex>, далее мы успеваем перекопировать обратно элемент из стека <tex>TS</tex> в стек <tex>R</tex>, перекопирование завершается, стеки <tex>L, L'</tex> меняются местами.
[[Файл:PersistentQueue_state4.png|300px|nothumb|left|]]
<br clear = "all"/>
Сделаем операцию <tex> \mathtt{push(7)} </tex>, у нас обычный режим, поэтому элемент пойдет в стек <tex>L</tex>, перекопирование активируется, <tex>R' = R</tex>, <tex>\mathtt{toCopy} = 3</tex>, за три операции мы успеваем перекопировать содержимое стека <tex>R</tex> в стек <tex>TS</tex>.
[[Файл:PersistentQueue_state7.png|300px|nothumb|left|]]
<br clear = "all"/>
Сделаем операцию <tex>\mathtt{pop}</tex>, мы находимся в режиме перекопирования, так что элемент извлекается из <tex>R'</tex>, <tex>\mathtt{toCopy} = 1</tex>. За три операции мы успеваем перекопировать один элемент стека <tex>L</tex> в стек <tex>R</tex>, а также извлечь два элемента стека <tex>TS</tex>, с учетом <tex>\mathtt{toCopy}</tex> только один элемент попадет в стек <tex>R</tex>, <tex>\mathtt{toCopy} = 0</tex>.
[[Файл:PersistentQueue_State9.png|300px|nothumb|left|]]
<br clear = "all"/>
Сделаем операцию <tex>\mathtt{pop}</tex>, мы находимся в режиме перекопирования, так что элемент извлекается из <tex>R'</tex>, но <tex>\mathtt{toCopy} = 0</tex>, так что нам приходится извлечь еше один элемент из стека <tex>R</tex>. Мы извлекаем один элемент из стека <tex>TS</tex>, перекопирование заканчивается, стеки <tex>L, L'</tex> меняются местами.
[[Файл:PersistentQueue_state10.png|300px|nothumb|left|]]
== Источники информации ==
* [http://hdl.handle.net/1813/6273 ''Hood R., Melville R.'' Real Time Queue Operations in Pure LISP. {{---}} Cornell University, 1980]
* [http://habrahabr.ru/post/241231 Хабрахабр {{---}} Персистентная очередь]
* [http://codeforces.com/blog/entry/15685 Codeforces {{---}} Персистентная очередь и её друзья]
[[Категория: Дискретная математика и алгоритмы]]
[[Категория: Амортизационный анализ]]
[[Категория: Структуры данных‏‎]]
24
правки

Навигация