В информатике приоритетная очередь — это абстрактный тип данных, аналогичный обычной очереди или стековому абстрактному типу данных. Каждый элемент в приоритетной очереди имеет связанный с ним приоритет. В приоритетной очереди элементы с высоким приоритетом обслуживаются раньше элементов с низким приоритетом. В некоторых реализациях, если два элемента имеют одинаковый приоритет, они обслуживаются в том же порядке, в котором они были поставлены в очередь. В других реализациях порядок элементов с одинаковым приоритетом не определен.
Хотя приоритетные очереди часто реализуются с использованием куч , они концептуально отличаются от куч. Приоритетная очередь — это абстрактный тип данных, такой как список или карта ; так же, как список может быть реализован с помощью связанного списка или массива , приоритетная очередь может быть реализована с помощью кучи или другого метода, такого как упорядоченный массив.
Приоритетная очередь должна как минимум поддерживать следующие операции:
Кроме того, peek (в этом контексте часто называемый find-max или find-min ), который возвращает элемент с наивысшим приоритетом, но не изменяет очередь, очень часто реализуется и почти всегда выполняется за время O (1) . Эта операция и ее производительность O (1) имеют решающее значение для многих приложений приоритетных очередей.
Более продвинутые реализации могут поддерживать более сложные операции, такие как pull_lowest_priority_element , проверку первых нескольких элементов с наивысшим или наименьшим приоритетом, очистку очереди, очистку подмножеств очереди, выполнение пакетной вставки, объединение двух или более очередей в одну, увеличение приоритета любого элемента и т. д.
Стеки и очереди могут быть реализованы как особые виды очередей приоритетов, где приоритет определяется порядком, в котором вставляются элементы. В стеке приоритет каждого вставленного элемента монотонно увеличивается; таким образом, последний вставленный элемент всегда извлекается первым. В очереди приоритет каждого вставленного элемента монотонно уменьшается; таким образом, первый вставленный элемент всегда извлекается первым.
Существует множество простых, обычно неэффективных способов реализации приоритетной очереди. Они предоставляют аналогию, помогающую понять, что такое приоритетная очередь.
Например, можно хранить все элементы в несортированном списке ( время вставки O (1)). Всякий раз, когда запрашивается элемент с наивысшим приоритетом, выполняется поиск по всем элементам для элемента с наивысшим приоритетом. ( время извлечения O ( n )),
вставить (узел) { список.добавить(узел)}
тянуть () { наивысший = список.получить_первый_элемент() foreach узел в списке { если наивысший.приоритет < приоритет.узла { наивысший = узел } } список.удалить(самый высокий) возвращение наивысшего}
В другом случае можно сохранить все элементы в списке, отсортированном по приоритету ( время сортировки вставкой O (n)), и всякий раз, когда запрашивается элемент с наивысшим приоритетом, может быть возвращен первый элемент в списке ( время извлечения O (1)) .
вставить (узел) { foreach (индекс, элемент) в списке { если узел.приоритет < элемент.приоритет { список.вставить_в_индекс(узел,индекс) перерыв } }}
тянуть () { самый высокий = список.получить_по_индексу(список.длина-1) список.удалить(самый высокий) возвращение наивысшего}
Для повышения производительности очереди с приоритетами обычно основаны на куче , что дает производительность O (log n ) для вставок и удалений и O ( n ) для первоначального построения кучи из набора из n элементов. Варианты базовой структуры данных кучи, такие как парные кучи или кучи Фибоначчи, могут обеспечить лучшие границы для некоторых операций. [1]
В качестве альтернативы, когда используется самобалансирующееся двоичное дерево поиска , вставка и удаление также занимают O (log n ) времени, хотя построение деревьев из существующих последовательностей элементов занимает O ( n log n ) времени; это типично, когда у вас уже может быть доступ к этим структурам данных, например, с помощью сторонних или стандартных библиотек. С точки зрения сложности пространства использование самобалансирующегося двоичного дерева поиска со связанным списком требует больше памяти, поскольку оно требует хранения дополнительных ссылок на другие узлы.
С точки зрения вычислительной сложности приоритетные очереди соответствуют алгоритмам сортировки. Раздел об эквивалентности приоритетных очередей и алгоритмов сортировки ниже описывает, как эффективные алгоритмы сортировки могут создавать эффективные приоритетные очереди.
Существует несколько специализированных структур данных кучи , которые либо предоставляют дополнительные операции, либо превосходят реализации на основе кучи для определенных типов ключей, в частности, целочисленных ключей. Предположим, что набор возможных ключей — это {1, 2, ..., C}.
Для приложений, которые выполняют много операций " peek " для каждой операции "extract-min", временная сложность для действий peek может быть снижена до O (1) во всех реализациях дерева и кучи путем кэширования элемента с наивысшим приоритетом после каждой вставки и удаления. Для вставки это добавляет максимум постоянную стоимость, поскольку вновь вставленный элемент сравнивается только с ранее кэшированным минимальным элементом. Для удаления это добавляет максимум дополнительную стоимость "peek", которая обычно дешевле стоимости удаления, поэтому общая временная сложность не сильно влияет.
Монотонные очереди с приоритетами — это специализированные очереди, оптимизированные для случая, когда ни один элемент не вставляется с более низким приоритетом (в случае min-heap), чем любой ранее извлеченный элемент. Это ограничение удовлетворяется несколькими практическими применениями очередей с приоритетами.
Вот временные сложности [5] различных структур данных кучи. Сокращение am. указывает на то, что данная сложность амортизирована, в противном случае это сложность наихудшего случая. Для значения " O ( f )" и " Θ ( f )" см . нотацию Big O. Имена операций предполагают минимальную кучу.
Семантика приоритетных очередей естественным образом предполагает метод сортировки: вставьте все элементы, которые должны быть отсортированы, в приоритетную очередь и последовательно удалите их; они выйдут в отсортированном порядке. Это на самом деле процедура, используемая несколькими алгоритмами сортировки , как только слой абстракции, предоставляемый приоритетной очередью, удален. Этот метод сортировки эквивалентен следующим алгоритмам сортировки:
Алгоритм сортировки также может быть использован для реализации очереди приоритетов. В частности, Торуп говорит: [21]
Мы представляем общее детерминированное линейное сокращение пространства от очередей с приоритетами до сортировки, подразумевающее, что если мы можем сортировать до n ключей за время S ( n ) на ключ, то существует очередь с приоритетами, поддерживающая удаление и вставку за время O ( S ( n )) и поиск минимума за постоянное время.
То есть, если есть алгоритм сортировки, который может сортировать за O ( S ) времени на ключ, где S является некоторой функцией от n и размера слова , [22] то можно использовать данную процедуру для создания очереди с приоритетами, где извлечение элемента с наивысшим приоритетом занимает O (1) времени, а вставка новых элементов (и удаление элементов) занимает O ( S ) времени. Например, если есть алгоритм сортировки O ( n log n ), можно создать очередь с приоритетами с O (1) извлечением и O ( log n ) вставкой.
Приоритетную очередь часто считают « контейнерной структурой данных ».
Стандартная библиотека шаблонов (STL) и стандарт C++ 1998 определяют std::priority_queue как один из шаблонов классов адаптеров контейнеров STL . Однако он не определяет, как должны обслуживаться два элемента с одинаковым приоритетом, и, действительно, общие реализации не будут возвращать их в соответствии с их порядком в очереди. Он реализует очередь с максимальным приоритетом и имеет три параметра: объект сравнения для сортировки, такой как объект функции (по умолчанию less<T>, если не указано), базовый контейнер для хранения структур данных (по умолчанию std::vector<T>) и два итератора в начало и конец последовательности. В отличие от реальных контейнеров STL, он не допускает итерации своих элементов (он строго придерживается своего определения абстрактного типа данных). STL также имеет служебные функции для управления другим контейнером с произвольным доступом как двоичной max-heap. Библиотеки Boost также имеют реализацию в библиотечной куче.
Модуль Python heapq реализует двоичную минимальную кучу поверх списка.
Библиотека JavaPriorityQueue
содержит класс, который реализует очередь с минимальным приоритетом в виде двоичной кучи.
Библиотека .NET содержит класс PriorityQueue, который реализует четвертичную минимальную кучу на основе массива.
Библиотека Scala содержит класс PriorityQueue, который реализует очередь с максимальным приоритетом.
Библиотека Go содержит модуль контейнера/кучи, который реализует минимальную кучу поверх любой совместимой структуры данных.
Расширение стандартной библиотеки PHP содержит класс SplPriorityQueue.
Фреймворк Core Foundation компании Apple содержит структуру CFBinaryHeap, которая реализует минимальную кучу.
Приоритетная очередь может использоваться для управления ограниченными ресурсами, такими как полоса пропускания на линии передачи от сетевого маршрутизатора . В случае возникновения очереди исходящего трафика из-за недостаточной пропускной способности все остальные очереди могут быть остановлены для отправки трафика из очереди с наивысшим приоритетом по прибытии. Это гарантирует, что приоритетный трафик (такой как трафик в реальном времени, например, поток RTP VoIP- соединения) будет пересылаться с наименьшей задержкой и наименьшей вероятностью отклонения из-за достижения очередью максимальной емкости. Весь остальной трафик может обрабатываться, когда очередь с наивысшим приоритетом пуста. Другой используемый подход заключается в отправке непропорционально большего количества трафика из очередей с более высоким приоритетом.
Многие современные протоколы для локальных сетей также включают концепцию приоритетных очередей на подуровне управления доступом к среде (MAC), чтобы гарантировать, что высокоприоритетные приложения (такие как VoIP или IPTV ) испытывают меньшую задержку, чем другие приложения, которые могут обслуживаться с наилучшими усилиями . Примерами являются IEEE 802.11e (поправка к IEEE 802.11 , которая обеспечивает качество обслуживания ) и ITU-T G.hn (стандарт для высокоскоростной локальной сети, использующей существующую домашнюю проводку ( линии электропередач , телефонные линии и коаксиальные кабели ).
Обычно ограничение (policer) устанавливается для ограничения полосы пропускания, которую может занять трафик из очереди с наивысшим приоритетом, чтобы не допустить, чтобы пакеты с высоким приоритетом перекрывали весь остальной трафик. Этот предел обычно никогда не достигается из-за высокоуровневых контрольных экземпляров, таких как Cisco Callmanager, которые можно запрограммировать на блокировку вызовов, которые могут превысить запрограммированный предел полосы пропускания.
Другое применение приоритетной очереди — управление событиями в дискретном моделировании событий . События добавляются в очередь, а время их моделирования используется в качестве приоритета. Выполнение моделирования происходит путем многократного вытягивания вершины очереди и выполнения события на ней.
См. также : Планирование (вычисления) , теория очередей
Если граф хранится в виде списка смежности или матрицы, то для эффективного извлечения минимума при реализации алгоритма Дейкстры можно использовать приоритетную очередь , хотя также необходима возможность эффективно изменять приоритет конкретной вершины в приоритетной очереди.
Если вместо этого граф хранится как объекты узлов, а пары приоритет-узел вставляются в кучу, изменение приоритета конкретной вершины не требуется, если отслеживаются посещенные узлы. После посещения узла, если он снова появляется в куче (имея ранее связанный с ним более низкий номер приоритета), он выталкивается и игнорируется.
Кодирование Хаффмана требует многократного получения двух деревьев с самой низкой частотой. Приоритетная очередь — один из методов сделать это .
Алгоритмы поиска «лучший первый» , такие как алгоритм поиска A* , находят кратчайший путь между двумя вершинами или узлами взвешенного графа , пробуя сначала наиболее перспективные маршруты. Очередь приоритетов (также известная как fringe ) используется для отслеживания неисследованных маршрутов; тот, для которого оценка (нижняя граница в случае A*) общей длины пути наименьшая, получает наивысший приоритет. Если ограничения памяти делают поиск «лучший первый» непрактичным, вместо него можно использовать варианты, такие как алгоритм SMA* , с двухсторонней очередью приоритетов , чтобы разрешить удаление элементов с низким приоритетом.
Алгоритм Real-time Optimally Adapting Meshes ( ROAM ) вычисляет динамически изменяющуюся триангуляцию рельефа. Он работает путем разделения треугольников, где требуется больше деталей, и их объединения, где требуется меньше деталей. Алгоритм назначает каждому треугольнику на рельефе приоритет, обычно связанный с уменьшением ошибки, если бы этот треугольник был разделен. Алгоритм использует две очереди приоритетов, одну для треугольников, которые можно разделить, и другую для треугольников, которые можно объединить. На каждом шаге треугольник из очереди разделения с наивысшим приоритетом разделяется, или треугольник из очереди слияния с наименьшим приоритетом объединяется со своими соседями.
Используя очередь приоритета минимальной кучи в алгоритме Прима для поиска минимального остовного дерева связного и неориентированного графа , можно добиться хорошего времени выполнения. Эта очередь приоритета минимальной кучи использует структуру данных минимальной кучи, которая поддерживает такие операции, как вставка , минимум , извлечение минимума , уменьшение ключа . [23] В этой реализации вес ребер используется для определения приоритета вершин . Чем меньше вес, тем выше приоритет, а чем больше вес, тем ниже приоритет. [24]
Распараллеливание может использоваться для ускорения приоритетных очередей, но требует некоторых изменений в интерфейсе приоритетной очереди. Причина таких изменений в том, что последовательное обновление обычно имеет только или стоимость, и нет практического выигрыша от распараллеливания такой операции. Одним из возможных изменений является разрешение одновременного доступа нескольких процессоров к одной и той же приоритетной очереди. Вторым возможным изменением является разрешение пакетных операций, которые работают с элементами, а не только с одним элементом. Например, extractMin удалит первые элементы с наивысшим приоритетом.
Если приоритетная очередь допускает параллельный доступ, несколько процессов могут выполнять операции одновременно в этой приоритетной очереди. Однако это поднимает две проблемы. Во-первых, определение семантики отдельных операций больше не очевидно. Например, если два процесса хотят извлечь элемент с наивысшим приоритетом, должны ли они получить один и тот же элемент или разные? Это ограничивает параллелизм на уровне программы, использующей приоритетную очередь. Кроме того, поскольку несколько процессов имеют доступ к одному и тому же элементу, это приводит к конкуренции.
Параллельный доступ к приоритетной очереди может быть реализован на модели PRAM Concurrent Read, Concurrent Write (CRCW). В дальнейшем приоритетная очередь реализована как список пропусков . [25] [26] Кроме того, примитив атомарной синхронизации CAS используется для того, чтобы сделать список пропусков неблокируемым . Узлы списка пропусков состоят из уникального ключа, приоритета, массива указателей для каждого уровня на следующие узлы и метки удаления . Метка удаления отмечает , что узел собирается быть удаленным процессом. Это гарантирует, что другие процессы смогут соответствующим образом отреагировать на удаление.
Если разрешен одновременный доступ к приоритетной очереди, между двумя процессами могут возникнуть конфликты. Например, конфликт возникает, если один процесс пытается вставить новый узел, но в то же время другой процесс собирается удалить предшественника этого узла. [25] Существует риск того, что новый узел будет добавлен в список пропуска, но он больше не будет доступен. ( См. изображение )
В этой настройке операции с приоритетной очередью обобщаются на пакет элементов. Например, k_extract-min удаляет наименьшие элементы приоритетной очереди и возвращает их.
В условиях разделяемой памяти параллельная приоритетная очередь может быть легко реализована с использованием параллельных двоичных деревьев поиска и алгоритмов дерева на основе объединения . В частности, k_extract-min соответствует разделению на двоичном дереве поиска, которое имеет стоимость и дает дерево, содержащее наименьшие элементы. k_insert может быть применено путем объединения исходной приоритетной очереди и пакета вставок. Если пакет уже отсортирован по ключу, k_insert имеет стоимость. В противном случае нам нужно сначала отсортировать пакет, поэтому стоимость будет . Другие операции для приоритетной очереди могут быть применены аналогичным образом. Например, k_decrease-key может быть выполнено путем применения сначала разности , а затем объединения , которое сначала удаляет элементы, а затем вставляет их обратно с обновленными ключами. Все эти операции высокопараллельны, и теоретическую и практическую эффективность можно найти в соответствующих исследовательских работах. [27] [28]
Остальная часть этого раздела посвящена алгоритму на основе очередей в распределенной памяти. Мы предполагаем, что каждый процессор имеет свою собственную локальную память и локальную (последовательную) приоритетную очередь. Элементы глобальной (параллельной) приоритетной очереди распределены по всем процессорам.
Операция k_insert назначает элементы равномерно случайным образом процессорам, которые вставляют элементы в свои локальные очереди. Обратите внимание, что отдельные элементы все еще могут быть вставлены в очередь. При использовании этой стратегии глобальные наименьшие элементы находятся в объединении локальных наименьших элементов каждого процессора с высокой вероятностью. Таким образом, каждый процессор удерживает репрезентативную часть глобальной приоритетной очереди.
Это свойство используется при выполнении k_extract-min , поскольку наименьшие элементы каждой локальной очереди удаляются и собираются в набор результатов. Элементы в наборе результатов по-прежнему связаны с их исходным процессором. Количество элементов, удаляемых из каждой локальной очереди, зависит от и количества процессоров . [29] С помощью параллельного выбора определяются наименьшие элементы набора результатов. С высокой вероятностью это глобальные наименьшие элементы. Если нет, элементы снова удаляются из каждой локальной очереди и помещаются в набор результатов. Это делается до тех пор, пока в наборе результатов не окажутся глобальные наименьшие элементы. Теперь эти элементы можно вернуть. Все остальные элементы набора результатов вставляются обратно в свои локальные очереди. Ожидается время выполнения k_extract-min , где и — размер приоритетной очереди. [29]
Приоритетную очередь можно дополнительно улучшить, не перемещая оставшиеся элементы набора результатов напрямую обратно в локальные очереди после операции k_extract-min . Это экономит перемещение элементов туда и обратно все время между набором результатов и локальными очередями.
Удаляя несколько элементов одновременно, можно достичь значительного ускорения. Но не все алгоритмы могут использовать этот тип очереди с приоритетами. Например, алгоритм Дейкстры не может работать на нескольких узлах одновременно. Алгоритм берет узел с наименьшим расстоянием от очереди с приоритетами и вычисляет новые расстояния для всех его соседних узлов. Если бы вы удалили узлы, работа с одним узлом могла бы изменить расстояние до другого узла . Таким образом, использование операций с k-элементами разрушает свойство установки меток алгоритма Дейкстры.
{{cite web}}
: CS1 maint: архивная копия как заголовок ( ссылка )