Класс PriorityQueue в Java - Вопросы
Всего: 6 вопросов
1. Как устроена PriorityQueue в Java внутри?
Как устроена PriorityQueue в Java внутри?
PriorityQueue хранит элементы в массиве Object[], организованном как бинарная куча: каждый элемент не больше своих потомков. Для элемента с индексом i потомки лежат в ячейках 2*i + 1 и 2*i + 2, родитель — в (i - 1) / 2, поэтому наименьший элемент всегда в ячейке 0 — это голова очереди. Такая куча называется min-heap; с компаратором обратного порядка вверху оказывается наибольший элемент — max-heap.
При offer() новый элемент записывается в конец массива и всплывает (sift up), меняясь с родителем, пока тот больше. При poll() на место головы ставится последний элемент и погружается (sift down). Обе операции проходят не больше одного пути от корня до листа — O(log n). Между «соседями» порядка нет: после offer(5, 1, 4, 2, 3) массив выглядит как [1, 2, 4, 5, 3].
2. Какая сложность у основных операций PriorityQueue?
Какая сложность у основных операций PriorityQueue?
offer()/add(),poll()/remove()— O(log n): элемент всплывает или погружается по одному пути кучи.peek()/element(),size(),isEmpty()— O(1): голова всегда в ячейке 0.contains(Object)иremove(Object)— O(n): куча не упорядочена для поиска, массив перебирается целиком.remove(Object)сравнивает черезequals(), а не через компаратор.addAll(c)добавляет по одному элементу — O(m log n); конструкторPriorityQueue(c)строит кучу сразу за O(n).
По той же причине в min-heap минимум доступен за O(1), а максимум ищется за O(n): он может лежать в любом листе, а листьев около n/2. Если нужны оба конца, держат две кучи или берут TreeSet.
3. В каком порядке PriorityQueue перебирается в цикле for-each и как получить элементы отсортированными?
В каком порядке PriorityQueue перебирается в цикле for-each и как получить элементы отсортированными?
Итератор, for-each, toString() и toArray() обходят внутренний массив кучи, поэтому порядок не совпадает с порядком приоритета: после offer(5, 1, 4, 2, 3) for-each выводит 1 2 4 5 3. Гарантирован только первый элемент — голова.
Отсортированный порядок дают два способа:
- последовательные вызовы
poll()— порядок правильный, но очередь опустошается; - сортировка копии:
pq.stream().sorted().toList()(очередь не меняется). Для очереди с компаратором —pq.stream().sorted(pq.comparator()); при естественном порядкеcomparator()возвращаетnull, иsorted(null)броситNullPointerException, поэтому там нужен простоsorted().
4. Как положить в PriorityQueue собственные объекты и сделать max-heap?
Как положить в PriorityQueue собственные объекты и сделать max-heap?
Класс элементов должен реализовать Comparable, или в конструктор передают Comparator:
PriorityQueue<Task> tasks =
new PriorityQueue<>(Comparator.comparingInt(Task::priority));
Иначе код скомпилируется, но offer() бросит ClassCastException (в JDK 11+ — уже на первом элементе). Массивы int[] тоже не Comparable: new PriorityQueue<int[]>((a, b) -> Integer.compare(a[1], b[1])).
Max-heap — компаратор обратного порядка: new PriorityQueue<>(Collections.reverseOrder()) или Comparator.comparingInt(Task::priority).reversed(). Запись (a, b) -> b - a лучше не использовать: вычитание переполняется на больших числах, безопасно — (a, b) -> Integer.compare(b, a).
Порядок элементов с равным приоритетом не гарантирован: куча нестабильна. Если нужен FIFO среди равных, добавляют порядковый номер вторым критерием: .thenComparingLong(Task::seq).
5. Что будет, если изменить приоритет объекта, который уже лежит в PriorityQueue? Можно ли ограничить размер очереди и использовать её из нескольких потоков?
Что будет, если изменить приоритет объекта, который уже лежит в PriorityQueue? Можно ли ограничить размер очереди и использовать её из нескольких потоков?
Изменение приоритета. Куча перестраивается только при offer() и poll(), поэтому изменение поля, по которому сравниваются объекты, она не замечает — порядок извлечения становится неверным. Метода «обновить приоритет» нет: объект удаляют (remove(obj), O(n)), меняют поле и добавляют заново. Надёжнее делать поля сравнения неизменяемыми, например через record.
Размер. Число в конструкторе — только начальная ёмкость (по умолчанию 11, меньше 1 — IllegalArgumentException), массив растёт сам, offer() всегда возвращает true. Ограничение пишут вручную: после offer() проверяют size() и вызывают poll() — так ищут k наибольших элементов за O(n log k).
Потоки. PriorityQueue не синхронизирована, одновременные offer() и poll() могут повредить кучу. Потокобезопасный аналог — java.util.concurrent.PriorityBlockingQueue с блокирующим take().
6. Чем PriorityQueue отличается от TreeSet и когда что выбирать?
Чем PriorityQueue отличается от TreeSet и когда что выбирать?
Обе коллекции упорядочивают элементы через Comparable или Comparator, но устроены по-разному:
- Устройство:
PriorityQueue— бинарная куча на одном массиве;TreeSet— красно-чёрное дерево (внутриTreeMap) с узлом на каждый элемент. - Дубликаты:
PriorityQueueих хранит;TreeSetне добавит элемент, для которого компаратор вернул 0. - Доступ: у
PriorityQueueтолько голова (peek()за O(1)); уTreeSetоба конца и соседи (first(),last(),ceiling(),floor()) за O(log n). - Обход: итератор
PriorityQueueне сортирует, уTreeSetобход отсортирован. - Поиск и удаление элемента: O(n) против O(log n).
PriorityQueue выбирают для обработки по приоритету, планировщиков, top-k и алгоритмов на графах; TreeSet — для уникальных отсортированных значений, диапазонов и случаев, когда нужны и минимум, и максимум.