Priority Queue — Junior Level¶
One-line summary: A priority queue is an abstract data type (ADT) that hands you items in priority order rather than FIFO order. The most common implementation is a binary heap, but the interface —
push,pop-highest-priority,peek— is what defines a priority queue, not the data structure underneath it.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Choosing an Implementation
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
A normal queue is first-in, first-out: the order things come out is the order they went in. A priority queue is highest-priority-out: the order things come out is determined by an importance value attached to each item.
It is one of the most common ADTs in computer science precisely because real life rarely respects insertion order. An operating system runs the most urgent task first. A hospital sees the most critical patient first. Dijkstra's algorithm processes the closest unsettled node first. A Kafka consumer with retry priorities pulls failed messages back into the front of the line.
The critical mental shift at this stage: priority queue is an interface, not a data structure. Five different data structures (binary heap, sorted array, BST, skip list, Fibonacci heap) all support the same push / pop-min / peek operations with very different time-and-space trade-offs. Picking one is part of your job as a programmer.
In practice, "priority queue" without qualification almost always means "binary-heap-backed priority queue" — heapq in Python, PriorityQueue in Java, container/heap in Go, std::priority_queue in C++. That binding is so strong that many programmers conflate the two. Try to keep them separate in your head: heaps are one way to build a priority queue, the way most often used because it's simple and fast.
Prerequisites¶
- Queue ADT — FIFO order:
enqueue,dequeue. - Stack ADT — LIFO order, for contrast.
- Big-O notation —
O(1),O(log n),O(n). - Binary heap (see
../01-binary-heap/junior.md) — the default implementation. - Basic comparison operators —
<,>and the idea of a comparator function.
Helpful but not required:
- BST and sorted-list mental model.
- Knowledge that "priority" in CS is lower-value-comes-first by convention (min-heap), unless explicitly inverted.
Glossary¶
| Term | Meaning |
|---|---|
| Priority queue (PQ) | An ADT supporting insert(item, priority), pop-best, peek-best. |
| Min-priority queue | "Smallest priority comes out first" (default in heapq, container/heap). |
| Max-priority queue | "Largest priority comes out first" (default in Java PriorityQueue with a reversed comparator; default in C++ std::priority_queue). |
| Comparator | Function or lambda (a, b) → int that defines the priority order. |
| Stability | Equal-priority items come out in insertion order. PQs are not naturally stable; add a counter to make them stable. |
| Bounded PQ | A PQ with a maximum capacity; pushing on a full queue evicts something (often the worst item). |
| Blocking PQ | A thread-safe PQ where pop blocks the calling thread if the queue is empty. |
| Indexed PQ | A PQ that supports decrease-key because it remembers each item's position. |
| Heap-backed PQ | Implemented on top of a binary heap (the usual case). |
| Bucket PQ / radix heap | PQ for bounded integer priorities, achieving O(1) per op. |
Core Concepts¶
1. PQ is an interface¶
push(item, priority) # add an item
pop() # return + remove highest-priority item
peek() # return highest-priority item without removing
size(), empty()
That's it. Any data structure that provides those four operations is a priority queue, no matter how it's built inside.
2. Priority order: min or max¶
By convention in algorithm textbooks, "highest priority" = "smallest priority value." Lower numbers mean more urgent. This is consistent with shortest-distance dictionaries: smaller dist = process sooner.
Min-PQ: pop() returns the item with the smallest priority key.
Max-PQ: pop() returns the item with the largest priority key.
If your library only gives you a min-PQ but you need a max-PQ, the two standard tricks are:
- Push
-priorityinstead ofpriority(works for numeric keys). - Pass a reversed comparator:
(a, b) → b.priority - a.priority.
3. Storing the priority with the item¶
Most PQs store (priority, item) pairs or use a comparator that knows how to extract the priority. The item is the payload; the priority is the key.
A subtle pitfall: if Python's heapq compares two pairs whose first elements are equal, it goes on to compare the second elements. If those are objects without an __lt__ method, you get a TypeError. The fix is the tiebreaker counter pattern: (priority, counter, item) — the counter guarantees the second component always orders.
4. Stability¶
A priority queue is stable if items with equal priority come out in the same order they went in. Binary heaps are not stable by themselves. To make them stable, you push (priority, counter, item); the counter is an ever-increasing integer.
5. Indexed PQ for decrease-key¶
When the priority of an existing item can change (Dijkstra finds a shorter path mid-run), you need:
To support this in O(log n), the PQ must remember where each item lives inside its underlying structure. That's an indexed PQ. See middle.md for the implementation.
Big-O Summary¶
| Operation | Binary-heap PQ | Sorted-array PQ | BST PQ | Fibonacci-heap PQ |
|---|---|---|---|---|
peek | O(1) | O(1) | O(log n) | O(1) |
push | O(log n) | O(n) | O(log n) | O(1) amortised |
pop | O(log n) | O(1) | O(log n) | O(log n) |
decrease-key | O(log n) (indexed) | O(n) | O(log n) | O(1) amortised |
meld | O(n) | O(n) | O(n log n) | O(1) |
| Space | O(n) | O(n) | O(n) | O(n) (extra pointers) |
Different implementations dominate on different operation mixes — that's the whole point of treating PQ as an ADT.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
| Min-PQ | Hospital triage — the most critical patient is seen first, regardless of arrival order. |
| Tiebreaker counter | Two patients with identical injury severity — first to arrive goes in first. |
| Bounded PQ with eviction | A VIP guest list of size 50 — when a more important guest arrives, the lowest-ranked one gets removed. |
| Blocking PQ | A pizza pickup window — staff pop orders by priority; if the queue is empty they wait. |
| Indexed PQ | A waiting-room board where you can find and update your own position when the receptionist gives you a new appointment time. |
Where the analogy breaks: in a real hospital you re-prioritise dozens of patients at once; most heap-backed PQs are happiest doing one mutation at a time.
Pros & Cons¶
| Pros | Cons |
|---|---|
| One small interface unlocks dozens of algorithms (Dijkstra, Prim, A*, Huffman, scheduling). | Choosing the right backing structure requires understanding your operation mix. |
O(log n) per mutation is fast enough for almost all production workloads. | Not naturally stable — equal-priority items come out in unspecified order. |
Bounded variants give natural top-k / leaderboard semantics. | decrease-key requires an indexed implementation or "lazy duplicates." |
| Thread-safe variants ship in every major language stdlib. | Single global PQ becomes a contention bottleneck at high QPS. |
peek is O(1) — perfect for "ready-to-run" scheduler loops. | Merging two PQs is expensive in the default binary-heap form. |
When to use: anywhere "process the most important next" is the natural verb — schedulers, search algorithms, simulators, retry queues, message brokers with priority classes.
When NOT to use: strict FIFO (use a plain queue), bounded integer priorities with high QPS (consider a bucket / radix PQ), or read-heavy "search by value" (heap is O(n) for that).
Choosing an Implementation¶
| Workload | Best choice | Why |
|---|---|---|
| Generic PQ with mixed ops | Binary-heap PQ | Simple, fast, library-supported. |
Many decrease-keys per pop (Dijkstra on dense graphs) | d-ary heap or indexed binary heap | Fewer levels, cheaper sift-up. |
Frequent meld (event simulators) | Leftist / pairing / Fibonacci heap | O(log n) or O(1) meld. |
| Tiny integer priorities, e.g. 0..255 | Bucket / radix heap | Constant-time ops via direct array index. |
| Concurrent producers/consumers | java.util.concurrent.PriorityBlockingQueue or sharded heaps | Built-in lock. |
Top-k running query | Bounded heap of size k | Auto-eviction. |
| Persistent / durable PQ | Redis ZSET, Kafka with priority topics | Disk-backed + replication. |
Code Examples¶
Example 1: Using the language library¶
Go¶
package main
import (
"container/heap"
"fmt"
)
// Int-priority min-PQ on a custom Item type.
type Item struct {
name string
priority int
index int // position in heap (for decrease-key, optional)
}
type PQ []*Item
func (pq PQ) Len() int { return len(pq) }
func (pq PQ) Less(i, j int) bool { return pq[i].priority < pq[j].priority }
func (pq PQ) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i]; pq[i].index = i; pq[j].index = j }
func (pq *PQ) Push(x interface{}) { it := x.(*Item); it.index = len(*pq); *pq = append(*pq, it) }
func (pq *PQ) Pop() interface{} {
old := *pq
n := len(old)
it := old[n-1]
*pq = old[:n-1]
return it
}
func main() {
pq := &PQ{}
heap.Init(pq)
heap.Push(pq, &Item{name: "patch", priority: 3})
heap.Push(pq, &Item{name: "deploy", priority: 1})
heap.Push(pq, &Item{name: "lint", priority: 2})
for pq.Len() > 0 {
it := heap.Pop(pq).(*Item)
fmt.Println(it.priority, it.name)
}
// 1 deploy
// 2 lint
// 3 patch
}
Java¶
import java.util.*;
public class PQExample {
record Job(String name, int priority) {}
public static void main(String[] args) {
PriorityQueue<Job> pq = new PriorityQueue<>(Comparator.comparingInt(Job::priority));
pq.add(new Job("patch", 3));
pq.add(new Job("deploy", 1));
pq.add(new Job("lint", 2));
while (!pq.isEmpty()) {
Job j = pq.poll();
System.out.println(j.priority() + " " + j.name());
}
}
}
Python¶
import heapq
import itertools
class PriorityQueue:
"""Heap-backed PQ with stable ordering via a counter."""
def __init__(self):
self._data = []
self._counter = itertools.count()
def push(self, item, priority):
heapq.heappush(self._data, (priority, next(self._counter), item))
def pop(self):
priority, _, item = heapq.heappop(self._data)
return priority, item
def peek(self):
priority, _, item = self._data[0]
return priority, item
def __len__(self):
return len(self._data)
pq = PriorityQueue()
pq.push("patch", 3)
pq.push("deploy", 1)
pq.push("lint", 2)
while len(pq):
print(*pq.pop())
# 1 deploy
# 2 lint
# 3 patch
What it does: inserts three jobs with priorities and prints them in ascending priority order. Run: go run main.go | javac PQExample.java && java PQExample | python pq.py
Coding Patterns¶
Pattern 1: Top-K with a bounded PQ¶
Intent: Keep only the k smallest items in a stream. Use a max-PQ of size k; whenever the new item is smaller than the current max, evict and replace.
import heapq
def top_k_smallest(stream, k):
h = [] # min-heap of negated values to fake max-heap
for x in stream:
if len(h) < k:
heapq.heappush(h, -x)
elif -x > h[0]: # x < current max
heapq.heapreplace(h, -x)
return sorted(-v for v in h)
Pattern 2: Tiebreaker stability¶
Without the counter, Python compares payloads when priorities tie — and crashes if payloads are not orderable.
Pattern 3: Delayed jobs (priority = deadline)¶
import heapq, time
def scheduler(jobs):
pq = [(deadline, fn) for fn, deadline in jobs]
heapq.heapify(pq)
while pq:
deadline, fn = pq[0]
now = time.monotonic()
if deadline > now:
time.sleep(deadline - now)
heapq.heappop(pq)
fn()
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
IndexError on empty pop/peek | Trying to read from an empty PQ. | Check len(pq) > 0 first or catch the exception. |
TypeError: '<' not supported between Foo and Foo | Equal priorities cause Python to compare payloads. | Push (priority, counter, payload) tuples. |
ConcurrentModificationException | Mutating the PQ while iterating. | Drain via pop() calls, not iteration. |
| Stale entries in lazy Dijkstra cause wrong results | Forgot to skip cur.dist > dist[node]. | Always check freshness after pop. |
| Reverse-comparator gives wrong order in Java | Swapping operand order in Integer.compare(a, b). | Use Comparator.reverseOrder() or (a, b) → b - a (watch for overflow). |
Performance Tips¶
- Reach for the standard library first. They are written in C and use bulk
O(n)heapify. - Use
heapq.heapreplace(orreplacein Java's PQ viapoll+offerin a single critical section) instead ofpopthenpush. - For huge PQs in Python, prefer
heapqon a list overqueue.PriorityQueue(which adds thread-safe locking overhead). - Avoid object allocations inside the heap; pre-allocate items if the workload is hot.
- If priorities are small integers, consider a bucket or radix heap —
O(1)per op.
Best Practices¶
- Define your comparator once and document the order (
minvsmax). - Never mutate an item's priority while it sits in a PQ — wrap
push/pop/decrease-keyaround the field updates. - Push immutable value objects when possible (
recordin Java,frozenset/tuple in Python). - Log the size at periodic checkpoints — runaway growth is the most common PQ bug in production.
- For multi-producer workloads, prefer the language's concurrent PQ rather than building your own lock around a plain heap.
Edge Cases & Pitfalls¶
- Empty PQ: define behaviour of
peek/popup front. Throw, return sentinel, or block. - Duplicate priorities: ensure stability if your downstream expects FIFO among equals.
- Negative priorities: legal but watch for overflow when subtracting in comparators.
NaNpriorities (floats):NaNbreaks total order — guard withassert not isnan(p).- Mutable payloads: make payload comparison a no-op (tiebreaker counter) so equal priorities never reach the payload.
- Unbounded growth in lazy Dijkstra: an early skip check on
popis what keeps it from exploding.
Common Mistakes¶
- Confusing the ADT with binary heap: thinking "priority queue" can only mean "binary heap."
- Forgetting the tiebreaker counter and crashing on equal priorities.
- Using a max-heap when you wanted min, and vice versa.
- Mutating a priority while the item is still in the PQ — the invariant rots.
- Reaching for
decrease-keywithout an indexed structure — there is no way to find the item. - Treating
peekaspopand not seeing the next item ever change.
Cheat Sheet¶
| Operation | Default cost (binary heap) | Notes |
|---|---|---|
peek | O(1) | Read the root. |
push | O(log n) | Sift up. |
pop | O(log n) | Sift down after swap-with-last. |
replace | O(log n) | Pop + push in one sift-down. |
decrease-key | O(log n) | Requires indexed PQ. |
meld | O(n) | Concatenate + bulk heapify. |
| Library: Python | heapq.heappush/heappop | Functions on a list. |
| Library: Go | container/heap.Push/Pop | Implement heap.Interface. |
| Library: Java | PriorityQueue<T> + Comparator | Not thread-safe; use PriorityBlockingQueue. |
Visual Animation¶
See
animation.htmlfor an interactive view.The animation demonstrates: - A PQ as the interface — toggle between three backing structures (binary heap, sorted array, BST) and watch the same operations run with different costs. - Stable vs unstable behaviour with the tiebreaker counter. - A bounded PQ in top-
kmode evicting items when full.
Summary¶
The priority queue is the ADT you reach for whenever "process the most important thing next" is part of your algorithm. It is almost always backed by a binary heap, but it is good practice to mentally distinguish the interface from the implementation — the two are independent decisions, and different workloads favour different backings.
Further Reading¶
- Introduction to Algorithms (CLRS), Chapter 6.5 — "Priority queues."
- Java —
java.util.PriorityQueueandjava.util.concurrent.PriorityBlockingQueueJavadoc. - Go —
container/heappackage docs. - Python —
heapqmodule docs, including "Priority Queue Implementation Notes." - Sedgewick & Wayne, Algorithms, §2.4 — beautifully concise treatment of PQs.