PriorityQueue
a binary heap in an array. sift up, sift down.
- chapter
- chapter 06.08
- reading time
- 6 min read
a PriorityQueue hands you the smallest element first (or the "best", by your Comparator). inside it's a binary heap stored in a plain array: a tree where every parent beats its children, laid out level by level.
offer adds at the end and sifts up, poll takes the root and sifts down. both O(log n). the array is not sorted, and neither is iterating it.
the picture
you know how a hospital emergency room doesn't serve patients in arrival order? whoever's worst goes next. but nobody keeps the whole waiting room in perfect order either: they just make sure they always know who's worst right now. that's a priority queue, and the trick that makes it cheap is the heap.
a heap is a tree with one rule: every parent beats its children (for a min-heap: is smaller or equal). nothing is promised between siblings or cousins. so the root is always the overall winner, and fixing the tree after a change only takes one path from top to bottom.
and the tree doesn't need any pointers. store it level by level in an array and the arithmetic does the linking:
- the children of index
iare at2i + 1and2i + 2 - the parent of
iis at(i - 1) >>> 1
offer a small number and watch it climb past bigger parents. poll and watch the last element drop into the root and sink. it stops as soon as the rule holds. flip to a max-heap to see the comparator flip every decision.sift up, sift down
offer(x): put x in the first free slot (the end of the array, the bottom-left-most free spot of the tree). while it beats its parent, swap them. at most one swap per level, so O(log n).
poll(): the answer is queue[0]. to fill the hole, take the last element (removing it is free), put it at the root, and while it loses to its better child, swap them. again one path, O(log n).
peek(): queue[0]. O(1).
the real code in jdk 25 (siftUpComparable, siftDownComparable) doesn't actually swap. it carries the moving element in a local variable, slides parents (or children) into the hole, and writes the element once at the end. same comparisons, fewer writes.
void main() {
var pq = new PriorityQueue<Integer>();
for (int x : new int[] {5, 1, 4, 2, 3}) pq.offer(x);
IO.println(pq + " peek=" + pq.peek());
var out = new StringBuilder();
while (!pq.isEmpty()) out.append(pq.poll()).append(' ');
IO.println(out.toString().trim());
}[1, 2, 4, 5, 3] peek=1 1 2 3 4 5
comparators: max-heaps and real objects
a PriorityQueue is a min-heap by natural order. anything else is a Comparator:
void main() {
int k = 3;
var top = new PriorityQueue<Integer>();
for (int x : new int[] {5, 1, 9, 3, 7, 2, 8}) {
top.offer(x);
if (top.size() > k) top.poll();
}
var max = new PriorityQueue<Integer>(Comparator.reverseOrder());
max.addAll(List.of(5, 1, 9, 3));
IO.println(top.peek() + " " + new TreeSet<>(top) + " " + max.poll());
}7 [7, 8, 9] 9
the first half is the classic top-k trick: to keep the k largest, use a min-heap of size k and evict its smallest whenever it grows past k. what's left is the k largest, and peek() is the k-th largest. O(n log k) time, O(k) memory, works on a stream you can't hold in memory.
record Task(String name, int priority) {}
void main() {
var pq = new PriorityQueue<Task>(Comparator.comparingInt(Task::priority));
for (String n : List.of("a", "b", "c", "d", "e", "f")) pq.offer(new Task(n, 1));
var out = new StringBuilder();
while (!pq.isEmpty()) out.append(pq.poll().name());
IO.println(out);
}what it costs
| operation | cost |
|---|---|
offer, poll, remove() | O(log n) |
peek, element, size | O(1) |
contains(o), remove(o) | O(n), it's a linear scan of the array |
new PriorityQueue<>(collection) | O(n), it builds the heap bottom-up ("heapify") instead of n offers |
| iteration | O(n), in heap order |
the array starts at 11 slots and grows like ArrayDeque's: roughly doubling while small (under 64), then by 50%. null elements are rejected (poll uses null to mean empty). it isn't thread-safe; for producer/consumer work there's PriorityBlockingQueue (see concurrent collections).
why not just keep a sorted list?+
a sorted ArrayList gives you the minimum in O(1) too, but every insert has to find its spot and shift everything after it: O(n). a balanced tree (TreeMap, TreeSet) does both in O(log n), but each element is a 40-byte node with three pointers, and a TreeSet drops "duplicates" that compare equal. the heap only keeps partial order, exactly enough to know the winner, which is why it fits in a flat array and beats both for "keep giving me the next best". it's also the engine behind heapsort, Dijkstra's shortest paths, and every job scheduler you've used.
- a
PriorityQueueis a binary heap in an array. children of i: 2i+1, 2i+2. offersifts up,pollsifts down. O(log n).peekis O(1).- iteration and
toStringare heap order, not sorted. onlypollsorts. - max-heap or custom order: pass a
Comparator. - not stable: ties come out in no particular order. add a tie-breaker.
- top-k largest: a min-heap of size k.