Персистентная очередь
После того, как мы получили очередь в реальном времени с обычными стеками, ее можно легко превратить в персистентную, сделав все стеки персистентными, но на самом деле персистентность позволяет не создавать явной копии стека, так что достаточно всего пяти стеков.
Содержание
[убрать]Эффективная реализация
Будем отталкиваться от реализации на двух стеках. Пусть у нас есть стек для операций , стек для операций . К моменту опустошения стека нам нужно получить стек , содержащий все элементы стека в правильном для извлечения порядке. Этого можно добиться, если переместить все элементы стека , в стек , назовем такой процесс перекопированием, а очередь будет в это время находится в режиме перекопирования (recopy mode). Режим перекопирования активируется, когда во время очередной операции мы уже можем не успеть переместить нужные элементы, а именно . Состояние очереди будет фиксировать логическая переменная .
Теперь рассмотрим как обрабатывать операции во время перекопирования. В режиме перекопирования операция
, так как мы не можем извлекать элементы, находившиеся в не пустом стеке , значит очередь всегда не пуста. Договоримся также, что операция не меняет внутреннюю структуру очереди и не создает новой версии очереди.Поскольку стек
во время перемещения содержимого теряет свою структуру, мы не сможем положить туда элементы, пришедшие с . Для таких элементов заведем специальный стек в который будем складывать поступающие элементы, а после перекопирования обменяем его ролями с .Этот алгоритм почти работает, но есть проблема: никто не гарантирует, что стек
опустошится за время перекопирования, то есть мы получим в итоге два стека с выходными данными, а значит во время следующего перекопирования у нас может не быть свободного стека (например, если все операции были ). Избавимся от этой проблемы следующим образом: мы принудительно извлечем все элементы стека во вспомогательный стек , затем переместим все элементы стека в стек , затем обратно переместим все элементы в стек . Легко показать, что такая последовательность действий приведет к правильному для извлечения порядку элементов в .Теперь разберемся с операциями
. Теперь у нас теряет свою структуру стек , значит нужна его копия для извлечения элементов. Без персистентности нам бы пришлось создавать такую копию параллельно с самим стеком , но теперь мы можем просто записать в последнюю версию стека и дальше извлекать элементы уже из нее. Чтобы учесть извлеченные из копии элементы, будем использовать переменную , будем уменьшать ее на при каждом извлечении из и операциях . Так как извлеченные элементы будут нарастать со дна стека , мы никогда не извлечем некорректный элемент, если .Теперь проанализируем наш алгоритм. Пусть в стеке
на начало перекопирования содержатся элементов, тогда в стеке находится элемент. Мы можем корректно обработать все операции , а также операций , операции мы не учитываем. Тогда мы должны завершить перекопирование не больше, чем за операцию.Всего в режиме перекопирования нужно:
- Переместить содержимое в , времени, памяти.
- Переместить содержимое в , времени, памяти.
- Переместить первые элементов из стека в стек , остальные выкинуть, времени, памяти.
- Поменять ролями стеки , времени.
Таким образом, мы получили
времени и памяти на операцию, то есть времени и памяти на операцию. Совершая три дополнительных действия вместе с каждой операцией в режиме перекопирования мы гарантируем, что перекопирование завершится до опустошения стека .Теперь проверим корректность условия активации. Пусть среди
следующих за активацией операций присуствует операций и операций . Тогда, очевидно, в стеке окажется элементов, а в стеке станет элементов, то есть на элемент меньше, чем в стеке , а значит до следующего перекопирования операции.Заметим, что теперь если очередь находится в обычном режиме,
, значит .Итак, очередь
будет состоять из пяти стеков , а также двух внутренних переменных , которые нужны для корректности перекопирования.Инвариант очереди (обычный режим):
- Стек содержит левую половину очереди, порядок при извлечении обратный.
- Стек содержит правую половину очереди, порядок при извлечении прямой.
Инвариант очереди (режим перекопирования):
- Если , то первые элементов — корректны, то есть действительно содержатся в очереди.
Очередь будет работать в двух режимах:
- Обычный режим, кладем в , извлекаем из , операция .
- Режим перекопирования, кладем в , извлекаем из , , совершаем дополнительные действия.
Также после операции в обычном режиме следует проверка на активацию перекопирования(
), если это так, , совершается первый набор дополнительных действий.После операции в режиме перекопирования следует проверка на завершение перекопирования (
), а при завершении меняются ролями стеки .В качестве версии очереди мы будем использовать запись
, содержащую пять версий стеков и две переменных. Пусть персистентный стек возвращает вместе с обычным результатом стека новую версию, то есть операция возвращает , а операция возвращает , аналогично свою новую версию возвращает и персистентная очередь.Следующий псевдокод выполняет требуемые операции:
empty
empty()
return !recopy and R.size == 0
push
push(x)
if !recopy:
Ln = L.push(x)
Q' = <Ln, L', R, R', T, recopy, toCopy>
return Q'.checkRecopy()
else:
Ln' = L'.push(x)
Q' = <L, Ln', R, R', T, recopy, toCopy>
return Q'.checkNormal()
pop
pop()
if !recopy:
<Rn, x> = R.pop()
Q' = <L, L', Rn, R', T, recopy, toCopy>
return <Q'.checkRecopy(), x>
else:
<Rn', x> = R'.pop()
Q' = <L, L', R, Rn', T, recopy, toCopy - 1>
return <Q'.checkNormal(), x>
checkRecopy
checkRecopy()
if L.size > R.size:
Q' = <L, L', R, R', T, true, R.size>
return Q'.additionalOperations()
else:
return <L, L', R, R', T, false, toCopy>
checkNormal
checkNormal()
Q' = Q.additionalOperations()
// Если мы не все перекопировали, то у нас не пуст стек T
return <Q'.L, Q'.L', Q'.R, Q'.R', Q'.T, Q'.T.size
0, Q'.toCopy>
additionalOperations
additionalOperations()
// Нам достаточно 3 операций на вызов
toDo = 3
// Пытаемся перекопировать R в T
Rn = R
Tn = T
while toDo > 0 and Rn.size > 0:
<Rn, x> = Rn.pop()
Tn = Tn.push(x)
toDo = toDo - 1
Ln = L
// Пытаемся перекопировать L в R
while toDo > 0 and Ln.size > 0:
<Ln, x> = Ln.pop()
Rn = Rn.push(x)
toDo = toDo - 1
curCopy = toCopy
// Пытаемся перекопировать T в R с учетом toCopy
while toDo > 0 and Tn.size > 0:
<Tn, x> = Tn.pop()
if curCopy > 0:
Rn = Rn.push(x)
curCopy = curCopy - 1
toDo = toDo - 1
Ln' = L'
// Если все скопировано, то меняем роли L1, L2
if T.size == 0:
swap(Ln, Ln')
return <Ln, Ln', Rn, R', Tn, recopy, curCopy>