Priority Queue

Очередь с приоритетом — структура данных, в которой элементы извлекаются не
по порядку добавления, а по приоритету.

Главное правило:

сначала извлекается элемент с самым важным приоритетом

Обычная очередь работает по FIFO:

первым пришёл -> первым вышел

Очередь с приоритетом работает иначе:

самый высокий приоритет -> первым вышел

Пример:

push("обычная задача", priority = 5)
push("срочная задача", priority = 1)
push("фоновая задача", priority = 10)

Если меньшее число означает более высокий приоритет, порядок извлечения будет:

"срочная задача"
"обычная задача"
"фоновая задача"

Термины

priority queue       -> очередь с приоритетом
priority             -> приоритет элемента
push(x, priority)    -> добавить элемент с приоритетом
pop()                -> удалить и вернуть элемент с лучшим приоритетом
peek()               -> посмотреть элемент с лучшим приоритетом без удаления
isEmpty()            -> проверить, пуста ли очередь
size                 -> количество элементов
min-priority queue   -> первым выходит элемент с минимальным приоритетом
max-priority queue   -> первым выходит элемент с максимальным приоритетом

Очередь с приоритетом задаётся четырьмя вещами:

1. набор элементов
2. приоритет каждого элемента
3. правило сравнения приоритетов
4. элемент, который должен выйти следующим

Структура очереди с приоритетом

Структура отвечает на вопрос:

Какой элемент сейчас самый важный?

Внутренний порядок хранения может отличаться от порядка добавления:

добавлены:
(5, "обычная задача")
(1, "срочная задача")
(10, "фоновая задача")

Логический порядок извлечения:

(1, "срочная задача")
(5, "обычная задача")
(10, "фоновая задача")

Важно: очередь с приоритетом обычно не сортирует все элементы полностью.

Её главная задача:

быстро найти и извлечь следующий элемент с лучшим приоритетом

Свойства

Короткая карта свойств очереди с приоритетом:

приоритеты          -> у каждого элемента есть значение для сравнения
лучший элемент      -> элемент с минимальным или максимальным приоритетом
не FIFO             -> порядок добавления не является главным правилом
размер              -> количество элементов в очереди
пустота             -> очередь может не содержать элементов
сравнение           -> нужно уметь сравнивать приоритеты

Конкретная очередь с приоритетом содержит конечное число элементов:

size = n
пустая очередь = size == 0
непустая очередь = size > 0

Для непустой очереди можно определить следующий элемент:

top(priorityQueue)

Важно отделять свойства структуры от характеристик конкретной очереди:

свойство:       очередь с приоритетом извлекает лучший элемент
характеристика: лучший элемент сейчас = "срочная задача"

свойство:       у элементов есть приоритеты
характеристика: приоритет "срочной задачи" = 1

Min Priority Queue

В min-priority queue первым извлекается элемент с минимальным приоритетом.

priority: 1, 5, 10
pop() -> priority 1

Так работает heapq в Python.

Max Priority Queue

В max-priority queue первым извлекается элемент с максимальным приоритетом.

priority: 1, 5, 10
pop() -> priority 10

Если доступна только min-heap реализация, max-priority queue часто делают через
отрицательные приоритеты:

priority = 10 -> stored priority = -10
priority = 5  -> stored priority = -5

Тогда минимальное сохранённое значение соответствует максимальному исходному
приоритету.

Одинаковые приоритеты

Если у нескольких элементов одинаковый приоритет, нужно явно понимать правило
разрешения ничьей.

Возможные варианты:

по порядку добавления
по дополнительному ключу
по произвольному внутреннему порядку реализации

На практике часто добавляют счётчик:

(priority, insertionOrder, value)

Тогда элементы с одинаковым приоритетом извлекаются в порядке добавления.

Базовые операции

Создание

priorityQueue = []

Обычно:

O(1)

Push

Добавить элемент с приоритетом:

push(priorityQueue, x, priority)

Пример:

push("A", 5)
push("B", 1)
push("C", 10)

Если структура реализована через бинарную кучу:

O(log n)

Pop

Удалить и вернуть элемент с лучшим приоритетом:

pop(priorityQueue)

Пример для min-priority queue:

priorityQueue = [(1, "B"), (5, "A"), (10, "C")]
pop() -> "B"

Условие:

size(priorityQueue) > 0

Если структура реализована через бинарную кучу:

O(log n)

Peek / Top

Посмотреть элемент с лучшим приоритетом без удаления:

peek(priorityQueue)
top(priorityQueue)

Пример:

priorityQueue = [(1, "B"), (5, "A"), (10, "C")]
peek() -> "B"

Условие:

size(priorityQueue) > 0

Если структура реализована через бинарную кучу:

O(1)

Is Empty

Проверить, пуста ли очередь:

isEmpty(priorityQueue)

Пример:

[]          -> true
[(1, "B")]  -> false

Сложность:

O(1)

Size

Получить количество элементов:

size(priorityQueue)

Обычно:

O(1)

если размер хранится отдельно.

Таблица сложностей

Для реализации через бинарную кучу:

Операция Сложность
создание O(1)
push(x, priority) O(log n)
pop() O(log n)
peek() / top() O(1)
isEmpty() O(1)
size() O(1)
построение из массива O(n)
поиск произвольного значения O(n)

Реализация

Очередь с приоритетом — это абстрактная структура данных. Её можно реализовать
разными способами.

Через бинарную кучу

Самая частая реализация — бинарная куча.

Для min-heap корень содержит минимальный элемент:

        1
      /   \
     3     5
    / \
   7   9

В массиве та же куча хранится так:

heap = [1, 3, 5, 7, 9]

Главная идея:

лучший элемент всегда находится в корне

Поэтому:

peek() -> O(1)
push() -> O(log n), потому что нужно восстановить свойство кучи
pop()  -> O(log n), потому что нужно восстановить свойство кучи

Через отсортированный массив

Можно хранить элементы отсортированными по приоритету:

[(1, "B"), (5, "A"), (10, "C")]

Тогда:

peek() -> O(1)
pop()  -> O(1), если удаляем с удобного конца
push() -> O(n), потому что нужно найти место и сдвинуть элементы

Через неотсортированный массив

Можно просто добавлять элементы в конец:

push() -> O(1)

Но при извлечении нужно искать лучший приоритет:

pop() -> O(n)
peek() -> O(n)

Такой вариант прост, но плохо подходит для частых извлечений.

Очередь, дек и очередь с приоритетом

Обычная очередь:

queue -> первым выходит первый добавленный элемент

Дек:

deque -> можно добавлять и удалять с обоих концов

Очередь с приоритетом:

priority queue -> первым выходит элемент с лучшим приоритетом

Пример различия:

добавили: A(priority=5), B(priority=1), C(priority=10)

queue          -> A, B, C
priority queue -> B, A, C

Python

В Python очередь с приоритетом часто реализуют через:

heapq

heapq реализует min-heap: первым извлекается элемент с минимальным значением.

Пример:

import heapq

pq = []

heapq.heappush(pq, (5, "обычная задача"))
heapq.heappush(pq, (1, "срочная задача"))
heapq.heappush(pq, (10, "фоновая задача"))

print(heapq.heappop(pq))  # (1, "срочная задача")
print(heapq.heappop(pq))  # (5, "обычная задача")
print(heapq.heappop(pq))  # (10, "фоновая задача")

Для max-priority queue можно хранить отрицательный приоритет:

import heapq

pq = []

heapq.heappush(pq, (-10, "важная задача"))
heapq.heappush(pq, (-3, "обычная задача"))

priority, value = heapq.heappop(pq)

print(-priority, value)  # 10 важная задача

Если приоритеты могут совпадать, удобно добавлять счётчик:

import heapq

pq = []
counter = 0

def push(priority, value):
    global counter
    heapq.heappush(pq, (priority, counter, value))
    counter += 1

push(1, "A")
push(1, "B")

print(heapq.heappop(pq))  # (1, 0, "A")
print(heapq.heappop(pq))  # (1, 1, "B")

Для потокобезопасной очереди с приоритетом есть:

queue.PriorityQueue

Она полезна в многопоточных задачах, но в алгоритмических задачах чаще
используют heapq.

Типичные применения

Dijkstra
A*
Prim
top K elements
merge K sorted lists
медиана потока чисел
планировщик задач
симуляции событий
выбор следующей самой выгодной операции

Формальная схема

PriorityQueue = {
  elements: e1, e2, ..., en,
  priority: priority(ei),
  comparator: правило сравнения приоритетов,
  top: элемент с лучшим приоритетом,
  size: n
}

Операции очереди с приоритетом изменяют или используют одну из четырёх
сущностей:

приоритет
лучший элемент
размер
правило сравнения

Примеры:

push(x, priority) -> добавляет элемент и восстанавливает порядок приоритетов
pop()             -> возвращает лучший элемент и восстанавливает структуру
peek()            -> читает лучший элемент без удаления
isEmpty()         -> проверяет размер

Минимальная формулировка:

очередь с приоритетом = структура для быстрого извлечения самого важного элемента
Прокрутить вверх