Класс PriorityQueue в Java
Добавьте в PriorityQueue<Integer> числа 5, 1, 4, 2, 3 и выведите очередь через System.out.println(pq) — получится [1, 2, 4, 5, 3], а не [1, 2, 3, 4, 5]. Очередь не сломана: она гарантирует только то, что первым выйдет наименьший элемент, а всё остальное хранит в виде кучи.
PriorityQueue (java.util.PriorityQueue, priority queue — «очередь с приоритетом») — класс из Java 5, реализующий интерфейс Queue на бинарной куче. Элементы извлекаются не в порядке добавления, а по приоритету: по умолчанию первым выходит наименьший элемент в естественном порядке (Comparable), а с Comparator можно задать любой другой. Дубликаты разрешены, null запрещён, класс не потокобезопасен.
Как устроена PriorityQueue: бинарная куча (min-heap)
Внутри PriorityQueue лежит массив Object[], в котором элементы образуют бинарную кучу (binary heap): каждый элемент не больше своих потомков. Для элемента с индексом i потомки находятся в ячейках 2*i + 1 и 2*i + 2, а родитель — в (i - 1) / 2. Поэтому наименьший элемент всегда лежит в ячейке 0 — это голова очереди.
Так выглядит куча после offer(5), offer(1), offer(4), offer(2), offer(3):
массив: [1, 2, 4, 5, 3]
индекс: 0 1 2 3 4
1
/ \
2 4
/ \
5 3 Каждый родитель меньше потомков, но между «соседями» порядка нет: 4 стоит раньше 3. Такая куча называется min-heap. Если передать компаратор с обратным порядком, вверху окажется наибольший элемент — это max-heap (пример в разделе PriorityQueue с Comparator).
Новый элемент записывается в конец массива и «всплывает» (sift up), меняясь с родителем, пока тот больше. При poll() голова удаляется, на её место ставится последний элемент и «погружается» (sift down) вниз. Обе операции проходят не больше одного пути от корня до листа, то есть O(log n):
import java.util.PriorityQueue;
public class HeapDemo {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(4);
pq.offer(2);
pq.offer(3);
System.out.println(pq); // [1, 2, 4, 5, 3]
System.out.println(pq.poll()); // 1
System.out.println(pq); // [2, 3, 4, 5]
}
} Вывод:
[1, 2, 4, 5, 3]
1
[2, 3, 4, 5] После poll() последний элемент 3 встал в корень, опустился на место двойки, и куча снова корректна. toString() печатает массив кучи как есть, поэтому по выводу очереди нельзя судить о порядке извлечения.
Конструкторы PriorityQueue
У PriorityQueue семь конструкторов: они отличаются начальной ёмкостью, компаратором и источником начальных элементов. Если компаратор не передан, используется естественный порядок Comparable. Начальная ёмкость по умолчанию — 11 элементов, и это не ограничение: массив растёт автоматически.
| Конструктор | Порядок элементов | Граничный случай |
|---|---|---|
PriorityQueue() | Естественный (Comparable), ёмкость 11 | — |
PriorityQueue(int initialCapacity) | Естественный | initialCapacity меньше 1 — IllegalArgumentException |
PriorityQueue(Comparator<? super E> comparator) | По компаратору (конструктор появился в Java 8) | null вместо компаратора — естественный порядок |
PriorityQueue(int initialCapacity, Comparator<? super E> comparator) | По компаратору | initialCapacity меньше 1 — IllegalArgumentException |
PriorityQueue(Collection<? extends E> c) | Если c — SortedSet или PriorityQueue, берётся их компаратор, иначе естественный порядок | Элемент null в c — NullPointerException; куча строится за O(n) |
PriorityQueue(PriorityQueue<? extends E> c) | Как у c | — |
PriorityQueue(SortedSet<? extends E> c) | Как у c | — |
Когда массив заполняется, он пересоздаётся: пока ёмкость меньше 64, примерно вдвое, дальше — на 50%. Передавать ёмкость в конструктор имеет смысл, когда число элементов известно заранее, — это экономит копирования. Жёстко ограничить размер PriorityQueue конструктором нельзя (способ — в разделе k наибольших элементов).
Пример PriorityQueue: естественный и обратный порядок
В естественном порядке PriorityQueue<String> отдаёт строки по алфавиту (точнее, по кодам символов Unicode), а с компаратором Collections.reverseOrder() — в порядке, обратном естественному. Одинаковые элементы при этом не отбрасываются, в отличие от множеств Set.
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueExample {
public static void main(String[] args) {
Queue<String> queue1 = new PriorityQueue<>();
queue1.offer("Киев");
queue1.offer("Харьков");
queue1.offer("Львов");
queue1.offer("Кременчуг");
queue1.offer("Кременчуг");
System.out.println("Голова: " + queue1.peek());
System.out.print("Priority queue с Comparable: ");
while (queue1.size() > 0) {
System.out.print(queue1.remove() + " ");
}
System.out.println();
PriorityQueue<String> queue2
= new PriorityQueue<>(5, Collections.reverseOrder());
queue2.offer("Киев");
queue2.offer("Харьков");
queue2.offer("Львов");
queue2.offer("Кременчуг");
queue2.offer("Кременчуг");
System.out.print("Priority queue с Comparator: ");
while (queue2.size() > 0) {
System.out.print(queue2.remove() + " ");
}
}
} Вывод:
Голова: Киев
Priority queue с Comparable: Киев Кременчуг Кременчуг Львов Харьков
Priority queue с Comparator: Харьков Львов Кременчуг Кременчуг Киев Переменная queue1 объявлена типом интерфейса Queue, элементы — строки. Метод offer() добавляет пять элементов, peek() показывает голову без удаления. В цикле while, пока коллекция не пуста, метод remove() извлекает элемент из головы очереди, одновременно удаляя его.
Для queue2 в конструктор передаются начальная ёмкость 5 и компаратор Collections.reverseOrder(), который сортирует элементы в порядке, обратном естественному. Элементы добавлялись в одном порядке, а извлекаются отсортированными, причём «Кременчуг» встречается дважды — как и был добавлен. Ёмкость 5 тоже не мешает: очередь приняла бы и шестой элемент.
Методы PriorityQueue и сложность операций
Добавление и извлечение головы в PriorityQueue выполняются за O(log n), просмотр головы и размер — за O(1), а поиск и удаление произвольного элемента (contains(), remove(Object)) — за O(n), потому что куча не упорядочена для поиска. Так сложность описана и в документации PriorityQueue.
| Метод | Что делает | Сложность | Пустая очередь, null и другие граничные случаи |
|---|---|---|---|
offer(e), add(e) | Добавляет элемент и восстанавливает кучу | O(log n) | null — NullPointerException; элемент без Comparable и без компаратора — ClassCastException; всегда возвращают true |
poll() | Удаляет и возвращает голову (наименьший элемент) | O(log n) | Пусто — null |
remove() | То же, что poll() | O(log n) | Пусто — NoSuchElementException |
peek() | Возвращает голову, не удаляя её | O(1) | Пусто — null |
element() | То же, что peek() | O(1) | Пусто — NoSuchElementException |
remove(Object o) | Удаляет один элемент, равный o по equals() | O(n) | Элемента нет — false; сравнение идёт через equals(), а не через компаратор |
contains(Object o) | Проверяет наличие элемента | O(n) | contains(null) — false, без исключения |
size(), isEmpty() | Число элементов / проверка на пустоту | O(1) | — |
comparator() | Возвращает компаратор очереди | O(1) | Естественный порядок — null |
iterator(), toArray(), toString() | Обход и копия элементов | O(n) | Порядок массива кучи, а не порядок приоритета |
addAll(c) | Добавляет элементы c по одному | O(m log n) для m элементов | Быстрее создать очередь конструктором PriorityQueue(c) — O(n) |
clear() | Удаляет все элементы | O(n) | Ёмкость массива не уменьшается |
Чем отличаются пары offer/add, poll/remove и peek/element в любых очередях, разобрано в разделе Интерфейс Queue в Java. У PriorityQueue нет методов getFirst() и getLast() из Java 21: класс не реализует Deque и SequencedCollection, и вызов pq.getFirst() не скомпилируется.
PriorityQueue с Comparator: свои объекты и max-heap
Чтобы положить в PriorityQueue собственные объекты, класс должен реализовать Comparable или в конструктор нужно передать Comparator. Компаратор же превращает очередь в max-heap: с Collections.reverseOrder() (или Comparator.reverseOrder()) первым выходит наибольший элемент.
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
public class TaskQueue {
record Task(String name, int priority) {}
public static void main(String[] args) {
PriorityQueue<Task> tasks =
new PriorityQueue<>(Comparator.comparingInt(Task::priority));
tasks.offer(new Task("Отчёт", 3));
tasks.offer(new Task("Исправить баг в проде", 1));
tasks.offer(new Task("Код-ревью", 2));
while (!tasks.isEmpty()) {
Task t = tasks.poll();
System.out.println(t.priority() + " " + t.name());
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.addAll(List.of(5, 1, 4, 2, 3));
System.out.println("Максимум: " + maxHeap.peek());
}
} Вывод:
1 Исправить баг в проде
2 Код-ревью
3 Отчёт
Максимум: 5 Чтобы задачи с наибольшим числом выходили первыми, достаточно Comparator.comparingInt(Task::priority).reversed(). Как собирать компараторы из comparing(), thenComparing() и reversed(), показано в разделе Comparator.comparing, thenComparing, reversed. Популярную запись max-heap через лямбду (a, b) -> b - a лучше не использовать: на больших по модулю числах вычитание переполняется (почему нельзя писать return a - b), безопасный вариант — (a, b) -> Integer.compare(b, a).
Что будет без Comparable и без компаратора
Компилятор не проверяет, сравнимы ли элементы: new PriorityQueue<Point>() компилируется для любого класса. Ошибка появится только при выполнении:
record Point(int x, int y) {}
PriorityQueue<Point> points = new PriorityQueue<>();
points.offer(new Point(1, 2));
// JDK 11+: java.lang.ClassCastException: class Point cannot be cast to class java.lang.Comparable
// Java 8: первый offer() проходит, исключение бросит второй В Java 8 первый элемент записывался в массив без сравнения, поэтому ошибка проявлялась на втором offer(), а в современных JDK — сразу. Исправление — реализовать Comparable<Point> или передать компаратор, например Comparator.comparingInt(Point::x).
Порядок обхода: почему итератор не сортирует
Итератор, цикл for-each, toString() и toArray() у PriorityQueue обходят внутренний массив кучи, поэтому порядок элементов в них не совпадает с порядком приоритета. Отсортированно элементы выходят только при последовательных вызовах poll(). Что выведет этот код?
import java.util.List;
import java.util.PriorityQueue;
public class IterationOrder {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : List.of(5, 1, 4, 2, 3)) {
pq.offer(x);
}
for (int x : pq) {
System.out.print(x + " ");
}
System.out.println();
System.out.println(pq.stream().sorted().toList());
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " ");
}
System.out.println();
System.out.println("Размер: " + pq.size());
}
} Вывод:
1 2 4 5 3
[1, 2, 3, 4, 5]
1 2 3 4 5
Размер: 0 For-each показал массив кучи. stream().sorted() вернул отсортированную копию, не трогая очередь (toList() — с Java 16; на Java 8 используйте collect(Collectors.toList())). Цикл с poll() выдал правильный порядок, но опустошил очередь. Если очередь с компаратором, сортируйте копию тем же компаратором: pq.stream().sorted(pq.comparator()) — при естественном порядке comparator() вернёт null, и sorted(null) бросит NullPointerException, поэтому там нужен просто sorted().
Практический пример: k наибольших элементов
Чтобы найти k наибольших элементов в большом наборе, не сортируя его целиком, используют min-heap размера k: каждый новый элемент добавляют в очередь, а когда размер превысил k, удаляют голову — текущий минимум. В очереди остаются k наибольших, время работы O(n log k), память O(k).
import java.util.List;
import java.util.PriorityQueue;
public class TopK {
static List<Integer> topK(List<Integer> numbers, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int n : numbers) {
heap.offer(n);
if (heap.size() > k) {
heap.poll(); // выбрасываем наименьший
}
}
return heap.stream().sorted().toList();
}
public static void main(String[] args) {
List<Integer> data = List.of(5, 1, 9, 3, 7, 2, 8);
System.out.println(topK(data, 3));
System.out.println(topK(data, 10));
System.out.println(topK(List.of(), 3));
}
} Вывод:
[7, 8, 9]
[1, 2, 3, 5, 7, 8, 9]
[] Если k больше размера данных, возвращаются все элементы; для пустого списка — пустой результат без исключений. Тот же приём даёт «очередь ограниченного размера» для потока событий: в памяти хранятся только k лучших значений, сколько бы данных ни пришло. Для k наименьших берут max-heap, то есть очередь с Collections.reverseOrder().
Нюансы: null, изменение приоритета, потоки
Три свойства PriorityQueue чаще всего приводят к ошибкам: очередь не принимает null, не замечает изменения полей уже добавленного объекта и не защищена от одновременного доступа из нескольких потоков. Все три не видны компилятору.
null. offer(null) и add(null) бросают NullPointerException: элемент null нельзя сравнить, а poll() и peek() возвращают null как признак пустой очереди. Почему null в очередях опасен, разобрано в разделе Queue и null.
Изменение приоритета. Куча перестраивается только при offer() и poll(). Если изменить поле, по которому сравниваются объекты, уже лежащие в очереди, порядок извлечения станет неверным:
import java.util.Comparator;
import java.util.PriorityQueue;
public class MutablePriority {
static class Job {
final String name;
int priority;
Job(String name, int priority) { this.name = name; this.priority = priority; }
}
public static void main(String[] args) {
PriorityQueue<Job> jobs = new PriorityQueue<>(Comparator.comparingInt((Job j) -> j.priority));
Job a = new Job("A", 1);
jobs.offer(a);
jobs.offer(new Job("B", 2));
jobs.offer(new Job("C", 3));
a.priority = 10; // куча об этом не знает
System.out.println(jobs.peek().name); // A — хотя у A теперь самый низкий приоритет
jobs.remove(a); // правильно: убрать,
a.priority = 10; // изменить
jobs.offer(a); // и добавить заново
System.out.println(jobs.peek().name); // B
}
} Вывод:
A
B Приоритет меняют только снаружи очереди
Метода «обновить приоритет» у PriorityQueue нет. Схема «remove(obj) → изменить поле → offer(obj)» работает, но remove(Object) стоит O(n). В алгоритмах вроде Дейкстры поэтому обычно не удаляют старую запись, а добавляют новую с обновлённым приоритетом и пропускают устаревшие при извлечении. Надёжнее всего делать поля, участвующие в сравнении, неизменяемыми — например, через record.
Потоки. PriorityQueue не синхронизирована: одновременные offer() и poll() из разных потоков могут повредить кучу. Потокобезопасная очередь с приоритетом — java.util.concurrent.PriorityBlockingQueue: она неограниченна, использует тот же порядок, а её метод take() ждёт, пока в очереди появится элемент. Другие потокобезопасные коллекции перечислены в разделе Потокобезопасные коллекции Java. И ещё одна мелочь: PriorityQueue не переопределяет equals(), поэтому две очереди с одинаковыми элементами не равны (подробнее).
PriorityQueue vs TreeSet
PriorityQueue и TreeSet обе используют Comparable или Comparator, но решают разные задачи. PriorityQueue — куча: быстрый доступ к одному крайнему элементу и дубликаты. TreeSet — красно-чёрное дерево: полностью отсортированный обход, оба конца, поиск за O(log n), но без дубликатов.
| Признак | PriorityQueue | TreeSet |
|---|---|---|
| Внутреннее устройство | Бинарная куча на массиве | Красно-чёрное дерево (внутри TreeMap) |
| Дубликаты | Да | Нет: элемент, для которого компаратор вернул 0, не добавится |
| Доступ к элементам | Только голова: peek() за O(1) | Оба конца (first(), last()) и ближайшие (ceiling(), floor()) за O(log n) |
| Порядок обхода итератором | Не отсортирован | Отсортирован |
contains(), remove(Object) | O(n) | O(log n) |
| Память | Один массив | Отдельный узел на каждый элемент |
null | Нет — NullPointerException | Нет при естественном порядке |
| Когда выбирать | Обработка по приоритету, планировщики, top-k, алгоритмы на графах | Уникальные отсортированные значения, диапазоны, поиск соседей |
Если нужна очередь с приоритетом, которая должна отдавать и минимум, и максимум, PriorityQueue не подходит: берут TreeSet (если дубликатов нет) или TreeMap со счётчиком повторов. Для обычной очереди FIFO без приоритетов используют ArrayDeque.
Часто задаваемые вопросы
В каком порядке PriorityQueue отдаёт элементы с одинаковым приоритетом?
Порядок не гарантирован: куча нестабильна, и два элемента с равным приоритетом могут выйти не в том порядке, в каком были добавлены. Если нужен FIFO среди равных, добавьте в объект порядковый номер и второй критерий сравнения: Comparator.comparingInt(Task::priority).thenComparingLong(Task::seq).
Как создать PriorityQueue для массивов int[] или пар значений, например в алгоритме Дейкстры?
Массивы не реализуют Comparable, поэтому компаратор обязателен: new PriorityQueue<int[]>((a, b) -> Integer.compare(a[1], b[1])) — здесь a[0] вершина, a[1] расстояние. Читабельнее завести record Edge(int node, int dist) и передать Comparator.comparingInt(Edge::dist).
Почему в min-heap минимум доступен за O(1), а максимум ищется за O(n)?
Куча гарантирует только то, что родитель не больше потомков, поэтому минимум всегда в корне. Максимум может оказаться в любом листе, а листьев примерно n/2 — их приходится перебрать все. Если нужны оба конца, держат две кучи или используют TreeSet — сравнение в разделе PriorityQueue vs TreeSet.
Можно ли ограничить размер PriorityQueue?
Встроенного ограничения нет: число в конструкторе — только начальная ёмкость, offer() всегда возвращает true. Ограничение пишут вручную: после offer() проверяют size() и вызывают poll(), если очередь стала больше лимита. Полный пример с выводом — в разделе k наибольших элементов.
Video Explanation
Prefer video format? Watch this lesson with examples and explanations.
Comments