I think of a priority queue as one of the most quietly essential data structures in computer science — it rarely gets the spotlight the way sorting algorithms do, but it sits underneath an enormous number of systems I interact with daily: task schedulers, pathfinding algorithms, event simulators, and even the Heap Sort algorithm I cover elsewhere in this series.
What makes priority queues interesting to me is the specific promise they make: instead of retrieving elements in the order I inserted them (like a normal queue) or in reverse order (like a stack), a priority queue always gives me back the element with the highest (or lowest) priority, regardless of insertion order. This article walks through how I build one from scratch using a binary heap.
History and Background
The concept of prioritized processing predates formal computer science, but the specific data structure I call a “priority queue,” implemented efficiently via a binary heap, traces back to J.W.J. Williams’ 1964 paper introducing Heapsort, where the binary heap itself was first formally described as an array-based structure.
The priority queue abstraction as a distinct concept — separate from the heap that implements it — became standard in algorithms literature through the 1970s, as computer scientists formalized abstract data types (ADTs) as a way of separating an interface (what operations are supported) from an implementation (how those operations are carried out). I find this separation valuable: a priority queue can be implemented with a binary heap, a Fibonacci heap, a pairing heap, or even a simple sorted list — each with different performance trade-offs — while still satisfying the same abstract contract.
Problem Statement
The problem I’m solving is this: I need a data structure that lets me repeatedly insert elements, each carrying some priority value, and efficiently retrieve (and remove) the element with the highest priority at any time — without having to re-sort the entire collection after every insertion or removal.
A naive approach — keeping a sorted array and always removing from the front — makes retrieval fast ($O(1)$) but insertion slow ($O(n)$, since I’d need to shift elements to maintain sorted order). A priority queue backed by a binary heap gives me a better balance: both insertion and extraction run in $O(\log n)$.
Core Concepts
- Priority: A value associated with each element determining its importance; “highest priority” might mean largest value (max-priority queue) or smallest value (min-priority queue), depending on the convention I choose.
- Binary heap: A complete binary tree, usually represented as an array, satisfying the heap property (in a max-heap, every parent is $\geq$ its children).
- Heap property: The invariant that defines a valid heap — for a max-heap,
A[parent(i)] >= A[i]for every nodeiother than the root. - Heapify-up (sift-up): The operation used after insertion to restore the heap property by moving a newly inserted element upward until it’s no longer larger than its parent.
- Heapify-down (sift-down): The operation used after removing the root to restore the heap property by moving the replacement element downward until both heap-property conditions are satisfied at its new position.
- Array-based tree indexing: For a node at index
i(0-indexed), its parent is at(i-1)/2, its left child is at2i+1, and its right child is at2i+2.
How It Works
For my max-priority queue, here’s the sequence I follow for each supported operation:
Insertion:
- Add the new element at the end of the underlying array (the next available leaf position in the tree).
- Compare the new element to its parent; if it’s larger, swap them.
- Repeat step 2, moving upward, until the element is no longer larger than its parent, or it reaches the root.
Extract-max:
- Save the root element (the maximum) to return later.
- Move the last element in the array to the root position.
- Reduce the heap size by one (effectively removing the last, now-duplicated, slot).
- Compare the new root to its children; swap with the larger child if the root is smaller.
- Repeat step 4, moving downward, until the heap property is restored.
- Return the saved maximum value.
Peek-max: Simply return the root element without modifying the structure — this is $O(1)$ since the maximum is always at index 0 in a max-heap.
Working Principle
The internal logic of a priority queue backed by a binary heap relies on two things working together: the shape property (the tree is always complete, meaning every level is fully filled except possibly the last, which fills left to right) and the heap property (parents dominate children in priority).
The shape property is what lets me represent the tree compactly as an array with simple index arithmetic instead of needing explicit pointer-based tree nodes. The heap property is what guarantees the root always holds the maximum (or minimum) element. Every operation I perform — insertion or extraction — is really just a local repair operation: I make one small change (add a leaf, or replace the root) that might locally violate the heap property, and then I “bubble” that violation up or down the tree until the property is restored everywhere.
Mathematical Foundation
The height of a binary heap containing $n$ elements is crucial to the performance analysis, since it’s a complete binary tree. The height $h$ satisfies:
$$ h = \lfloor \log_2 n \rfloor $$
This follows because a complete binary tree of height $h$ has between $2^h$ and $2^{h+1} – 1$ nodes.
Since both heapifyUp and heapifyDown move an element along a single root-to-leaf path (or leaf-to-root path), the number of comparisons/swaps each performs is bounded by the height:
$$ T_{\text{insert}}(n) = O(\log n), \qquad T_{\text{extractMax}}(n) = O(\log n) $$
For peekMax, since the maximum always resides at index 0:
$$ T_{\text{peekMax}}(n) = O(1) $$
If I insert $n$ elements one at a time into an initially empty heap, the total cost is bounded by:
$$ \sum_{i=1}^{n} O(\log i) = O(n \log n) $$
This is the same asymptotic bound I’d get by building a heap element-by-element via insertion — worth contrasting with the tighter $O(n)$ bound achievable via the bottom-up buildMaxHeap procedure I describe in a separate article on building a heap, which processes all elements at once rather than inserting them individually.
Diagrams
Here is the overall operational flow of a priority queue:
flowchart TD
A[Priority Queue] --> B[Insert element]
B --> C[Place at end of array]
C --> D[Heapify-Up: swap with parent while larger]
D --> E[Heap property restored]
A --> F[Extract-Max]
F --> G[Save root value]
G --> H[Move last element to root]
H --> I[Heapify-Down: swap with larger child while smaller]
I --> J[Return saved max, heap property restored]And here is a diagram showing the array-to-tree mapping that makes this all possible:
flowchart LR
A["Array index i"] --> B["Parent: (i-1)/2"]
A --> C["Left child: 2i+1"]
A --> D["Right child: 2i+2"]
B --> E["Enables tree navigation without pointers"]
C --> E
D --> EPseudocode
INSERT(PQ, value)
PQ.data[PQ.size] = value
HEAPIFY-UP(PQ, PQ.size)
PQ.size = PQ.size + 1
HEAPIFY-UP(PQ, i)
while i > 0 and PQ.data[PARENT(i)] < PQ.data[i]
swap PQ.data[PARENT(i)] and PQ.data[i]
i = PARENT(i)
EXTRACT-MAX(PQ)
if PQ.size == 0
error "priority queue is empty"
max = PQ.data[0]
PQ.data[0] = PQ.data[PQ.size - 1]
PQ.size = PQ.size - 1
HEAPIFY-DOWN(PQ, 0)
return max
HEAPIFY-DOWN(PQ, i)
largest = i
left = LEFT-CHILD(i)
right = RIGHT-CHILD(i)
if left < PQ.size and PQ.data[left] > PQ.data[largest]
largest = left
if right < PQ.size and PQ.data[right] > PQ.data[largest]
largest = right
if largest != i
swap PQ.data[i] and PQ.data[largest]
HEAPIFY-DOWN(PQ, largest)
PEEK-MAX(PQ)
return PQ.data[0]
Step-by-Step Example
Let me walk through inserting the values 5, 15, 10, 20, 3 into an empty max-priority queue, one at a time.
Insert 5: Heap: $[5]$. Single element, nothing to sift up.
Insert 15: Heap: $[5, 15]$. Compare 15 (index 1) with parent 5 (index 0): $15 > 5$, swap. Heap becomes $[15, 5]$.
Insert 10: Heap: $[15, 5, 10]$. Compare 10 (index 2) with parent 15 (index 0): $10 < 15$, no swap needed. Heap stays $[15, 5, 10]$.
Insert 20: Heap: $[15, 5, 10, 20]$. Compare 20 (index 3) with parent 5 (index 1): $20 > 5$, swap → $[15, 20, 10, 5]$. Continue: compare 20 (now index 1) with parent 15 (index 0): $20 > 15$, swap → $[20, 15, 10, 5]$.
Insert 3: Heap: $[20, 15, 10, 5, 3]$. Compare 3 (index 4) with parent 15 (index 1): $3 < 15$, no swap needed.
Final heap array: $[20, 15, 10, 5, 3]$.
Extracting all elements (extract-max repeatedly):
This matches my tested C program exactly, which reported: Max element: 20 and extraction order 20 15 10 5 3.
Time Complexity
- Insert: $O(\log n)$ in all cases (best, average, worst), since it always traces a single path from a leaf toward the root.
- Extract-Max: $O(\log n)$ in all cases, tracing a single path from the root toward a leaf.
- Peek-Max: $O(1)$ in all cases, since the maximum is always at a fixed, known index.
- Building from $n$ elements one at a time (via repeated insert): $O(n \log n)$ total.
Space Complexity
A binary heap-based priority queue requires:
$$ O(n) $$
space to store $n$ elements, with no auxiliary structure needed beyond the array itself (unlike Merge Sort, which needs a separate output array). This is one of the properties I appreciate most about heap-based priority queues — they’re compact.
Correctness Analysis
I verify correctness by showing that both HEAPIFY-UP and HEAPIFY-DOWN preserve the heap property as a loop invariant.
Insertion correctness: Before insertion, I assume the heap property holds for all $n$ existing elements. Adding a new element at the next available leaf position may only violate the heap property along the single path from that leaf to the root (since every other parent-child relationship is untouched). HEAPIFY-UP walks exactly that path, swapping wherever the property is violated, and stops as soon as it finds a parent that is $\geq$ the current element (or reaches the root) — at which point the property holds everywhere.
Extraction correctness: Removing the root and replacing it with the last element may only violate the heap property along some path downward from the root (since all other subtrees remain valid heaps, by the inductive assumption that the heap was valid before extraction). HEAPIFY-DOWN walks down, always choosing to swap with the larger child (ensuring I don’t accidentally break the property in the subtree I move into), and stops once the current node is $\geq$ both its children or it reaches a leaf.
Since both operations only ever touch a single root-to-leaf path and always terminate in a heap-property-satisfying configuration, the structure remains a valid heap after every operation, by induction on the number of operations performed.
Advantages
- Efficient $O(\log n)$ insertion and extraction, much better than a naive sorted-array approach for insertion.
- Compact array-based representation with no pointer overhead.
- Simple to implement correctly relative to more complex priority queue variants like Fibonacci heaps.
- Forms the basis for other important algorithms, including Heap Sort and graph algorithms like Dijkstra’s shortest path and Prim’s minimum spanning tree.
Disadvantages
- $O(n)$ search for an arbitrary (non-maximum) element, since a heap only guarantees ordering along root-to-leaf paths, not full sorted order.
- Not stable in the sense of preserving insertion order among equal-priority elements, unless I add extra bookkeeping (like a secondary insertion-order key).
- Fixed-size array implementations require resizing logic (or knowing capacity in advance) to handle growth, unless using a dynamic array.
- More advanced variants (Fibonacci heaps, pairing heaps) offer better amortized performance for certain operations like
decrease-key, which a plain binary heap doesn’t support efficiently without additional index tracking.
Applications
- Task scheduling in operating systems, where the highest-priority process should always run next.
- Dijkstra’s shortest path algorithm and Prim’s minimum spanning tree algorithm, both of which repeatedly extract the minimum-distance/weight vertex.
- Event-driven simulations, where events need to be processed in chronological (or priority) order.
- Huffman coding, which repeatedly merges the two lowest-frequency nodes using a min-priority queue.
- The A* pathfinding algorithm, which relies on a priority queue to always expand the most promising node next.
Implementation in C
Here is my tested max-priority queue implementation, backed by a binary heap stored in a dynamically allocated array:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
int size;
int capacity;
} PriorityQueue;
PriorityQueue* createPQ(int capacity) {
PriorityQueue *pq = malloc(sizeof(PriorityQueue));
pq->data = malloc(capacity * sizeof(int));
pq->size = 0;
pq->capacity = capacity;
return pq;
}
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int parent(int i) { return (i - 1) / 2; }
int leftChild(int i) { return 2 * i + 1; }
int rightChild(int i) { return 2 * i + 2; }
// Restores the heap property by moving element at index i upward
void heapifyUp(PriorityQueue *pq, int i) {
while (i > 0 && pq->data[parent(i)] < pq->data[i]) {
swap(&pq->data[parent(i)], &pq->data[i]);
i = parent(i);
}
}
// Restores the heap property by moving element at index i downward
void heapifyDown(PriorityQueue *pq, int i) {
int largest = i;
int left = leftChild(i);
int right = rightChild(i);
if (left < pq->size && pq->data[left] > pq->data[largest])
largest = left;
if (right < pq->size && pq->data[right] > pq->data[largest])
largest = right;
if (largest != i) {
swap(&pq->data[i], &pq->data[largest]);
heapifyDown(pq, largest);
}
}
void insert(PriorityQueue *pq, int value) {
if (pq->size == pq->capacity) {
printf("Priority queue is full\n");
return;
}
pq->data[pq->size] = value;
heapifyUp(pq, pq->size);
pq->size++;
}
int extractMax(PriorityQueue *pq) {
if (pq->size == 0) {
printf("Priority queue is empty\n");
return -1;
}
int max = pq->data[0];
pq->data[0] = pq->data[pq->size - 1];
pq->size--;
heapifyDown(pq, 0);
return max;
}
int peekMax(PriorityQueue *pq) {
return pq->data[0];
}
int main() {
PriorityQueue *pq = createPQ(10);
insert(pq, 5);
insert(pq, 15);
insert(pq, 10);
insert(pq, 20);
insert(pq, 3);
printf("Max element: %d\n", peekMax(pq));
printf("Extracting in priority order: ");
while (pq->size > 0) {
printf("%d ", extractMax(pq));
}
printf("\n");
free(pq->data);
free(pq);
return 0;
}
Sample Input and Output
Input: Insertions of 5, 15, 10, 20, 3 into an empty priority queue, followed by repeated extraction.
Output (verified by compiling and running the code above):
Max element: 20
Extracting in priority order: 20 15 10 5 3
Optimization Techniques
- Dynamic resizing: Instead of a fixed capacity, doubling the underlying array when full (similar to a dynamic array/vector) amortizes the resizing cost to $O(1)$ per insertion on average.
- Index tracking for decrease-key: Maintaining a hash map from element identity to its current array index allows efficient
decrease-keyoperations, which are essential for algorithms like Dijkstra’s shortest path. - d-ary heaps: Using more than two children per node (a “d-ary heap”) can reduce the height of the tree, trading off a slightly more expensive
heapifyDown(more children to compare) for a cheaperheapifyUp. - Batch construction: If I know all elements in advance, building the heap using the bottom-up $O(n)$
buildMaxHeapprocedure is significantly faster than inserting elements one at a time ($O(n \log n)$). - Lazy deletion: In some applications, marking elements as deleted rather than immediately removing them (and skipping them during extraction) can reduce the overhead of frequent removals.
Common Mistakes
- Confusing parent/child index formulas, especially forgetting that these formulas assume 0-indexed arrays; using 1-indexed formulas on a 0-indexed array silently corrupts the heap.
- Forgetting to decrement size before heapifying down during extraction, which can cause the algorithm to compare against a stale or duplicated last element.
- Not checking for empty or full queue conditions, leading to undefined behavior or crashes when inserting into a full queue or extracting from an empty one.
- Assuming a heap is fully sorted. A common misconception is thinking any traversal of the heap array yields sorted order — only the root is guaranteed to be the maximum; the rest of the array only satisfies the local parent-child heap property.
- Mixing up max-heap and min-heap comparison directions, especially when adapting existing code from one convention to the other without updating every comparison operator.
Further Reading
- Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (CLRS), MIT Press — https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- J.W.J. Williams, “Algorithm 232 – Heapsort”, Communications of the ACM, 1964 — https://dl.acm.org/doi/10.1145/512274.512284
- Wikipedia, “Priority queue” — https://en.wikipedia.org/wiki/Priority_queue
- Wikipedia, “Binary heap” — https://en.wikipedia.org/wiki/Binary_heap
- Visualgo, Heap Visualization — https://visualgo.net/en/heap
