Программа CircleQ демонстрирует этот подход к реализации циклической очереди. Введите строку и нажмите кнопку Enter (Ввести) для добавления нового элемента в очередь. Нажмите на кнопку Leave (Покинуть) для удаления верхнего элемента из очереди. Программа будет при необходимости изменять размер очереди.
Программа CircleQ2 аналогична программе CircleQ, но она использует для работы с очередью класс CircleQueue.
Помните, что при каждом изменении размера очереди в программе, она копирует элементы во временный массив, изменяет размер очереди, а затем копирует элементы обратно. Эти дополнительные шаги делают изменение размера циклических очередей более медленным, чем изменение размера связных списков и стеков. Даже очереди на основе массивов, в которых также требуются дополнительные действия для изменения размера, не требуют такого объема работы.
С другой стороны, если число элементов в очереди не сильно меняется, и если правильно задать параметры изменения размера, может никогда не понадобиться менять размер массива. Даже если иногда это все‑таки придется делать, уменьшение частоты этих изменений стоит дополнительных усилий на программирование.
Совсем другой подход к реализации очередей состоит в использовании двусвязных списков. Для отслеживания начала и конца списка можно использовать сигнальные метки. Новые элементы добавляются в очередь перед меткой в конце очереди, а элементы, следующие за меткой начала очереди, удаляются. На рис. 3.7 показан двусвязный список, который используется в качестве очереди.
===========58-59
Добавлять и удалять элементы из двусвязного списка легко, поэтому в этом случае не потребуется применять сложных алгоритмов для изменения размера. Преимущество этого метода также в том, что он интуитивно понятнее по сравнению с циклической очередью на основе массива. Недостаток его в том, что для указателей связного списка NextCell и PrevCell требуется дополнительная память. В отношении занимаемой памяти очереди на основе связных списков немного менее эффективны, чем циклические списки.
Программа LinkedQ работает с очередью при помощи двусвязного списка. Введите строку, нажмите на кнопку Enter, чтобы добавить элемент в конец очереди. Нажмите на кнопку Leave для удаления элемента из очереди.
Программа LinkedQ2 аналогична программе LinkedQ, но она использует для управления очередью класс LinkedListqueue.
Коллекции Visual Basic представляют собой очень простую форму очереди. Программа может использовать метод Add коллекции для добавления элемента в конец очереди, и метод Remove с параметром 1 для удаления первого элемента из очереди. Следующий код управляет очередью на основе коллекций:
Dim Queue As New Collection
Private Sub EnterQueue(value As String)
Queue.Add value
End Sub
Private Function LeaveQueue() As String
LeaveQueue = Queue.Item(1)
Queue.Remove 1
Еnd Function
@Рис. 3.7. Очередь на основе связного списка
=======60
Несмотря на то, что этот код очень прост, коллекции в действительности не предназначены для использования в качестве очередей. Они предоставляют дополнительные возможности, такие как ключи элементов, и поддержка этих дополнительных возможностей делает коллекции более медленными, чем другие реализации очередей. Тем не менее, очереди на основе коллекций настолько просты, что они могут быть приемлемым выбором для приложений, в которых производительность не является проблемой.
Программа CollectQ демонстрирует очередь на основе коллекций.
Каждый элемент в приоритетной очереди (priority queue) имеет связанный с ним приоритет. Если программе нужно удалить элемент из очереди, она выбирает элемент с наивысшим приоритетом. Как хранятся элементы в приоритетной очереди, не имеет значения, если программа всегда может найти элемент с наивысшим приоритетом.
Некоторые операционные системы использую приоритетные очереди для планирования заданий. В операционной системе UNIX все процессы имеют разные приоритеты. Когда процессор освобождается, выбирается готовый к исполнению процесс с наивысшим приоритетом. Процессы с более низким приоритетом должны ждать завершения или блокировки (например, при ожидании внешнего события, такого как чтение данных с диска) процессов с более высокими приоритетами.
Концепция приоритетных очередей также используется при управлении авиаперевозками. Наивысший приоритет имеют самолеты, у которых кончается топливо во время посадки. Второй приоритет имеют самолеты, заходящие на посадку. Третий приоритет имеют самолеты, находящиеся на земле, так как они находятся в более безопасном положении, чем самолеты в воздухе. Приоритеты изменяются со временем, так как у самолетов, которые пытаются приземлиться, в конце концов, закончится топливо.
Простой способ организации приоритетной очереди — поместить все элементы в список. Если требуется удалить элемент из очереди, можно найти в списке элемент с наивысшем приоритетом. Чтобы добавить элемент в очередь, он помещается в начало списка. При использовании этого метода, для добавления нового элемента в очередь требуется только один шаг. Чтобы найти и удалить элемент с наивысшим приоритетом, требуется O(N) шагов, если очередь содержит N элементов.
Немного лучше была бы схема с использованием связного списка, в котором элементы были бы упорядочены в прямом или обратном порядке. Используемый в списке класс PriorityCell мог бы объявлять переменные следующим образом:
Public Priority As Integer ' Приоритет элемента.
Public NextCell As PriorityCell ' Указатель на следующий элемент.
Public Value As String ' Данные, нужные программе.
Чтобы добавить элемент в очередь, нужно найти его правильное положение в списке и поместить его туда. Чтобы упростить поиск положения элемента, можно использовать сигнальные метки в начале и конце списка, присвоив им соответствующие приоритеты. Например, если элементы имеют приоритеты от 0 до 100, можно присвоить метке начала приоритет 101 и метке конца — приоритет ‑1. Приоритеты всех реальных элементов будут находиться между этими значениями.
На рис. 3.8 показана приоритетная очередь, реализованная на основе связного списка.
=====61
@Рис. 3.8. Приоритетная очередь на основе связного списка
Следующий фрагмент кода показывает ядро этой процедуры поиска:
Dim cell As PriorityCell
Dim nxt As PriorityCell
' Найти место элемента в списке.
cell = TopSentinel
nxt = cell.NextCell
Do While cell.Priority > new_priority
cell = nxt
nxt = cell.NextCell
Loop
' Вставить элемент после ячейки в списке.
:
Для удаления из списка элемента с наивысшим приоритетом, просто удаляется элемент после сигнальной метки начала. Так как список отсортирован в порядке приоритетов, первый элемент всегда имеет наивысший приоритет.
Добавление нового элемента в эту очередь занимает в среднем N/2 шагов. Иногда новый элемент будет оказываться в начале списка, иногда ближе к концу, но в среднем он будет оказываться где‑то в середине. Простая очередь на основе списка требовала O(1) шагов для добавления нового элемента и O(N) шагов для удаления элементов с наивысшим приоритетом из очереди. Версия на основе упорядоченного связного списка требует O(N) шагов для добавления элемента и O(1) шагов для удаления верхнего элемента. Обеим версиям требует O(N) шагов для одной из этих операций, но в случае упорядоченного связного списка в среднем требуется только (N/2) шагов.
Программа PriList использует упорядоченный связный список для работы с приоритетной очередью. Вы можете задать приоритет и значение элемента данных и нажать кнопку Enter для добавления его в приоритетную очередь. Нажмите на кнопку Leave для удаления из очереди элемента с наивысшим приоритетом.
Программа PriList2 аналогична программе PriList, но она использует для управления очередью класс LinkedPriorityQueue.
========63
Затратив еще немного усилий, можно построить приоритетную очередь, в которой добавление и удаление элемента потребуют порядка O(log(N)) шагов. Для очень больших очередей, ускорение работы может стоить этих усилий. Этот тип приоритетных очередей использует структуры данных в виде пирамиды, которые также применяются в алгоритме пирамидальной сортировки. Пирамиды и приоритетные очереди на их основе обсуждаются более подробно в 9 главе.
Интересной разновидностью очередей являются многопоточные очереди (multi‑headed queues). Элементы, как обычно, добавляются в конец очереди, но очередь имеет несколько потоков (front end) или голов (heads). Программа может удалять элементы из любого потока.
Примером многопоточной очереди в обычной жизни является очередь клиентов в банке. Все клиенты находятся в одной очереди, но их обслуживает несколько служащих. Освободившийся банковский работник обслуживает клиента, который находится в очереди первым. Такой порядок обслуживания кажется справедливым, поскольку клиенты обслуживаются в порядке прибытия. Он также эффективен, так как все служащие остаются занятыми до тех пор, пока клиенты ждут в очереди.
Сравните этот тип очереди с несколькими однопоточными очередями в супермаркете, в которых покупатели не обязательно обслуживаются в порядке прибытия. Покупатель в медленно движущейся очереди, может прождать дольше, чем тот, который подошел позже, но оказался в очереди, которая продвигается быстрее. Кассиры также могут быть не всегда заняты, так как какая‑либо очередь может оказаться пустой, тогда как в других еще будут находиться покупатели.
В общем случае, многопоточные очереди более эффективны, чем несколько однопоточных очередей. Последний вариант используется в супермаркетах потому, что тележки для покупок занимают много места. При использовании многопоточной очереди всем покупателям пришлось бы построиться в одну очередь. Когда кассир освободится, покупателю пришлось бы переместиться с громоздкой тележкой к кассиру. С другой стороны, в банке посетителям не нужно двигать большие тележки для покупок, поэтому они легко могут уместиться в одной очереди.