Priority Queues
Imagine an operating system scheduler deciding which of a thousand waiting processes to run next. Or picture a GPS system searching millions of road segments for the shortest route. In both cases, the algorithm repeatedly needs the single "best" item from a large, dynamically changing collection. This might be the process with the earliest deadline. Or it might be the unvisited node with the smallest tentative distance. Sorting the whole collection every time this question is asked would be wasteful. Only the minimum (or maximum) is actually needed. The collection also keeps changing between queries. The priority queue is the abstract data type built exactly for this pattern. It maintains a dynamic set of elements, each tagged with a priority (a key). It supports fast retrieval of the highest-priority element. It also supports fast insertion of new elements, and fast priority updates for existing elements. It generalizes the plain queue (FIFO) and stack (LIFO). Instead, priority, not arrival order, dictates service order. This article treats the priority queue as an interface. It distinguishes it sharply from any one implementation. The workhorse realization is the binary heap. It supports insert, extract-min, and decrease-key all in O(log n) time. This uses an implicit array-based tree. We derive these bounds carefully. We also connect them to the correctness invariant that makes heaps work (the heap-order property). We also show how the ADT powers Dijkstra's shortest-path algorithm, Prim's minimum spanning tree algorithm, Huffman coding, and event-driven simulation. By the end, you will be able to state the priority queue interface precisely. You will be able to implement it on top of a binary heap. You will also be able to reason about why each operation costs O(log n). You will also recognize the ADT hiding inside classic graph and scheduling algorithms. We also preview alternative implementations. These include Skip Lists adapted for ordered dictionaries. They trade off some guarantees for other advantages, such as easy merging or better cache behavior.
- A priority queue supports insert, extract-min (or extract-max), and often decrease-key, ordered by priority rather than arrival time.
- A binary heap implements all three operations in O(log n) time and O(1) auxiliary space, using an implicit array-based complete binary tree.
- Correctness rests on the heap-order invariant: every node's key is ≤ (min-heap) its children's keys, so the root is always the minimum.
- decrease-key requires tracking each element's position in the array — typically via an auxiliary index map — to support the O(log n) bound needed by Dijkstra and Prim.
- Priority queues drive Dijkstra's algorithm, Prim's MST algorithm, Huffman coding, heapsort, event simulation, and OS/task schedulers.
- Alternatives like pairing heaps, Fibonacci heaps, and Skip Lists-based structures trade simplicity for amortized or merge-friendly performance.
A priority queue is the abstraction "give me the best item fast," and a binary heap realizes it in O(log n) per operation by keeping a complete binary tree balanced implicitly inside an array.
The idea
A priority queue is an abstract data type storing a dynamic collection of elements, each associated with a key (its priority), and supporting at least three operations: `Insert(x)`, which adds element `x`; `ExtractMin()` (or `ExtractMax()`), which removes and returns the element with the smallest (or largest) key; and often `DecreaseKey(x, newKey)`, which lowers `x`'s key in place, re-establishing order. A related read-only operation, `FindMin()`, returns the minimum without removing it. Ties in priority may be broken arbitrarily or by insertion order (making the structure stable), depending on the application. The ADT deliberately says nothing about how these operations are implemented — only their signatures and guarantees. This separation matters because several structures realize the interface with different tradeoffs: sorted arrays give O(1) extract-min but O(n) insert; unsorted arrays give O(1) insert but O(n) extract-min; balanced binary search trees give O(log n) for everything but with larger constants and more memory overhead; and the binary heap gives O(log n) insert and extract-min with excellent constants and O(1) extra space, at the cost of O(n) arbitrary search unless auxiliary bookkeeping is added. This article focuses on the binary heap as the canonical implementation, since it is the one used almost universally in practice for graph algorithms and schedulers. The key structural idea reused from Binary Heaps is the heap-order property: in a min-heap, every node's key is less than or equal to the keys of its children, which forces the global minimum to sit at the root. Combined with the shape property — the tree is complete, filled left to right at every level except possibly the last — this lets the heap be stored implicitly in a plain array with no pointers, using arithmetic to locate parents and children. The priority-queue ADT adds one further requirement beyond the plain heap: to support `DecreaseKey` in O(log n), the structure must be able to locate an arbitrary element's current array position instantly, which is why practical priority-queue implementations pair the heap array with a handle or index map from element identity to array index.
How it works
A binary min-heap stores `n` elements in an array `A[1..n]` (1-indexed for clean arithmetic). For index `i`: ``` parent(i) = floor(i / 2) left(i) = 2i right(i) = 2i + 1 ```
Insert(x). Append `x` at position `n+1` (the next open slot in the complete tree), then repeatedly compare it to its parent and swap upward while it is smaller — this is called sift-up or bubble-up. The process terminates when `x` reaches the root or finds a parent no larger than itself.
ExtractMin(). The minimum is always `A[1]`. Remove it, move the last element `A[n]` into position 1, shrink the array by one, and restore heap order by sift-down (bubble-down): repeatedly swap the node with its smaller child until it is smaller than both children or has none.
DecreaseKey(x, newKey). Given the array index of `x` (found via an index map), lower its key and sift it up — decreasing a key can only violate the heap property with respect to its ancestors, never its descendants, so only sift-up is needed.
IncreaseKey / arbitrary update / Delete(x) follow symmetrically: increasing a key requires sift-down; deleting an arbitrary element is done by decreasing its key to −∞ (conceptually) and extracting it, or directly by replacing it with the last element and sifting in whichever direction restores order. ``` function BUILD-INDEX-MAP(A): // pos[x] = current array index of element x for i in 1..length(A): pos[A[i].id] = i function SWAP(A, pos, i, j): swap(A[i], A[j]) pos[A[i].id] = i pos[A[j].id] = j function SIFT-UP(A, pos, i): while i > 1 and A[parent(i)].key > A[i].key: SWAP(A, pos, i, parent(i)) i = parent(i) function SIFT-DOWN(A, pos, i, n): while true: l = 2i r = 2i + 1 smallest = i if l <= n and A[l].key < A[smallest].key: smallest = l if r <= n and A[r].key < A[smallest].key: smallest = r if smallest == i: break SWAP(A, pos, i, smallest) i = smallest function INSERT(A, pos, n, x): n = n + 1 A[n] = x pos[x.id] = n SIFT-UP(A, pos, n) return n function EXTRACT-MIN(A, pos, n): if n == 0: error "heap is empty" min = A[1] A[1] = A[n] pos[A[1].id] = 1 n = n - 1 if n > 0: SIFT-DOWN(A, pos, 1, n) delete pos[min.id] return min, n function DECREASE-KEY(A, pos, x_id, newKey): i = pos[x_id] if newKey > A[i].key: error "new key is larger than current key" A[i].key = newKey SIFT-UP(A, pos, i) ``` Each `SIFT-UP` or `SIFT-DOWN` call moves an element along a single root-to-leaf path, and the tree's height is `⌊log₂ n⌋` because it is complete — this is the single fact from which every complexity bound below follows.
Complexity & correctness
Time complexity. `Insert`, `ExtractMin`, and `DecreaseKey` each run in `O(log n)` time, and `FindMin` runs in `O(1)` time. Justification. The heap's shape invariant forces the tree to be complete, so with `n` elements its height is exactly `⌊log₂ n⌋`. Both `SIFT-UP` and `SIFT-DOWN` move an element along a single path from a leaf toward the root, or from the root toward a leaf, doing `O(1)` work (one comparison-and-possible-swap) per level. Hence each call performs at most `O(log n)` swaps. `Insert` does one append (`O(1)`) plus one sift-up (`O(log n)`). `ExtractMin` does one removal and one sift-down (`O(log n)`). `DecreaseKey` looks up the position via the index map in `O(1)` (a hash table or array keyed by element id) and then sifts up in `O(log n)`. `FindMin` simply reads `A[1]`.
Space complexity. `O(n)` for the array plus `O(n)` for the index map — both linear, with small constants, and no pointer overhead per node, unlike pointer-based trees.
Correctness — heap-order invariant. Define the invariant H(i): for every node `i` in the array with a parent, `A[parent(i)].key ≤ A[i].key`. We argue each operation preserves `H` for all indices, by induction on the number of operations performed.
- Base case: the empty heap trivially satisfies `H`.
- Insert: appending `x` at position `n+1` may only violate `H` at the single edge `(parent(n+1), n+1)`, since no other parent-child relationship changed. `SIFT-UP` repeatedly checks exactly this edge, and if violated, swaps — which fixes that edge and shifts the potential violation one level up, to `(parent(new position), new position)`. The loop terminates when either the root is reached or the edge holds, at which point every edge in the tree satisfies `H` again.
- ExtractMin: removing `A[1]` and promoting the former last element `A[n]` to the root can only violate `H` at edges from the root downward — every other edge is untouched. `SIFT-DOWN` repeatedly checks the two child edges of the current node, swaps with the smaller violating child if any violation exists, and recurses into that child's position. Choosing the smaller child (not just any child) is essential: swapping with the larger child could leave the other child smaller than the new parent, reintroducing a violation. The loop terminates at a leaf or when both child edges hold.
- DecreaseKey: lowering `A[i].key` can only violate `H` on the edge `(parent(i), i)`, since children's keys are unaffected and still ≥ the (now smaller) `A[i].key`. `SIFT-UP` handles this exactly as in `Insert`.
Since the root always satisfies `H` transitively down every path (the invariant composed along a root-to-node path implies the root is ≤ every descendant), and `H` holds for all edges after each operation, `A[1]` is always the global minimum — proving `ExtractMin` is correct.
Tradeoffs. The array-heap gives excellent constants and cache locality (it is contiguous memory, unlike pointer-based trees), but plain heaps do not support `DecreaseKey` in O(log n) unless the caller can supply the element's current index — hence the index map is not optional for graph algorithms, it is required infrastructure. Searching for an arbitrary key (not via the map) costs `O(n)`, since the heap gives no ordering information beyond parent-child. Merging two heaps of size `n` and `m` costs `O(n + m)` for binary heaps (rebuild), whereas mergeable heaps (binomial heaps, Fibonacci heaps, pairing heaps) support merge in `O(log n)` or better — a reason those structures exist despite higher constants. Fibonacci heaps achieve amortized `O(1)` `Insert` and `DecreaseKey` with amortized `O(log n)` `ExtractMin`, improving Dijkstra's asymptotic bound on dense graphs, but their large constants and implementation complexity mean binary heaps (or 4-ary heaps, or pairing heaps) usually win in practice.
Worked example
Trace Dijkstra's algorithm on a small weighted graph, exercising `Insert`, `ExtractMin`, and `DecreaseKey` together — the operation triple that defines the priority-queue ADT in this article's scope. Graph: vertices `S, A, B, C, D`. Edges: `S–A (4)`, `S–B (1)`, `B–A (2)`, `B–C (5)`, `A–C (1)`, `C–D (3)`, `B–D (8)`. Initialize `dist[S]=0`, all others `∞`, and insert all five vertices into a min-heap keyed by `dist`.
| Step | Heap contents (key:vertex) | Action | Effect |
|---|---|---|---|
| 0 | S:0, A:∞, B:∞, C:∞, D:∞ | — | initial state |
| 1 | ExtractMin → S:0 | relax S's edges | DecreaseKey(A,4), DecreaseKey(B,1) |
| 2 | A:4, B:1, C:∞, D:∞ | ExtractMin → B:1 | relax B's edges |
| 3 | DecreaseKey(A,3) [1+2 < 4] | DecreaseKey(C,6) [1+5 < ∞] | |
| 4 | A:3, C:6, D:∞ | ExtractMin → A:3 | relax A's edges |
| 5 | DecreaseKey(C,4) [3+1 < 6] | D untouched (no edge A–D) | |
| 6 | C:4, D:∞ | ExtractMin → C:4 | relax C's edges |
| 7 | DecreaseKey(D,7) [4+3 < ∞] | ||
| 8 | D:7 | ExtractMin → D:7 | heap empty, done |
Final distances: `S:0, B:1, A:3, C:4, D:7`. Each `DecreaseKey` call needed `O(1)` lookup of the vertex's array index (via the index map) followed by an `O(log n)` sift-up — for `n = 5` this is at most 3 swaps, matching `⌊log₂5⌋ = 2` levels of possible movement. Across the whole run, the algorithm performs `n` `ExtractMin` calls and up to `m` `DecreaseKey` calls (`m` = number of edges), giving the classic bound `O((n + m) log n)` for heap-based Dijkstra — exactly the complexity contributed by the priority queue, since graph traversal itself is `O(n + m)`.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Swapping with the larger child during sift-down | Breaks the invariant on the untouched sibling edge | Always compare both children and swap with the smaller |
| Off-by-one in `parent`/`child` index arithmetic | Mixing 0-indexed and 1-indexed formulas | Fix one convention; for 0-indexed arrays use `parent(i)=(i-1)/2`, `left(i)=2i+1`, `right(i)=2i+2` |
| `DecreaseKey` without an index map | Heap array alone gives no O(1) way to find an element | Maintain a hash map or array from element id to current heap index, updated on every swap |
| Calling `DecreaseKey` with a larger key | Silently corrupts heap order if unchecked | Validate `newKey ≤ current key`; use a separate `IncreaseKey`/`Update` path otherwise |
| Forgetting to update `n` and shrink array on extract | Leaves stale/duplicate entries reachable | Always decrement size and clear the vacated last slot |
| Using a max-heap where the problem wants nearest/soonest | Confusing "priority = importance" with "priority = numeric key direction" | Be explicit: min-heap for shortest distance/earliest deadline, max-heap for highest value/latest deadline |
Pioneers & history
The priority queue's practical form crystallized with the invention of the binary heap for heapsort, but the underlying idea — service by priority rather than arrival order — predates the data structure by decades in operations research and scheduling theory.
Origins · 1 minJohn William Joseph Williams and how this was discovered
Williams introduced the binary heap and heapsort in a 1964 Communications of the ACM paper, giving the first array-based, pointer-free implementation of a priority queue with logarithmic insert and extract-min. Robert W. Floyd shortly after gave an improved O(n) heap-construction algorithm (bottom-up heapify), the "Build-Heap" procedure now standard in every treatment of the topic.
Where this matters
Operating system schedulers use priority queues to select the next process or thread to run, often keyed by dynamic priority that ages waiting jobs to prevent starvation. Network routers and quality-of-service systems queue packets by priority class. Discrete-event simulators — from network simulators to physics engines — use a priority queue keyed by event time to always process the next chronological event, an application sometimes called an "event calendar." Huffman coding builds an optimal prefix code by repeatedly extracting the two lowest-frequency nodes from a priority queue and merging them, directly using `ExtractMin` and `Insert`. Dijkstra's shortest-path and Prim's minimum-spanning-tree algorithms are the textbook demonstrations of `DecreaseKey` in action, and both are foundational to routing protocols (OSPF-style link-state routing) and network design tools. Many language standard libraries expose a priority queue directly — C++'s `std::priority_queue`, Java's `PriorityQueue`, Python's `heapq` module — each backed by a binary heap essentially identical to the one described here. Understanding this ADT is also the natural bridge to more advanced ordered structures such as Skip Lists, which support similar ordered-access patterns with probabilistic balancing instead of an implicit tree, and to mergeable heap variants used in advanced graph algorithms.
Ask the tutor: How would you modify the binary-heap priority queue to support efficient merge of two priority queues, and what structure would you switch to for that?
What is the difference between a priority queue and a heap?
A priority queue is an abstract data type — an interface specifying operations like `Insert`, `ExtractMin`, and `DecreaseKey` — while a heap is one concrete data structure that implements that interface efficiently. Other structures, such as sorted arrays, balanced binary search trees, binomial heaps, or Fibonacci heaps, can also implement the priority-queue ADT, each with different time tradeoffs. Conflating the two is common because binary heaps are by far the most widely used implementation, but the distinction matters when choosing a structure: if you need fast merging of two priority queues, a plain binary heap is a poor fit even though it satisfies the same ADT, and a binomial or Fibonacci heap would serve better.
Why is DecreaseKey harder to support than Insert or ExtractMin?
`Insert` and `ExtractMin` only ever touch the root or the newest/last element, so the array position involved is always known in advance. `DecreaseKey`, however, must operate on an arbitrary existing element, whose current array index is not implicitly known — the heap array by itself offers no fast way to find "where is element x right now," since heap order is not a total order and elements move during other operations. Supporting `DecreaseKey` in O(log n) therefore requires an auxiliary index map (or a handle returned by `Insert`) that is kept synchronized with every swap performed during any sift operation.
Can a priority queue be implemented without a heap?
Yes. A sorted array or doubly linked list gives O(1) `ExtractMin` (just take the front) but O(n) `Insert` (must find the sorted position). An unsorted array or linked list gives O(1) `Insert` (append anywhere) but O(n) `ExtractMin` (must scan for the minimum). A balanced binary search tree gives O(log n) for all operations, including `DecreaseKey` and even arbitrary search, at the cost of larger constants and pointer overhead compared to an implicit array-based heap. The binary heap is preferred in practice because it achieves O(log n) insert/extract with the smallest constants and no pointer memory overhead.
How does heap-based Dijkstra's complexity compare to using a Fibonacci heap?
With a binary heap, Dijkstra's algorithm costs O((n + m) log n), since each of the n `ExtractMin` calls and each of up to m `DecreaseKey` calls costs O(log n). A Fibonacci heap achieves amortized O(log n) `ExtractMin` but amortized O(1) `DecreaseKey`, giving Dijkstra's algorithm a total of O(m + n log n) — asymptotically better on dense graphs where m is close to n². In practice, however, Fibonacci heaps carry large constant factors and complex bookkeeping (circular doubly linked child lists, lazy consolidation), so binary heaps or simpler pairing heaps often outperform them for typical graph sizes despite the worse asymptotic bound.
What happens if you call ExtractMin on an empty priority queue?
This is an edge case that a correct implementation must handle explicitly rather than silently. The array has no element at position 1, so attempting to read or swap invokes undefined behavior — an out-of-bounds access, a garbage value, or a crash, depending on the language. A robust implementation checks the size counter before touching the array and raises an explicit error, returns a sentinel (such as `null` or a special "empty" flag), or requires the caller to check `IsEmpty()` first, exactly parallel to how a stack or queue must guard against popping an empty container.
Why is 1-indexing common in heap implementations?
With 1-indexed arrays, the parent/child arithmetic simplifies to `parent(i) = i/2` (integer division), `left(i) = 2i`, `right(i) = 2i+1`, which is slightly cleaner than the 0-indexed formulas `parent(i) = (i-1)/2`, `left(i) = 2i+1`, `right(i) = 2i+2`. Both are equally correct and equally O(log n); the choice is a matter of implementation convenience and historical convention (many original heap papers and CLRS itself use 1-indexing), not a difference in asymptotic behavior. Real-world library implementations often use 0-indexing to match native array conventions in languages like C, Java, and Python.
Does a priority queue guarantee stability, i.e., FIFO order among equal-priority elements?
Not by default. A plain binary heap makes no promise about the relative order in which equal-priority elements are extracted, since sift-up and sift-down only compare keys, and swaps can reorder equal elements unpredictably as the tree reshapes. If stable tie-breaking is required — for example, "serve equal-priority jobs in arrival order" — the standard technique is to augment each key with a secondary tiebreaker, such as an insertion sequence number, and compare pairs `(priority, sequence)` lexicographically, which restores a strict total order and deterministic behavior without changing the O(log n) bounds.