Очередь
Определение
Очередь (Queue) — это структура данных, добавление и удаление элементов в которой происходит путём операций Push и Pop соответственно. Притом первым из очереди удаляется элемент, который был помещен туда первым, то есть в очереди реализуется принцип «первым вошел — первым вышел» (first-in, first-out — FIFO). У очереди имеется голова (head) и хвост (tail). Когда элемент ставится в очередь, он занимает место в её хвосте. Из очереди всегда выводится элемент, который находится в ее голове.
- (запись в очередь) - операция вставки нового элемента.
- (снятие с очереди) - операция удаления нового элемента.
- - проверка очереди на наличие в ней элементов
Реализация на массиве
Очередь, способную вместить не более
элементов, можно реализовать с помощью массива . Она будет обладать следующими полями:- (голова очереди)
- (хвост очереди)
- (размер очереди)
push
push(x) elements[tail] = x tail = (tail + 1) % elements.length size++
pop
pop() if !empty() x = elements[head] head = (head + 1) % elements.length size-- return x
empty
empty() return size == 0
Из-за того что нам не нужно перевыделять память, каждая операция выполняется за
времени.Плюсы:
- - прост в разработке
- - по сравнению с реализацией на списке, есть незначительная экономия памяти
Минусы:
- - количество элементов в очереди ограничено размером массива (исправляется написанием функции расширения массива)
- - при переполнении очереди требуется перевыделение памяти и копирование всех элементов в новый массив
Реализация на списке
Для данной реализации очереди необходимо создать список (
) и операции работы на созданном списке.Реализация очереди на односвязном списке:
list
- - поле, в котором хранится значение элемента
- - указатель на следующий элемент очереди
push
push(x) element = tail tail = new list(x, NULL) if size == 0 head = tail else element.next = tail size++
pop
pop() if empty() return element = head head = head.next size-- return element
empty
empty() return size == 0
Каждая операция выполняется за время
.Минусы:
- Память фрагментируется гораздо сильнее и последовательная итерация по такой очереди может быть ощутимо медленнее, нежели итерация по очереди реализованной на массиве
Реализация на двух стеках
Очередь можно реализовать на двух стеках и . Один из стеков будем использовать для операции , другой для операции . При этом, если при попытке извлечения элемента из он оказался пустым, просто перенесем все элементы из в него (при этом элементы в получатся уже в обратном порядке, что нам и нужно для извлечения элементов, а станет пустым).
- и - функции, реализующие операцию для соответствующего стека;
- и - аналогично операции .
push
push(x) pushLeft(x)
pop
if !rigthStack.empty() return popRight() else while !leftStack.empty() pushRight(popLeft()) return popRight()
При выполнении операции
будем использовать три монеты: одну для самой операции, вторую в качестве резерва на операцию из первого стека, третью во второй стек на финальный . Тогда для операций учётную стоимость можно принять равной нулю и использовать для операции монеты, оставшиеся после операции .Таким образом, для каждой операции требуется
монет, а значит, амортизационная стоимость операций .Минусы:
- Если не пуст, то операция может выполняться времени, в отличии от других реализаций, где всегда выполняется за
Реализация на шести стеках
Одним из минусов реализации на двух стеках является то, что в худшем случае мы тратим
времени на операцию. Если распределить время, необходимое для перемещения элементов из одного стека в другой, по операциям, мы получим очередь без худших случаев с истинного времени на операцию.Пусть мы имеем стеки
, причем стеки используются для операций , стек используется для операций , стеки используются в качестве копий стека , стек используется для перекопирования элементов.В каждый момент времени в очереди зафиксировано какой
из стеков и используется для помещения туда элементов, пришедших с операцией , а также какой из стеков и является в данный момент точной копией стека .Также очередь будет запоминать, находится ли она сейчас в режиме перекопирования (recopy mode), в который переходит из обычного режима, когда после очередной операции в стеке
становится больше элементов, чем в стеке . При активации инициализируется счетчик , показывающий, сколько находится неизвлеченных элементов в стеке .Пусть в этот момент в стеке
, а значит и в стеке находились элементов, тогда в стеке их . Обрабатываем поступающие дальше операции следующим образом: кладет элемент в парный нашему стеку стек , а извлекает элемент только из , при этом уменьшая счетчик , в этом режиме всегда возвращает , так как перекопирование закончится раньше, чем опустошится , а у нас еще есть элементы в .Заметим, что мы можем корректно обработать все операции
и первые операций , то есть у нас есть на перекопирование не меньше операции вместе с активирующей.Корректной ситуация станет, когда в стеках
и парном стеке окажутся все находящиеся на момент активации в очереди и не извлеченные после активации элементы.Чтобы получить корректную ситуацию и перейти в обычный режим, нужно:
- Извлечь весь стек в стек , действий.
- Извлечь весь стек в стеки , действие.
- Извлечь элементов в стеки , а оставшиеся выкинуть, действий.
- Назначить текущей копией стека , а — текущим стеком для операций , действия.
Таким образом, получили
действий на операций с очередью, то есть выполняя 3 дополнительных действия во время операции мы успеем перекопировать все элементы вовремя. Тогда очередь действительно будет выполнять каждое действие за реального времени.Теперь рассмотрим, какие изменения произошли за время перекопирования. Пусть среди
следующих за активацией операций у нас операций и операций . Тогда в стеке оказалось элементов, а в новом стеке оказалось элементов. Тогда в стеке на больше элементов, чем в стеке , а это значит, что до следующего режима перекопирования операции, и за это время мы успеем очистить старый стек , в котором находится максимум ненужных элементов, просто удаляя при каждой операции в обычном режиме один элемент из , если он непуст.Пусть наша очередь
имеет стеки , а также переменные и , тогда вышеприведенный алгоритм реализует следующий псевдокод:
empty() return !recopy and R.size == 0
push(x) if !recopy L1.push(x) checkRecopy() else L2.push(x) checkNormal()
pop() if !recopy tmp = R.pop() Rc1.pop() checkRecopy() return tmp else tmp = Rc1.pop() toCopy = toCopy - 1 checkNormal() return tmp
checkRecopy() if Rc2.size > 0 Rc2.pop() recopy = L1.size > R.size if recopy toCopy = Rc1.size additionalOperations()
checkNormal() additionalOperations() // Если мы не все перекопировали, то у нас не пуст стек T recopy = T.size != 0
additionalOperations() // Нас достаточно 3 операций на вызов toDo = 3 // Пытаемся перекопировать R в T while toDo > 0 and R.size > 0 T.push(R.pop()) toDo = toDo - 1 // Пытаемся перекопировать L1 в R и Rc2 while toDo > 0 and L1.size > 0 x = L1.pop() R.push(x) Rc2.push(x) toDo = toDo - 1 // Пытаемся перекопировать T в R и Rc2 с учетом toCopy while toDo > 0 and T.size > 0 x = T.pop() if toCopy > 0 R.push(x) Rc2.push(x) toCopy = toCopy - 1 toDo = toDo - 1 // Если все скопировано, то меняем роли L1, L2 и Rc1, Rc2 if toDo > 0 swap(L1, L2) swap(Rc1, Rc2)
См. также
Ссылки
- Википедия - Очередь (программирование)
- Т. Кормен. «Алгоритмы. Построение и анализ» второе издание, Глава 10.1, стр. 262
- T. H. Cormen. «Introduction to Algorithms» third edition, Chapter 10.1, p. 262