Priority queues intro
Priority queues: serving the most important value first, defined by a comparator, backed by a binary heap in Java's PriorityQueue, as a min-queue or a max-queue, with duplicates and custom objects.
Learning goals
- Explain how a priority queue differs from a FIFO queue
- Build min-queues and max-queues with comparators safely
- Predict what PriorityQueue does with equal priorities
- Define priority for custom objects
Prerequisites
An emergency room is a priority queue: patients are served by urgency, not arrival time, and a new arrival can overtake everybody waiting.
Comparator-defined priority
A priority queue removes the value with the **highest priority** next, whatever its arrival time. What “highest” means is not a property of the structure but of an ordering: in Java a Comparator (or the natural order of Comparable values) decides which of two elements comes out first. The element that sorts first leaves first.
Because the comparator is the definition of priority, it must be a consistent total order: if a comes before b and b before c, then a comes before c, and the answer never changes while the element is in the queue.
1static void order() {2PriorityQueue<String> byLength = new PriorityQueue<>(Comparator.comparingInt(String::length));3byLength.offer("pear"); byLength.offer("fig"); byLength.offer("banana");4System.out.println(byLength.poll()); // fig: the shortest word has the highest priority5}
Heap-backed PriorityQueue
PriorityQueue stores a binary heap in a plain array. offer appends at the end and **sifts up** while the new value beats its parent; poll takes the root, moves the last element to the root and **sifts down** while a child beats it. Both are O(log n); peek is O(1); remove(Object) is O(n) because it must first find the element.
A heap only orders each parent against its children. The array is therefore *not* sorted: after offering 5, 3, 8, 1 into a min-queue, toString() prints [1, 3, 8, 5], and iterating a PriorityQueue does not give sorted order. Use repeated poll() to read values in priority order. The animation reproduces this array exactly; it is the same layout as the real class.
Best
O(1)
Average
O(log n)
Worst
O(log n)
Space
O(n)
PriorityQueue: offer, poll and peek on a binary heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
polled so far (in order)
An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);3pq.offer(x); // append at the end, then sift up4Integer top = pq.poll(); // last element to the root, then sift down5Integer next = pq.peek(); // the root only; nothing is removed
Live state
size
0
order
min first
Execution trace (dry run table)Show 26 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 order=min first | An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2. |
| 2 | L3 | op=offer(5) size=1 | offer(5): append at the end (index 0), the only place that keeps the tree complete. |
| 3 | L3 | op=offer(3) size=2 | offer(3): append at the end (index 1), the only place that keeps the tree complete. |
| 4 | L3 | op=offer(3) child=3 parent=5 | The child has higher priority than its parent, so they swap. |
| 5 | L3 | op=offer(3) | Swapped: 3 moves up to index 0. |
| 6 | L3 | op=offer(8) size=3 | offer(8): append at the end (index 2), the only place that keeps the tree complete. |
| 7 | L3 | op=offer(8) child=8 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 8 | L3 | op=offer(1) size=4 | offer(1): append at the end (index 3), the only place that keeps the tree complete. |
| 9 | L3 | op=offer(1) child=1 parent=5 | The child has higher priority than its parent, so they swap. |
| 10 | L3 | op=offer(1) | Swapped: 1 moves up to index 1. |
| 11 | L3 | op=offer(1) child=1 parent=3 | The child has higher priority than its parent, so they swap. |
| 12 | L3 | op=offer(1) | Swapped: 1 moves up to index 0. |
| 13 | L4 | op=poll() returned=1 size=3 | poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down. |
| 14 | L4 | op=poll() node=5 better child=3 | The better child (3) beats the node, so they swap. |
| 15 | L4 | op=poll() | Swapped: 3 moves up to index 0. |
| 16 | L3 | op=offer(4) size=4 | offer(4): append at the end (index 3), the only place that keeps the tree complete. |
| 17 | L3 | op=offer(4) child=4 parent=5 | The child has higher priority than its parent, so they swap. |
| 18 | L3 | op=offer(4) | Swapped: 4 moves up to index 1. |
| 19 | L3 | op=offer(4) child=4 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 20 | L4 | op=poll() returned=3 size=3 | poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down. |
| 21 | L4 | op=poll() node=5 better child=4 | The better child (4) beats the node, so they swap. |
| 22 | L4 | op=poll() | Swapped: 4 moves up to index 0. |
| 23 | L4 | op=poll() returned=4 size=2 | poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down. |
| 24 | L4 | op=poll() node=8 better child=5 | The better child (5) beats the node, so they swap. |
| 25 | L4 | op=poll() | Swapped: 5 moves up to index 0. |
| 26 | L5 | size=2 return=[5, 8] | Heap array: [5, 8]. Only the root is guaranteed to be the best value; the rest is just heap-ordered, not sorted. |
Min-queue
With no comparator, PriorityQueue<Integer> is a **min-queue**: the smallest number leaves first, because natural order sorts small values first. The root of the heap is the smallest value, so peek() returns the minimum in O(1).
Use a min-queue whenever you repeatedly need the smallest remaining item: the k largest values (keep a min-queue of size k), merging sorted lists, Dijkstra's shortest paths.
1static List<Integer> sortedPolls(int[] values) {2PriorityQueue<Integer> pq = new PriorityQueue<>();3for (int v : values) pq.offer(v);4List<Integer> out = new ArrayList<>();5while (!pq.isEmpty()) out.add(pq.poll()); // ascending: n polls of O(log n) each6return out;7}
PriorityQueue: offer, poll and peek on a binary heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
polled so far (in order)
An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);3pq.offer(x); // append at the end, then sift up4Integer top = pq.poll(); // last element to the root, then sift down5Integer next = pq.peek(); // the root only; nothing is removed
Live state
size
0
order
min first
Execution trace (dry run table)Show 26 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 order=min first | An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2. |
| 2 | L3 | op=offer(5) size=1 | offer(5): append at the end (index 0), the only place that keeps the tree complete. |
| 3 | L3 | op=offer(3) size=2 | offer(3): append at the end (index 1), the only place that keeps the tree complete. |
| 4 | L3 | op=offer(3) child=3 parent=5 | The child has higher priority than its parent, so they swap. |
| 5 | L3 | op=offer(3) | Swapped: 3 moves up to index 0. |
| 6 | L3 | op=offer(8) size=3 | offer(8): append at the end (index 2), the only place that keeps the tree complete. |
| 7 | L3 | op=offer(8) child=8 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 8 | L3 | op=offer(1) size=4 | offer(1): append at the end (index 3), the only place that keeps the tree complete. |
| 9 | L3 | op=offer(1) child=1 parent=5 | The child has higher priority than its parent, so they swap. |
| 10 | L3 | op=offer(1) | Swapped: 1 moves up to index 1. |
| 11 | L3 | op=offer(1) child=1 parent=3 | The child has higher priority than its parent, so they swap. |
| 12 | L3 | op=offer(1) | Swapped: 1 moves up to index 0. |
| 13 | L4 | op=poll() returned=1 size=3 | poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down. |
| 14 | L4 | op=poll() node=5 better child=3 | The better child (3) beats the node, so they swap. |
| 15 | L4 | op=poll() | Swapped: 3 moves up to index 0. |
| 16 | L3 | op=offer(4) size=4 | offer(4): append at the end (index 3), the only place that keeps the tree complete. |
| 17 | L3 | op=offer(4) child=4 parent=5 | The child has higher priority than its parent, so they swap. |
| 18 | L3 | op=offer(4) | Swapped: 4 moves up to index 1. |
| 19 | L3 | op=offer(4) child=4 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 20 | L4 | op=poll() returned=3 size=3 | poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down. |
| 21 | L4 | op=poll() node=5 better child=4 | The better child (4) beats the node, so they swap. |
| 22 | L4 | op=poll() | Swapped: 4 moves up to index 0. |
| 23 | L4 | op=poll() returned=4 size=2 | poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down. |
| 24 | L4 | op=poll() node=8 better child=5 | The better child (5) beats the node, so they swap. |
| 25 | L4 | op=poll() | Swapped: 5 moves up to index 0. |
| 26 | L5 | size=2 return=[5, 8] | Heap array: [5, 8]. Only the root is guaranteed to be the best value; the rest is just heap-ordered, not sorted. |
Max-queue
To get the largest value first, reverse the order: new PriorityQueue<>(Comparator.reverseOrder()) (or Collections.reverseOrder()). Choose “Max-queue” in the animation: the root is now the largest value and every comparison flips.
Do not write the comparator as (a, b) -> b - a. The subtraction overflows for values far apart: with Integer.MIN_VALUE, 1 and Integer.MAX_VALUE such a queue polls them in the order −2147483648, 2147483647, 1, which is not descending. Use Integer.compare(b, a) or Comparator.reverseOrder().
1static PriorityQueue<Integer> maxQueue() {2return new PriorityQueue<>(Comparator.reverseOrder()); // safe3}4static PriorityQueue<Integer> alsoSafe() {5return new PriorityQueue<>((a, b) -> Integer.compare(b, a)); // never subtract6}
PriorityQueue: offer, poll and peek on a binary heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
polled so far (in order)
An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);3pq.offer(x); // append at the end, then sift up4Integer top = pq.poll(); // last element to the root, then sift down5Integer next = pq.peek(); // the root only; nothing is removed
Live state
size
0
order
min first
Execution trace (dry run table)Show 26 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 order=min first | An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2. |
| 2 | L3 | op=offer(5) size=1 | offer(5): append at the end (index 0), the only place that keeps the tree complete. |
| 3 | L3 | op=offer(3) size=2 | offer(3): append at the end (index 1), the only place that keeps the tree complete. |
| 4 | L3 | op=offer(3) child=3 parent=5 | The child has higher priority than its parent, so they swap. |
| 5 | L3 | op=offer(3) | Swapped: 3 moves up to index 0. |
| 6 | L3 | op=offer(8) size=3 | offer(8): append at the end (index 2), the only place that keeps the tree complete. |
| 7 | L3 | op=offer(8) child=8 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 8 | L3 | op=offer(1) size=4 | offer(1): append at the end (index 3), the only place that keeps the tree complete. |
| 9 | L3 | op=offer(1) child=1 parent=5 | The child has higher priority than its parent, so they swap. |
| 10 | L3 | op=offer(1) | Swapped: 1 moves up to index 1. |
| 11 | L3 | op=offer(1) child=1 parent=3 | The child has higher priority than its parent, so they swap. |
| 12 | L3 | op=offer(1) | Swapped: 1 moves up to index 0. |
| 13 | L4 | op=poll() returned=1 size=3 | poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down. |
| 14 | L4 | op=poll() node=5 better child=3 | The better child (3) beats the node, so they swap. |
| 15 | L4 | op=poll() | Swapped: 3 moves up to index 0. |
| 16 | L3 | op=offer(4) size=4 | offer(4): append at the end (index 3), the only place that keeps the tree complete. |
| 17 | L3 | op=offer(4) child=4 parent=5 | The child has higher priority than its parent, so they swap. |
| 18 | L3 | op=offer(4) | Swapped: 4 moves up to index 1. |
| 19 | L3 | op=offer(4) child=4 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 20 | L4 | op=poll() returned=3 size=3 | poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down. |
| 21 | L4 | op=poll() node=5 better child=4 | The better child (4) beats the node, so they swap. |
| 22 | L4 | op=poll() | Swapped: 4 moves up to index 0. |
| 23 | L4 | op=poll() returned=4 size=2 | poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down. |
| 24 | L4 | op=poll() node=8 better child=5 | The better child (5) beats the node, so they swap. |
| 25 | L4 | op=poll() | Swapped: 5 moves up to index 0. |
| 26 | L5 | size=2 return=[5, 8] | Heap array: [5, 8]. Only the root is guaranteed to be the best value; the rest is just heap-ordered, not sorted. |
Duplicates
A priority queue happily stores equal priorities. What it does *not* promise is FIFO order among them: PriorityQueue is not stable. Offering tasks a, b, c, d, e, f with priorities 2, 1, 2, 1, 2, 1 and polling them returns b d f a e c: the priority-1 tasks come first, but among the priority-2 tasks the order a, e, c differs from the arrival order a, c, e.
If equal priorities must be served in arrival order, add a tie-breaker to the comparator, usually an increasing sequence number. With a secondary comparison on the name the order becomes the fully determined b d f a c e.
Choose the “Duplicates” preset to see equal values sift without swapping: a tie never moves a node, which is exactly why order among equals is arbitrary.
1record Task(int priority, String name) {}23static String drain(boolean tieBreak) {4Comparator<Task> byPriority = Comparator.comparingInt(Task::priority);5PriorityQueue<Task> pq = new PriorityQueue<>(tieBreak ? byPriority.thenComparing(Task::name) : byPriority);6String[] names = {"a", "b", "c", "d", "e", "f"};7int[] priority = {2, 1, 2, 1, 2, 1};8for (int i = 0; i < names.length; i++) pq.offer(new Task(priority[i], names[i]));9StringBuilder sb = new StringBuilder();10while (!pq.isEmpty()) sb.append(pq.poll().name());11return sb.toString(); // drain(false) = "bdfaec", drain(true) = "bdface"12}
PriorityQueue: offer, poll and peek on a binary heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
polled so far (in order)
An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);3pq.offer(x); // append at the end, then sift up4Integer top = pq.poll(); // last element to the root, then sift down5Integer next = pq.peek(); // the root only; nothing is removed
Live state
size
0
order
min first
Execution trace (dry run table)Show 26 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 order=min first | An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2. |
| 2 | L3 | op=offer(5) size=1 | offer(5): append at the end (index 0), the only place that keeps the tree complete. |
| 3 | L3 | op=offer(3) size=2 | offer(3): append at the end (index 1), the only place that keeps the tree complete. |
| 4 | L3 | op=offer(3) child=3 parent=5 | The child has higher priority than its parent, so they swap. |
| 5 | L3 | op=offer(3) | Swapped: 3 moves up to index 0. |
| 6 | L3 | op=offer(8) size=3 | offer(8): append at the end (index 2), the only place that keeps the tree complete. |
| 7 | L3 | op=offer(8) child=8 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 8 | L3 | op=offer(1) size=4 | offer(1): append at the end (index 3), the only place that keeps the tree complete. |
| 9 | L3 | op=offer(1) child=1 parent=5 | The child has higher priority than its parent, so they swap. |
| 10 | L3 | op=offer(1) | Swapped: 1 moves up to index 1. |
| 11 | L3 | op=offer(1) child=1 parent=3 | The child has higher priority than its parent, so they swap. |
| 12 | L3 | op=offer(1) | Swapped: 1 moves up to index 0. |
| 13 | L4 | op=poll() returned=1 size=3 | poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down. |
| 14 | L4 | op=poll() node=5 better child=3 | The better child (3) beats the node, so they swap. |
| 15 | L4 | op=poll() | Swapped: 3 moves up to index 0. |
| 16 | L3 | op=offer(4) size=4 | offer(4): append at the end (index 3), the only place that keeps the tree complete. |
| 17 | L3 | op=offer(4) child=4 parent=5 | The child has higher priority than its parent, so they swap. |
| 18 | L3 | op=offer(4) | Swapped: 4 moves up to index 1. |
| 19 | L3 | op=offer(4) child=4 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 20 | L4 | op=poll() returned=3 size=3 | poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down. |
| 21 | L4 | op=poll() node=5 better child=4 | The better child (4) beats the node, so they swap. |
| 22 | L4 | op=poll() | Swapped: 4 moves up to index 0. |
| 23 | L4 | op=poll() returned=4 size=2 | poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down. |
| 24 | L4 | op=poll() node=8 better child=5 | The better child (5) beats the node, so they swap. |
| 25 | L4 | op=poll() | Swapped: 5 moves up to index 0. |
| 26 | L5 | size=2 return=[5, 8] | Heap array: [5, 8]. Only the root is guaranteed to be the best value; the rest is just heap-ordered, not sorted. |
Custom objects
For your own classes, either implement Comparable<T> (one natural order) or pass a Comparator<T> to the constructor (any number of orders, chosen at the call site). Records and Comparator.comparingInt(...).thenComparing(...) make this short and readable.
The comparator must only use fields that do not change while the object sits in the queue. If a priority changes, the heap is not told: remove and re-insert the element instead (an O(n) remove plus an O(log n) offer), or use a lazy approach where stale entries are skipped when polled.
1record Job(String name, int priority, long createdAt) {}23static PriorityQueue<Job> jobQueue() {4return new PriorityQueue<>(5Comparator.comparingInt(Job::priority).reversed() // higher priority first6.thenComparingLong(Job::createdAt)); // equal priority: older first7}
Practice ladder
Now try it yourself
Easy
- Kth Largest Element in an Array
- Last Stone Weight
Medium
- Top K Frequent Elements
- Task Scheduler
Advanced
- Merge k Sorted Lists
- Find Median from Data Stream
Key takeaways
- A priority queue serves the best value first, as defined by a comparator.
- PriorityQueue is a binary heap: offer and poll are O(log n), peek is O(1), iteration is not sorted.
- Never write comparators as b - a, and add a tie-breaker when equal priorities must be FIFO.