Chapter 13: Queues, Deques and Circular Buffers
13.05Beginner / Intermediate6 topics16 min4 interactive animations

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

The queue modelArrays and dynamic arrays

An emergency room is a priority queue: patients are served by urgency, not arrival time, and a new arrival can overtake everybody waiting.

13.05.1

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.

Java
1static void order() {
2 PriorityQueue<String> byLength = new PriorityQueue<>(Comparator.comparingInt(String::length));
3 byLength.offer("pear"); byLength.offer("fig"); byLength.offer("banana");
4 System.out.println(byLength.poll()); // fig: the shortest word has the highest priority
5}
13.05.2

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 input

Your input

Try

The heap is empty.

int[] arr · length 0

Being comparedJust movedRoot (next to leave)

polled so far (in order)

none yet
Setup

An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.

Step 1 / 26
Java
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue
2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);
3pq.offer(x); // append at the end, then sift up
4Integer top = pq.poll(); // last element to the root, then sift down
5Integer next = pq.peek(); // the root only; nothing is removed

Live state

0 comparisons0 heap swaps

size

0

order

min first

No condition evaluated on this step
Execution trace (dry run table)Show 26 steps
StepLineStateWhat happens
1L2size=0 order=min firstAn empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
2L3op=offer(5) size=1offer(5): append at the end (index 0), the only place that keeps the tree complete.
3L3op=offer(3) size=2offer(3): append at the end (index 1), the only place that keeps the tree complete.
4L3op=offer(3) child=3 parent=5The child has higher priority than its parent, so they swap.
5L3op=offer(3)Swapped: 3 moves up to index 0.
6L3op=offer(8) size=3offer(8): append at the end (index 2), the only place that keeps the tree complete.
7L3op=offer(8) child=8 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
8L3op=offer(1) size=4offer(1): append at the end (index 3), the only place that keeps the tree complete.
9L3op=offer(1) child=1 parent=5The child has higher priority than its parent, so they swap.
10L3op=offer(1)Swapped: 1 moves up to index 1.
11L3op=offer(1) child=1 parent=3The child has higher priority than its parent, so they swap.
12L3op=offer(1)Swapped: 1 moves up to index 0.
13L4op=poll() returned=1 size=3poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down.
14L4op=poll() node=5 better child=3The better child (3) beats the node, so they swap.
15L4op=poll()Swapped: 3 moves up to index 0.
16L3op=offer(4) size=4offer(4): append at the end (index 3), the only place that keeps the tree complete.
17L3op=offer(4) child=4 parent=5The child has higher priority than its parent, so they swap.
18L3op=offer(4)Swapped: 4 moves up to index 1.
19L3op=offer(4) child=4 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
20L4op=poll() returned=3 size=3poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down.
21L4op=poll() node=5 better child=4The better child (4) beats the node, so they swap.
22L4op=poll()Swapped: 4 moves up to index 0.
23L4op=poll() returned=4 size=2poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down.
24L4op=poll() node=8 better child=5The better child (5) beats the node, so they swap.
25L4op=poll()Swapped: 5 moves up to index 0.
26L5size=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.
13.05.3

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.

Java
1static List<Integer> sortedPolls(int[] values) {
2 PriorityQueue<Integer> pq = new PriorityQueue<>();
3 for (int v : values) pq.offer(v);
4 List<Integer> out = new ArrayList<>();
5 while (!pq.isEmpty()) out.add(pq.poll()); // ascending: n polls of O(log n) each
6 return out;
7}

PriorityQueue: offer, poll and peek on a binary heap

Interactive · edit the input

Your input

Try

The heap is empty.

int[] arr · length 0

Being comparedJust movedRoot (next to leave)

polled so far (in order)

none yet
Setup

An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.

Step 1 / 26
Java
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue
2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);
3pq.offer(x); // append at the end, then sift up
4Integer top = pq.poll(); // last element to the root, then sift down
5Integer next = pq.peek(); // the root only; nothing is removed

Live state

0 comparisons0 heap swaps

size

0

order

min first

No condition evaluated on this step
Execution trace (dry run table)Show 26 steps
StepLineStateWhat happens
1L2size=0 order=min firstAn empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
2L3op=offer(5) size=1offer(5): append at the end (index 0), the only place that keeps the tree complete.
3L3op=offer(3) size=2offer(3): append at the end (index 1), the only place that keeps the tree complete.
4L3op=offer(3) child=3 parent=5The child has higher priority than its parent, so they swap.
5L3op=offer(3)Swapped: 3 moves up to index 0.
6L3op=offer(8) size=3offer(8): append at the end (index 2), the only place that keeps the tree complete.
7L3op=offer(8) child=8 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
8L3op=offer(1) size=4offer(1): append at the end (index 3), the only place that keeps the tree complete.
9L3op=offer(1) child=1 parent=5The child has higher priority than its parent, so they swap.
10L3op=offer(1)Swapped: 1 moves up to index 1.
11L3op=offer(1) child=1 parent=3The child has higher priority than its parent, so they swap.
12L3op=offer(1)Swapped: 1 moves up to index 0.
13L4op=poll() returned=1 size=3poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down.
14L4op=poll() node=5 better child=3The better child (3) beats the node, so they swap.
15L4op=poll()Swapped: 3 moves up to index 0.
16L3op=offer(4) size=4offer(4): append at the end (index 3), the only place that keeps the tree complete.
17L3op=offer(4) child=4 parent=5The child has higher priority than its parent, so they swap.
18L3op=offer(4)Swapped: 4 moves up to index 1.
19L3op=offer(4) child=4 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
20L4op=poll() returned=3 size=3poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down.
21L4op=poll() node=5 better child=4The better child (4) beats the node, so they swap.
22L4op=poll()Swapped: 4 moves up to index 0.
23L4op=poll() returned=4 size=2poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down.
24L4op=poll() node=8 better child=5The better child (5) beats the node, so they swap.
25L4op=poll()Swapped: 5 moves up to index 0.
26L5size=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.
13.05.4

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().

Java
1static PriorityQueue<Integer> maxQueue() {
2 return new PriorityQueue<>(Comparator.reverseOrder()); // safe
3}
4static PriorityQueue<Integer> alsoSafe() {
5 return new PriorityQueue<>((a, b) -> Integer.compare(b, a)); // never subtract
6}

PriorityQueue: offer, poll and peek on a binary heap

Interactive · edit the input

Your input

Try

The heap is empty.

int[] arr · length 0

Being comparedJust movedRoot (next to leave)

polled so far (in order)

none yet
Setup

An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.

Step 1 / 26
Java
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue
2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);
3pq.offer(x); // append at the end, then sift up
4Integer top = pq.poll(); // last element to the root, then sift down
5Integer next = pq.peek(); // the root only; nothing is removed

Live state

0 comparisons0 heap swaps

size

0

order

min first

No condition evaluated on this step
Execution trace (dry run table)Show 26 steps
StepLineStateWhat happens
1L2size=0 order=min firstAn empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
2L3op=offer(5) size=1offer(5): append at the end (index 0), the only place that keeps the tree complete.
3L3op=offer(3) size=2offer(3): append at the end (index 1), the only place that keeps the tree complete.
4L3op=offer(3) child=3 parent=5The child has higher priority than its parent, so they swap.
5L3op=offer(3)Swapped: 3 moves up to index 0.
6L3op=offer(8) size=3offer(8): append at the end (index 2), the only place that keeps the tree complete.
7L3op=offer(8) child=8 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
8L3op=offer(1) size=4offer(1): append at the end (index 3), the only place that keeps the tree complete.
9L3op=offer(1) child=1 parent=5The child has higher priority than its parent, so they swap.
10L3op=offer(1)Swapped: 1 moves up to index 1.
11L3op=offer(1) child=1 parent=3The child has higher priority than its parent, so they swap.
12L3op=offer(1)Swapped: 1 moves up to index 0.
13L4op=poll() returned=1 size=3poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down.
14L4op=poll() node=5 better child=3The better child (3) beats the node, so they swap.
15L4op=poll()Swapped: 3 moves up to index 0.
16L3op=offer(4) size=4offer(4): append at the end (index 3), the only place that keeps the tree complete.
17L3op=offer(4) child=4 parent=5The child has higher priority than its parent, so they swap.
18L3op=offer(4)Swapped: 4 moves up to index 1.
19L3op=offer(4) child=4 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
20L4op=poll() returned=3 size=3poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down.
21L4op=poll() node=5 better child=4The better child (4) beats the node, so they swap.
22L4op=poll()Swapped: 4 moves up to index 0.
23L4op=poll() returned=4 size=2poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down.
24L4op=poll() node=8 better child=5The better child (5) beats the node, so they swap.
25L4op=poll()Swapped: 5 moves up to index 0.
26L5size=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.
13.05.5

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.

Java
1record Task(int priority, String name) {}
2
3static String drain(boolean tieBreak) {
4 Comparator<Task> byPriority = Comparator.comparingInt(Task::priority);
5 PriorityQueue<Task> pq = new PriorityQueue<>(tieBreak ? byPriority.thenComparing(Task::name) : byPriority);
6 String[] names = {"a", "b", "c", "d", "e", "f"};
7 int[] priority = {2, 1, 2, 1, 2, 1};
8 for (int i = 0; i < names.length; i++) pq.offer(new Task(priority[i], names[i]));
9 StringBuilder sb = new StringBuilder();
10 while (!pq.isEmpty()) sb.append(pq.poll().name());
11 return sb.toString(); // drain(false) = "bdfaec", drain(true) = "bdface"
12}

PriorityQueue: offer, poll and peek on a binary heap

Interactive · edit the input

Your input

Try

The heap is empty.

int[] arr · length 0

Being comparedJust movedRoot (next to leave)

polled so far (in order)

none yet
Setup

An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.

Step 1 / 26
Java
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue
2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);
3pq.offer(x); // append at the end, then sift up
4Integer top = pq.poll(); // last element to the root, then sift down
5Integer next = pq.peek(); // the root only; nothing is removed

Live state

0 comparisons0 heap swaps

size

0

order

min first

No condition evaluated on this step
Execution trace (dry run table)Show 26 steps
StepLineStateWhat happens
1L2size=0 order=min firstAn empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
2L3op=offer(5) size=1offer(5): append at the end (index 0), the only place that keeps the tree complete.
3L3op=offer(3) size=2offer(3): append at the end (index 1), the only place that keeps the tree complete.
4L3op=offer(3) child=3 parent=5The child has higher priority than its parent, so they swap.
5L3op=offer(3)Swapped: 3 moves up to index 0.
6L3op=offer(8) size=3offer(8): append at the end (index 2), the only place that keeps the tree complete.
7L3op=offer(8) child=8 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
8L3op=offer(1) size=4offer(1): append at the end (index 3), the only place that keeps the tree complete.
9L3op=offer(1) child=1 parent=5The child has higher priority than its parent, so they swap.
10L3op=offer(1)Swapped: 1 moves up to index 1.
11L3op=offer(1) child=1 parent=3The child has higher priority than its parent, so they swap.
12L3op=offer(1)Swapped: 1 moves up to index 0.
13L4op=poll() returned=1 size=3poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down.
14L4op=poll() node=5 better child=3The better child (3) beats the node, so they swap.
15L4op=poll()Swapped: 3 moves up to index 0.
16L3op=offer(4) size=4offer(4): append at the end (index 3), the only place that keeps the tree complete.
17L3op=offer(4) child=4 parent=5The child has higher priority than its parent, so they swap.
18L3op=offer(4)Swapped: 4 moves up to index 1.
19L3op=offer(4) child=4 parent=3The parent keeps priority over the child (ties do not swap): sift up stops.
20L4op=poll() returned=3 size=3poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down.
21L4op=poll() node=5 better child=4The better child (4) beats the node, so they swap.
22L4op=poll()Swapped: 4 moves up to index 0.
23L4op=poll() returned=4 size=2poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down.
24L4op=poll() node=8 better child=5The better child (5) beats the node, so they swap.
25L4op=poll()Swapped: 5 moves up to index 0.
26L5size=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.
13.05.6

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.

Java
1record Job(String name, int priority, long createdAt) {}
2
3static PriorityQueue<Job> jobQueue() {
4 return new PriorityQueue<>(
5 Comparator.comparingInt(Job::priority).reversed() // higher priority first
6 .thenComparingLong(Job::createdAt)); // equal priority: older first
7}

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.