PriorityQueue
PriorityQueue is an unbounded queue implementation backed by a binary heap that orders elements by priority rather than insertion order. The element with the lowest priority value (by natural ordering or a supplied Comparator) is always at the head and is the first to be removed. PriorityQueue is the standard Java implementation of the priority queue abstract data type — a fundamental structure in algorithms for task scheduling, graph shortest-path algorithms (Dijkstra, A*), and any scenario where the next item to process should be the most urgent or important rather than the oldest. This entry covers the binary heap internals, natural and custom ordering, the performance characteristics of each operation, the behaviour of iteration vs retrieval, and practical algorithmic use cases.
Binary Heap Internals and Performance
// ── PriorityQueue construction and basic operations ───────────────────
// Default: natural ordering (elements must be Comparable)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
minHeap.offer(2);
minHeap.offer(4);
System.out.println(minHeap.peek()); // 1 — minimum, O(1)
// ── Iteration vs ordered retrieval — the critical distinction ─────────
System.out.println("Iteration (unordered):");
for (int n : minHeap) {
System.out.print(n + " "); // e.g. 1 2 3 5 4 — heap storage order, NOT sorted
}
System.out.println("
Ordered retrieval (poll):");
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " "); // 1 2 3 4 5 — sorted, each poll O(log n)
}
// ── Internal heap array structure ─────────────────────────────────────
//
// After inserting 5, 1, 3, 2, 4 into a min-heap:
//
// 1 index 0 (root = minimum)
// / // 2 3 index 1, 2
// / // 5 4 index 3, 4
//
// Array: [1, 2, 3, 5, 4]
//
// Parent of index i: (i-1)/2
// Left child of i: 2i+1
// Right child of i: 2i+2
//
// Insertion of 0:
// Array: [1, 2, 3, 5, 4, 0] ← added at end
// Sift up: 0 < parent(3) → swap: [1, 2, 0, 5, 4, 3]
// 0 < parent(1) → swap: [0, 2, 1, 5, 4, 3] ← heap restored
// ── Performance summary ───────────────────────────────────────────────
// offer(e) O(log n) insert + sift up
// poll() O(log n) remove min + sift down
// peek() O(1) read root element
// remove(o) O(n) linear scan + O(log n) sift — avoid in hot paths
// contains(o) O(n) linear scan
// size() O(1) field read
// toArray() O(n) copy array
// All other Collection methods: O(n)Natural Ordering and Custom Comparators
// ── Natural ordering — min-heap by default ───────────────────────────
PriorityQueue<String> strings = new PriorityQueue<>();
strings.offer("banana");
strings.offer("apple");
strings.offer("cherry");
System.out.println(strings.poll()); // "apple" — lexicographically smallest
// ── Max-heap using reverseOrder ───────────────────────────────────────
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(4);
maxHeap.offer(1);
maxHeap.offer(5);
System.out.println(maxHeap.poll()); // 5 — largest value at head
// ── Custom ordering for domain objects ───────────────────────────────
record Task(String name, int priority, Instant submitted) {}
// Higher priority number = more urgent (reverse int order)
// Tie-break: earlier submission first (natural Instant order)
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
Comparator.comparingInt(Task::priority).reversed()
.thenComparing(Task::submitted));
taskQueue.offer(new Task("low-task", 1, Instant.now()));
taskQueue.offer(new Task("high-task", 9, Instant.now().plusSeconds(1)));
taskQueue.offer(new Task("urgent-task", 9, Instant.now().minusSeconds(1)));
System.out.println(taskQueue.poll().name()); // "urgent-task" — priority 9, earlier
System.out.println(taskQueue.poll().name()); // "high-task" — priority 9, later
System.out.println(taskQueue.poll().name()); // "low-task" — priority 1
// ── DANGER: subtraction antipattern ──────────────────────────────────
// WRONG — overflows near Integer.MIN_VALUE / Integer.MAX_VALUE:
PriorityQueue<Integer> danger =
new PriorityQueue<>((a, b) -> a - b); // DO NOT DO THIS
// CORRECT — type-safe comparison:
PriorityQueue<Integer> safe =
new PriorityQueue<>(Integer::compare);
// CORRECT — factory method:
PriorityQueue<Integer> safe2 =
new PriorityQueue<>(Comparator.comparingInt(Integer::intValue));
// ── Initial capacity hint ─────────────────────────────────────────────
// PriorityQueue default capacity is 11
// If size is known, provide initial capacity to avoid resizing:
PriorityQueue<Integer> sized = new PriorityQueue<>(10_000);
// Avoids reallocation cost during bulk insertionAlgorithmic Applications
// ── Dijkstra's shortest path ─────────────────────────────────────────
public Map<Integer, Integer> dijkstra(
Map<Integer, List<int[]>> graph, int source) {
Map<Integer, Integer> dist = new HashMap<>();
// PriorityQueue of [distance, node] — min-heap by distance
PriorityQueue<int[]> pq =
new PriorityQueue<>(Comparator.comparingInt(e -> e[0]));
pq.offer(new int[]{0, source});
dist.put(source, 0);
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0];
int node = curr[1];
if (d > dist.getOrDefault(node, Integer.MAX_VALUE)) continue;
for (int[] edge : graph.getOrDefault(node, List.of())) {
int neighbour = edge[0];
int newDist = d + edge[1];
if (newDist < dist.getOrDefault(neighbour, Integer.MAX_VALUE)) {
dist.put(neighbour, newDist);
pq.offer(new int[]{newDist, neighbour});
}
}
}
return dist;
}
// ── K smallest elements (max-heap of size k) ──────────────────────────
public int[] kSmallest(int[] nums, int k) {
// Max-heap of size k: always contains the k smallest elements seen
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(k, Comparator.reverseOrder());
for (int num : nums) {
maxHeap.offer(num);
if (maxHeap.size() > k) {
maxHeap.poll(); // remove largest — keep only k smallest
}
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = maxHeap.poll(); // drain in ascending order
}
return result;
}
// ── K-way merge of sorted lists ───────────────────────────────────────
public List<Integer> kWayMerge(List<List<Integer>> sortedLists) {
// [value, listIndex, elementIndex]
PriorityQueue<int[]> pq =
new PriorityQueue<>(Comparator.comparingInt(e -> e[0]));
for (int i = 0; i < sortedLists.size(); i++) {
if (!sortedLists.get(i).isEmpty()) {
pq.offer(new int[]{sortedLists.get(i).get(0), i, 0});
}
}
List<Integer> result = new ArrayList<>();
while (!pq.isEmpty()) {
int[] curr = pq.poll();
result.add(curr[0]);
int listIdx = curr[1];
int elemIdx = curr[2] + 1;
if (elemIdx < sortedLists.get(listIdx).size()) {
pq.offer(new int[]{
sortedLists.get(listIdx).get(elemIdx),
listIdx,
elemIdx
});
}
}
return result;
}PriorityQueue Limitations and Thread Safety
// ── Thread-safe priority queue ────────────────────────────────────────
// PriorityBlockingQueue — thread-safe, unbounded, blocking on take()
PriorityBlockingQueue<Task> safeQueue =
new PriorityBlockingQueue<>(11,
Comparator.comparingInt(Task::priority).reversed());
// Producer thread — never blocks (unbounded)
executor.submit(() -> {
safeQueue.offer(new Task("task1", 5));
safeQueue.offer(new Task("urgent", 9));
});
// Consumer thread — blocks if empty
executor.submit(() -> {
try {
while (true) {
Task task = safeQueue.take(); // blocks if empty
process(task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// ── Lazy deletion for priority update ────────────────────────────────
// Dijkstra with re-insertion instead of decrease-key:
Set<Integer> processed = new HashSet<>();
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int dist = curr[0], node = curr[1];
// Skip stale entries — this node was already processed with a shorter dist
if (!processed.add(node)) continue; // add returns false if already present
for (int[] edge : graph.getOrDefault(node, List.of())) {
int newDist = dist + edge[1];
if (newDist < distances.getOrDefault(edge[0], Integer.MAX_VALUE)) {
distances.put(edge[0], newDist);
pq.offer(new int[]{newDist, edge[0]}); // re-insert with new priority
// old entry for edge[0] is still in heap but will be skipped
}
}
}
// ── Draining to sorted list ───────────────────────────────────────────
PriorityQueue<Integer> pq2 = new PriorityQueue<>(
List.of(5, 2, 8, 1, 9, 3));
// Sorted in priority order:
List<Integer> sorted = new ArrayList<>();
while (!pq2.isEmpty()) sorted.add(pq2.poll());
// sorted: [1, 2, 3, 5, 8, 9]
// Alternative — Collections.sort is faster for one-time sorting:
// If you just want sorted output, Arrays.sort is O(n log n) with better constants