Priority queue
A priority queue is an abstract data type that stores a collection of elements, each associated with a key representing its priority under a total order, allowing elements to be inserted and removed based on priority rather than insertion order.[1][2] Unlike standard queues, which process elements in FIFO order, priority queues deprioritize lower-key elements to ensure the highest (or lowest, depending on convention) priority item is always accessible first.[3] This structure is fundamental in algorithm design for tasks requiring selective processing, such as event simulation or resource allocation.[2]
The core operations of a priority queue include insertion of a new element with its key, and removal of the element with the maximum (or minimum) key, both typically achieving logarithmic time complexity in efficient implementations.[2][3] Additional operations often supported are peeking at the maximum/minimum without removal, checking emptiness, and querying the size, all while maintaining the total order property where keys are comparable, reflexive, antisymmetric, and transitive.[1] These operations enable dynamic management of priorities, with some variants allowing priority modifications, though this requires knowing the element's position for efficiency.[3]
Priority queues can be implemented using various underlying structures, balancing trade-offs in time complexity for insertions and removals.[1] Simple array-based approaches, such as unsorted lists (O(1) insert, O(n) remove-max) or sorted lists (O(n) insert, O(1) remove-max), provide basic functionality but scale poorly.[2] More efficient realizations employ binary heaps, complete binary trees satisfying the heap-order property (parent key greater than or equal to children in max-heaps), which support both insert and remove-max in O(log n) time via up-heap and down-heap bubbling.[3][1] Balanced search trees offer similar logarithmic performance with added flexibility for other queries.[1]
In practice, priority queues underpin algorithms like heapsort, which achieves O(n log n) sorting by repeatedly removing the maximum from a heap, and are widely used in applications such as Dijkstra's shortest-path algorithm, job scheduling, and Huffman coding for data compression.[3][1] Their ability to handle dynamic priorities efficiently makes them essential in operating systems, networking, and real-time systems.[2]
Fundamentals
Definition and motivation
A priority queue is an abstract data type similar to a regular queue or stack, but it stores a collection of elements where each is associated with a priority value, and elements are dequeued based on priority order rather than first-in, first-out (FIFO) insertion sequence.[2][1] This structure ensures that the element with the highest (or lowest, depending on convention) priority is always accessible for removal, providing selective access to the most relevant item in the collection.[2]
The motivation for priority queues arises in scenarios requiring urgent or importance-based processing over strict temporal order, such as task scheduling in operating systems, where higher-priority processes are executed before lower ones to optimize resource allocation.[4] They are also essential in event-driven simulations, where events like arrivals or state changes are processed in priority order, often timestamped, to model real-world systems accurately.[5] A practical example is medical triage in emergency rooms, where patients are prioritized by condition severity—such as treating critical cases immediately—over arrival time to maximize life-saving outcomes.[6]
Priority queues support dynamic insertion and removal of elements while maintaining priority ordering, with priorities typically represented as numeric keys or via custom comparators for more complex types; ties in priority are often resolved using secondary criteria, like insertion timestamps, to ensure deterministic behavior.[2][7] The concept was formalized in 1964 by J. W. J. Williams, who introduced it in the context of heap structures for selection and sorting tasks.
Comparison to queues and stacks
A standard queue is an abstract data type (ADT) that adheres to the First-In-First-Out (FIFO) principle, where elements are inserted at the rear (enqueue) and removed from the front (dequeue), preserving the order of insertion.[8] In contrast, a priority queue dequeues the element with the highest (or lowest, depending on convention) priority regardless of its insertion position, allowing non-sequential access based on associated priority keys.[2] This enables priority queues to handle scenarios where urgency or importance dictates processing order, such as in task scheduling systems.[9]
A stack, another fundamental ADT, follows the Last-In-First-Out (LIFO) principle, with insertions (push) and removals (pop) occurring exclusively at the top, effectively reversing the order of operations relative to insertion.[8] Priority queues differ fundamentally by ignoring insertion order entirely and instead extracting based on priority, which may require maintaining a partial order across all elements rather than endpoint access.[10] Stacks are commonly used for recursion or undo mechanisms in applications like expression evaluation, where the most recent item must be processed next.[8]
Key differences between these structures lie in their access patterns and use cases: queues and stacks permit only endpoint operations for efficient O(1) time complexity, enforcing strict FIFO or LIFO sequencing suitable for order-preserving tasks like breadth-first search or function call management.[2] Priority queues, however, support priority-based extraction, often at O(log n) cost, ideal for urgency-driven processing in simulations or operating system schedulers where elements must be dynamically reordered.[9] From an ADT perspective, all three are collections of elements, but priority queues uniquely require a total order on priorities (via comparable keys) to determine extraction, distinguishing them from the positional ordering in queues and stacks.[8]
To illustrate these contrasts, consider simple pseudocode for core operations:
Queue operations:
enqueue(Q, x): // Add to rear
[append](/page/Append) x to Q
dequeue(Q): // Remove from front
return remove front of Q
enqueue(Q, x): // Add to rear
[append](/page/Append) x to Q
dequeue(Q): // Remove from front
return remove front of Q
[2]
Stack operations:
push(S, x): // Add to top
[append](/page/Append) x to S
pop(S): // Remove from top
return remove end of S
push(S, x): // Add to top
[append](/page/Append) x to S
pop(S): // Remove from top
return remove end of S
[8]
Priority queue operations:
insert(PQ, x): // Add with [priority](/page/Priority) key
add x to PQ and maintain [priority](/page/Priority) [order](/page/Order)
extract-max(PQ): // Remove highest priority
return and remove max-key element from PQ
insert(PQ, x): // Add with [priority](/page/Priority) key
add x to PQ and maintain [priority](/page/Priority) [order](/page/Order)
extract-max(PQ): // Remove highest priority
return and remove max-key element from PQ
[10]
Core Operations
Insertion and extraction
The insertion operation in a priority queue adds a new element paired with its priority value to the data structure, preserving the overall priority ordering without immediately altering the positions of existing elements. In a max-priority queue, higher numerical priority values denote greater urgency or importance, ensuring that elements with larger priorities are positioned to be serviced ahead of those with smaller ones.[2][11]
The extract-max operation removes and returns the element possessing the highest priority from the queue, effectively dequeuing the most urgent item while maintaining the integrity of the remaining structure. For an empty queue, this operation commonly raises an exception to indicate the error or returns a sentinel value like null to signal the absence of elements.[2][12] A high-level pseudocode representation of extract-max, at the abstract data type level, is as follows:
function extractMax(PQ):
if PQ is empty:
return [null](/page/Null) // or [raise](/page/Raise!) an exception
else:
maxElement = the element with highest [priority](/page/Priority)
remove maxElement from PQ
return maxElement
function extractMax(PQ):
if PQ is empty:
return [null](/page/Null) // or [raise](/page/Raise!) an exception
else:
maxElement = the element with highest [priority](/page/Priority)
remove maxElement from PQ
return maxElement
[13][14]
Max-priority queues, which assume priorities escalate toward the maximum value, find frequent application in graph algorithms such as Dijkstra's shortest path method, where selecting the node with the highest tentative priority (often adjusted via distances) drives the relaxation process.[15] For instance, in task scheduling systems, insertion might involve adding jobs with priority scores reflecting urgency levels (e.g., 10 for critical, 5 for routine), while extraction retrieves the job with the maximum score, such as processing a high-priority deadline task before others.[2] A min-priority queue serves as a symmetric variant, inverting the priority ordering to favor minimum values.[11]
Priority updates and peeking
In priority queues, the peek operation provides a non-destructive way to access the element with the highest priority without removing it from the structure. This is particularly useful for inspecting the current top priority before deciding on further actions, such as in event simulation where the next event needs to be examined.[16] In typical implementations using heaps, the peek function simply returns the root element if the queue is not empty, assuming the root holds the extremal priority.
The following pseudocode illustrates a basic peek implementation in an abstract priority queue:
function peek():
if queue is empty:
return null // or raise an [error](/page/Error)
return root // the highest-[priority](/page/Priority) [element](/page/Element)
function peek():
if queue is empty:
return null // or raise an [error](/page/Error)
return root // the highest-[priority](/page/Priority) [element](/page/Element)
Priority update operations, such as decrease-key and increase-key, allow modification of an element's priority after insertion, ensuring the queue's ordering is restored through adjustments like bubbling up or down in the structure. These updates are essential for scenarios where priorities evolve dynamically, avoiding the inefficiency of deletion and re-insertion. In a min-heap-based queue, decrease-key lowers the key value (thereby raising the priority) and percolates the element upward, while increase-key raises the key and percolates downward.[17] The symmetric increase-key operation serves a similar role in max-priority contexts, though decrease-key is more commonly emphasized in min-priority settings.[18]
A min-priority queue inverts the standard max-priority model by assigning higher priority to elements with smaller key values, where the smallest key represents the top priority. This variant supports peek via a minimum operation that returns the smallest-key element without removal, and extract-min that removes and returns it. Min-priority queues are widely used in shortest-path algorithms, such as Dijkstra's, where node distances start high and are progressively decreased to reflect better paths found.[19]
Certain advanced priority queue implementations also support a union or merge operation to combine two disjoint queues into a single one that preserves the overall priority ordering. This facilitates efficient aggregation of priority sets in divide-and-conquer algorithms.
As an example, consider a task management system where tasks are queued by urgency level (lower number indicating higher urgency in a min-priority queue). If midway through processing, new information elevates a task's urgency—say, reducing its key from 5 to 2—a decrease-key operation updates its position, bubbling it toward the front without disrupting other elements or requiring full re-queueing.[17] This maintains responsiveness in real-time scheduling while leveraging the queue's structure for quick adjustments.
Implementations
Unsorted and sorted lists
One of the simplest ways to implement a priority queue is using an unsorted list, where elements are stored in arbitrary order without maintaining any specific sequence based on priorities.[20] In this approach, insertion is efficient as new elements can be appended to the end of the list in constant time, O(1), regardless of the current size.[21] However, extraction of the maximum (or minimum) priority element requires scanning the entire list to identify the highest-priority item, followed by its removal, resulting in O(n time complexity in the worst case, where n is the number of elements.[20] This makes the unsorted list suitable for scenarios with a high ratio of insertions to extractions, such as when building the queue with many additions before any removals.[22]
An alternative basic implementation uses a sorted list, where elements are maintained in non-decreasing (or non-increasing) order of priority.[20] Insertion involves first locating the appropriate position using binary search, which takes O(log n) time, but then shifting existing elements to accommodate the new one, which requires O(n) time due to the linear relocation in a list structure.[23] Extraction of the maximum priority element is then straightforward, as it is always at one end of the list, allowing removal in O(1) time.[20] This variant performs better in situations dominated by extractions after initial insertions, though the overall worst-case time for key operations remains O(n).[21]
Both unsorted and sorted list implementations offer trade-offs in efficiency: the unsorted list prioritizes fast insertions at the expense of slower extractions, while the sorted list favors quick extractions but slows insertions.[20] Despite their simplicity, these approaches are generally inefficient for large-scale applications due to the linear factors in their primary operations, though they provide a foundational understanding before exploring more balanced structures like heaps.[22]
To illustrate the extract-max operation in an unsorted list, the following pseudocode demonstrates iterating through the list to find and remove the highest-priority element:
function extractMax(priorityQueue):
if priorityQueue is empty:
return [null](/page/Null)
maxIndex = 0
maxPriority = priorityQueue[0].priority
for i from 1 to length(priorityQueue) - 1:
if priorityQueue[i].priority > maxPriority:
maxPriority = priorityQueue[i].priority
maxIndex = i
maxElement = priorityQueue[maxIndex]
remove priorityQueue at maxIndex // O(n) shift in array-based list
return maxElement
function extractMax(priorityQueue):
if priorityQueue is empty:
return [null](/page/Null)
maxIndex = 0
maxPriority = priorityQueue[0].priority
for i from 1 to length(priorityQueue) - 1:
if priorityQueue[i].priority > maxPriority:
maxPriority = priorityQueue[i].priority
maxIndex = i
maxElement = priorityQueue[maxIndex]
remove priorityQueue at maxIndex // O(n) shift in array-based list
return maxElement
[1]
These list-based methods are primarily used in educational contexts to introduce priority queue concepts or in applications with small n, where the overhead of more complex data structures is unnecessary.[24] For larger datasets, they have been largely superseded by heap-based implementations that achieve logarithmic performance for both insertions and extractions.[2]
Balanced binary search trees
Priority queues can also be implemented using balanced binary search trees (BSTs), such as AVL trees or red-black trees, where elements are stored ordered by their keys.[2] Insertion and extraction of the minimum (or maximum) both take O(log n) time due to the tree's height, with extract-min simply removing the leftmost (or rightmost) node. These structures support additional operations like searching for a specific key in O(log n) time and deleting arbitrary elements efficiently, making them suitable when such flexibility is needed beyond basic priority queue operations. However, they typically require more space and have higher constant factors compared to heap implementations.[1]
Binary heaps
A binary heap is a common implementation of a priority queue based on a complete binary tree that satisfies the heap property: in a max-heap, the key of each node is greater than or equal to the keys of its children, ensuring the maximum key is at the root.[25] This structure provides efficient support for insertion and extraction operations, each in O(\log n) time, where n is the number of elements, outperforming the linear-time operations of simpler list-based implementations.[26]
The binary heap is typically represented as an array to exploit the complete tree property, where the tree is filled level by level from left to right, with no gaps except possibly at the last level. For a 0-based array index i, the left child is at $2i + 1 and the right child at $2i + 2, while the parent of a node at i (for i > 0) is at \lfloor (i - 1)/2 \rfloor.[25] This implicit indexing avoids explicit pointers, enabling compact storage and fast navigation. The original formulation of the binary heap as a data structure for heapsort, introduced by J. W. J. Williams in 1964, used this array-based complete binary tree to facilitate efficient sorting.[27]
To construct a binary heap from an unsorted array of n elements, the build-heap procedure starts from the bottom of the tree and applies heapify-down (also called sink) operations upward from the last non-leaf node at index \lfloor n/2 \rfloor - 1 to the root. This bottom-up approach, developed by Robert W. Floyd in 1964 as an improvement to Williams' initial method, achieves O(n) time complexity overall, rather than the O(n \log n) of naive repeated insertions.[28] The heapify-down operation at a node compares it with its children, swaps with the larger child if necessary to restore the heap property, and recurses downward, taking O(\log n) time per call but amortizing to linear time across the build.[25]
Insertion into a max-heap adds the new element at the end of the array (the next available leaf position) and then performs heapify-up (also called swim) to bubble it upward by repeatedly swapping with its parent if it is larger, until the heap property is satisfied; this takes O(\log n) time in the worst case.[26] Extraction of the maximum element swaps the root with the last element, reduces the heap size by one, and applies heapify-down from the root to restore the property, again in O(\log n) time.[25] Peeking at the maximum is O(1), simply accessing the root. The following pseudocode illustrates these operations for a max-heap (adapted from standard implementations):
Insert (push):
function insert(heap, key):
heap.append(key) // Add to end
i = len(heap) - 1
heapify_up(heap, i)
function heapify_up(heap, i):
while i > 0:
parent = (i - 1) // 2
if heap[i] <= heap[parent]:
break
swap(heap[i], heap[parent])
i = parent
function insert(heap, key):
heap.append(key) // Add to end
i = len(heap) - 1
heapify_up(heap, i)
function heapify_up(heap, i):
while i > 0:
parent = (i - 1) // 2
if heap[i] <= heap[parent]:
break
swap(heap[i], heap[parent])
i = parent
Extract-max (pop):
function extract_max(heap):
if len(heap) == 0:
return null
max = heap[0]
heap[0] = heap.pop() // Move last to root
if len(heap) > 0:
heapify_down(heap, 0)
return max
function heapify_down(heap, i):
n = len(heap)
while True:
left = 2 * i + 1
right = 2 * i + 2
largest = i
if left < n and heap[left] > heap[largest]:
largest = left
if right < n and heap[right] > heap[largest]:
largest = right
if largest == i:
break
swap(heap[i], heap[largest])
i = largest
function extract_max(heap):
if len(heap) == 0:
return null
max = heap[0]
heap[0] = heap.pop() // Move last to root
if len(heap) > 0:
heapify_down(heap, 0)
return max
function heapify_down(heap, i):
n = len(heap)
while True:
left = 2 * i + 1
right = 2 * i + 2
largest = i
if left < n and heap[left] > heap[largest]:
largest = left
if right < n and heap[right] > heap[largest]:
largest = right
if largest == i:
break
swap(heap[i], heap[largest])
i = largest
[26]
A min-heap variant inverts the comparisons in the heap property and operations, so the parent key is less than or equal to its children's keys, placing the minimum at the root; this is useful for min-priority queues and is achieved by swapping greater-than and less-than conditions in the algorithms.[26] The space complexity of a binary heap is O(n), as it uses a single contiguous array of size n to store all elements without additional overhead beyond the keys themselves.[25]
Advanced heap variants
Fibonacci heaps represent a significant advancement over binary heaps by achieving better amortized time bounds for key operations through a more flexible structure consisting of a collection of heap-ordered trees connected via a root list.[29] Introduced by Michael L. Fredman and Robert E. Tarjan in 1984 and published in 1987, these heaps support insertion and decrease-key operations in amortized O(1) time, extract-min in amortized O(log n) time, and deletion in amortized O(log n) time.[29] The efficiency stems from lazy merging of trees during unions (linking by size or degree) and delayed propagation of structural changes via marking for lazy deletions, which defers cuts until necessary during extract-min to consolidate the forest.[29] This amortized analysis relies on a potential function that accounts for the number of trees and marked nodes, enabling applications like faster shortest-path algorithms.[29]
Pairing heaps offer a simpler alternative to Fibonacci heaps while maintaining competitive performance, particularly in practice, by avoiding complex degree tracking and potential functions in their basic implementation.[30] Developed by Michael L. Fredman, Robert Sedgewick, Daniel D. Sleator, and Robert E. Tarjan in 1986, pairing heaps are represented as heap-ordered trees where unions are performed by recursively pairing roots in a tournament-like manner.[30] They support insert, meld, and decrease-key in amortized O(1) time, with extract-min in amortized O(log n) time, using a two-pass linking strategy for decrease-key that first attaches the node to its parent and then restructures via sibling pairing.[30] Although their worst-case analysis was refined later, pairing heaps excel in empirical speed due to their straightforward code and reduced constant factors.[30]
d-ary heaps generalize binary heaps to trees where each node has up to d children, allowing tunable trade-offs between operation speeds based on the arity d. As detailed in standard algorithmic texts, the height of a d-ary heap is O(\log_d n), making insert operations, which bubble up the tree, take O(\log_d n) = O(\frac{\log n}{\log d}) time, while extract-min, which sifts down and may compare up to d children, takes O(d \log_d n) time. For d > 2, larger arities reduce the tree height for faster extracts at the cost of more comparisons per level, proving useful in scenarios like Dijkstra's algorithm where decrease-key is less frequent than extracts. The structure maintains the complete d-ary tree property in array representation, with parent-child indexing adjusted for d children.
Soft heaps introduce approximation to priority queues, permitting a controlled number of "corruptions" (errors in reported minima) to achieve faster operations, particularly for selection problems like finding medians.[31] Invented by Bernard Chazelle in 2000, a soft heap supports insert in amortized O(1) time, meld in O(1), find-min in O(1), and delete in amortized O(log n / log log n) time, while allowing up to \varepsilon n corruptions for error rate \varepsilon.[31] The design uses a forest of binomial-like trees with "carpooling" to lazily release excess elements, ensuring the minimum is approximate but enabling optimal error rates for applications in deterministic linear-time selection and minimum spanning trees.[31]
Recent post-2020 research has explored cache-oblivious variants of heap structures, leveraging van Emde Boas layouts to minimize cache misses and enhance parallelism in multi-core environments without tuning to specific cache parameters.[32] For instance, generalizations of tree layouts to B-tree-like heaps achieve O(log_B n) I/O complexity for operations, where B is the cache line size, improving scalability on modern hardware.[32]
Runtime analysis
The runtime performance of priority queues varies significantly depending on the underlying implementation, with key operations including insertion, extraction of the minimum (or maximum), decrease-key (updating a priority to a lower value), and building the structure from n elements. These complexities are analyzed under the assumption that priority comparisons take constant O(1) time.[33]
The following table summarizes the time complexities for common implementations, distinguishing between worst-case and amortized bounds where applicable:
| Operation | Unsorted List | Sorted List | Binary Heap | Fibonacci Heap |
|---|
| Insert | O(1) | O(n) | O(\log n) | O(1) amortized |
| Extract-min | O(n) | O(1) | O(\log n) | O(\log n) amortized |
| Decrease-key | O(n) | O(n) | O(\log n) | O(1) amortized |
| Build (n elements) | O(n) | O(n \log n) | O(n) | O(n) |
These bounds are established for unsorted and sorted lists using simple array or linked list structures, binary heaps via array-based complete binary trees, and Fibonacci heaps through a collection of heap-ordered trees supporting lazy merging.[33][34][35]
In Fibonacci heaps, the O(1) amortized time for decrease-key and insert arises from potential function analysis, which accounts for the cost of structural changes like linking and cutting trees over a sequence of operations, rather than charging them immediately; worst-case times for these can reach O(\log n).[35]
All standard priority queue implementations require O(n) space to store n elements, as each element must be represented along with its priority. However, pointer-based variants like Fibonacci heaps incur additional auxiliary space due to multiple child pointers per node (up to degree \Theta(\log n) in the worst case), leading to higher memory overhead compared to the compact O(n) array storage in binary heaps.[34][35]
Practical runtime is also influenced by factors beyond asymptotic complexity, such as cache performance: array-based structures like binary heaps benefit from sequential memory access and better locality, reducing cache misses, whereas pointer-based implementations like Fibonacci heaps suffer from irregular access patterns and higher miss rates.[36] Additionally, experimental studies from the late 1980s, including benchmarks on decrease-key heavy workloads, showed that pairing heaps—a simpler self-adjusting variant—often outperform Fibonacci heaps in practice due to lower constant factors and easier implementation, despite lacking formal O(1) amortized decrease-key bounds.[37]
Theoretical Foundations
Equivalence to sorting algorithms
A priority queue maintains a partial ordering on its elements through properties such as the heap invariant, where for every node, its priority is greater than or equal to (in a max-heap) or less than or equal to (in a min-heap) the priorities of its children. This establishes a partial sort, as the structure ensures that the highest (or lowest) priority element is always accessible at the root, while the remaining elements satisfy a recursive ordering without guaranteeing a total linear order like a fully sorted array.[38]
The connection to sorting arises from the selection problem, where repeatedly extracting the minimum (or maximum) element from a priority queue containing n elements produces them in sorted order. Inserting all n elements followed by n extractions simulates a sorting process, directly linking the efficiency of priority queue operations to sorting performance. In the comparison model of computation, where decisions rely solely on pairwise comparisons, a priority queue supporting insert and extract-min in O(log n) time per operation enables sorting in O(n log n) time, matching the known lower bound for comparison-based sorting algorithms, which requires at least Ω(n log n) comparisons in the worst case due to the decision tree height needed to distinguish n! permutations.[38][39]
Conversely, efficient sorting implies efficient priority queues through reductions that simulate priority queue operations using a sorter. A general deterministic linear-space reduction transforms a sorting oracle into a priority queue by sorting batches of elements and maintaining sorted access to simulate inserts and extracts, ensuring that the per-operation cost of the priority queue matches the per-key sorting cost asymptotically. This equivalence holds in the comparison model, establishing that priority queues and sorting are computationally equivalent up to constant factors.[40][39]
The theoretical ties between priority queues and sorting efficiency trace back to the 1960s, when J. W. J. Williams introduced the binary heap as a data structure for achieving O(n log n) sorting through partial ordering maintenance. Binary heaps serve as a practical bridge to this equivalence by realizing the O(log n) operations that align with sorting lower bounds.
Heap sort and selection sort connections
One approach to sorting using a priority queue involves inserting all n elements into the queue, which requires O(n \log n) time for a binary heap implementation, followed by n extractions of the maximum (or minimum) element, each taking O(\log n) time, for a total of O(n \log n) time.[2] This method, often called priority queue sort or PQ-sort, produces a stable sort if the priority queue implementation preserves the order of elements with equal priorities, such as by using insertion order as a secondary key.[2] The total time complexity can be expressed more precisely as \sum_{i=1}^n \log i \approx n \log n - 1.44n, reflecting the varying heap sizes during extractions.
Heap sort represents an efficient, in-place adaptation of this priority queue-based sorting paradigm, leveraging the binary heap data structure. The algorithm begins with a build-heap operation on the array, which constructs a max-heap in O(n) time using a bottom-up heapify process starting from the last non-leaf node. It then performs n iterations of swapping the root (maximum element) with the last unsorted element, reducing the heap size by one, and heapifying the reduced heap in O(\log n) time per iteration, yielding an overall O(n \log n) time complexity.[27] Originally proposed by Williams in 1964, heap sort is unstable due to the swapping mechanism, which can reorder equal elements, but it achieves optimal asymptotic performance and requires no additional space beyond the input array.[27]
Selection sort can also be viewed through the lens of priority queue operations, where the unsorted portion of the array acts as an implicit priority queue implemented via an unsorted list. In each of n passes, the maximum element is selected and swapped to the end of the prefix, mimicking an extract-max operation; however, finding the maximum in an unsorted list takes O(n) time per pass, resulting in O(n^2) overall time. For linear-time selection of the k-th largest element (a building block for sorting), the median-of-medians algorithm partitions the array around a good pivot in O(n) time, enabling recursive selection.[41]
Conversely, a priority queue can be constructed from a sorted array using a binary search tree-based implementation, where the sorted elements seed the tree and balance is maintained to support logarithmic-time insertions, extractions, priority updates, and peeks.[2]
Applications
Shortest path and spanning tree algorithms
Priority queues play a central role in efficient implementations of shortest path and minimum spanning tree algorithms on graphs, particularly by managing the selection of the next vertex or edge based on priority values such as distances or weights.[42]
Dijkstra's algorithm, introduced in 1959, computes the shortest paths from a single source vertex to all other vertices in a graph with non-negative edge weights.[43] It employs a min-priority queue to store tentative distances to unvisited vertices, where the queue holds pairs of (distance, vertex). The algorithm repeatedly extracts the vertex with the minimum distance using extract-min, marks it as visited, and relaxes the distances to its adjacent vertices via decrease-key operations if shorter paths are found. With a binary heap implementation, the time complexity is O((V + E) log V), where V is the number of vertices and E is the number of edges, due to V extract-min operations and up to E decrease-key operations, each taking O(log V) time.[42]
The following pseudocode illustrates the core use of the priority queue in Dijkstra's algorithm:
Initialize priority queue Q with (0, source) and infinity for others
While Q is not empty:
u = extract-min(Q)
If u is marked visited, continue
Mark u visited
For each neighbor v of u:
If dist[v] > dist[u] + weight(u, v):
dist[v] = dist[u] + weight(u, v)
decrease-key(Q, v, dist[v])
Initialize priority queue Q with (0, source) and infinity for others
While Q is not empty:
u = extract-min(Q)
If u is marked visited, continue
Mark u visited
For each neighbor v of u:
If dist[v] > dist[u] + weight(u, v):
dist[v] = dist[u] + weight(u, v)
decrease-key(Q, v, dist[v])
This structure ensures that the frontier of unprocessed vertices is always expanded by the closest one, guaranteeing correctness for non-negative weights.[43]
Prim's algorithm, proposed in 1957, constructs a minimum spanning tree (MST) for a connected, undirected graph by growing a tree from an arbitrary starting vertex, adding the minimum-weight edge connecting a tree vertex to a non-tree vertex at each step.[44] It uses a priority queue to track the minimum key (edge weight) for each non-tree vertex, similar to Dijkstra's setup, where the queue prioritizes edges by weight. Extract-min selects the next vertex to add to the tree, and decrease-key updates keys for adjacent non-tree vertices. Using a binary heap, the time complexity is O(E log V), as the algorithm performs V extract-min and up to E decrease-key operations.[42]
The priority queue enables these algorithms to efficiently manage the dynamic set of candidate vertices or edges, avoiding the need to scan all elements repeatedly and achieving logarithmic-time access to the minimum priority element. For sparse graphs, using a Fibonacci heap instead of a binary heap in Dijkstra's algorithm reduces the time complexity to O(E + V log V), leveraging amortized O(1) decrease-key operations.
Huffman coding and data compression
Huffman coding, a lossless data compression technique, employs a min-priority queue to construct an optimal prefix code tree based on symbol frequencies, assigning shorter codes to more frequent symbols to minimize the average code length. The algorithm begins by creating leaf nodes for each unique symbol, with their frequencies as priorities, and inserting them into the priority queue. It then repeatedly extracts the two nodes with the minimum frequencies, merges them into a new internal node whose frequency is the sum of its children, and reinserts this new node back into the queue. This process continues until only one node remains, forming the root of a binary tree that defines the variable-length codes via traversal paths from root to leaves.[45][46]
The resulting Huffman tree enables encoding where the path from root to a leaf (left for 0, right for 1) yields the prefix-free code for that symbol, ensuring no code is a prefix of another and achieving optimality for known frequencies under the Kraft inequality. Using a binary heap as the priority queue implementation, the construction requires O(n) extract-min and insert operations, each taking O(log n) time, yielding an overall time complexity of O(n log n) for n symbols.[47][48]
For illustration, consider symbols A (frequency 5), B (3), and C (2). The priority queue initially holds these leaves. First, extract C and B (mins 2 and 3), merge into node X (frequency 5), reinsert X. Now queue has A (5) and X (5); extract both, merge into root Y (10). The tree assigns code 0 to A, 10 to B, and 11 to C, reducing total bits for a message like ABC from 24 (fixed 8-bit) to 5.[46]
Huffman coding underpins entropy encoding in standards like ZIP (via DEFLATE, combining LZ77 with dynamic Huffman trees) and JPEG (for baseline lossless compression of quantized DCT coefficients). It also approximates aspects of arithmetic coding by providing efficient variable-length codes for discrete symbols in broader compression pipelines.[49]
An extension, adaptive Huffman coding, builds and updates the tree incrementally for streaming data without prior frequency knowledge, using techniques like the FGK algorithm to rebalance nodes as symbols arrive. This variant, developed in the 1970s, addresses dynamic sources by periodically adapting code lengths based on running frequency estimates.[50]
Discrete event simulation and search algorithms
In discrete event simulation (DES), a min-priority queue is essential for managing the event list, where each event is associated with a timestamp serving as its priority key. Events are inserted into the queue upon scheduling, and the simulation advances by repeatedly extracting the minimum-priority event (the earliest timestamp) for processing, which may generate new future events to be inserted. This approach ensures events are handled in chronological order, enabling efficient modeling of systems like queueing networks in operations research. For instance, in simulating a bank teller system, customer arrival and service completion events are prioritized by time, allowing dynamic updates to the queue as the simulation progresses.[51]
Priority queues also play a central role in best-first search algorithms, where nodes are prioritized based on an evaluation function estimating path cost. In the A* algorithm, a specific form of best-first search, the priority is determined by the function f(n) = g(n) + h(n), where g(n) is the exact cost from the start node to n, and h(n) is a heuristic estimate of the cost from n to the goal; the algorithm uses extract-min operations on these f-scores to expand the most promising node next. Introduced in 1968, A* guarantees optimality if h is admissible (never overestimates the true cost).[52]
A practical example of A* is pathfinding in grid-based environments, such as robotics or video games, where the open set is maintained as a priority queue of nodes ordered by f-score, typically storing tuples like (f, g, \text{[node](/page/Node)}) to break ties and avoid exhaustive exploration as in breadth-first or depth-first search. This directed expansion reduces the search space significantly compared to uninformed methods, making it suitable for large graphs. In graph representations, A* with a binary heap priority queue achieves a time complexity of O(E \log V), where E is the number of edges and V is the number of vertices, assuming consistent heuristics.[52][53]
Priority queues have been applied in computer graphics for adaptive terrain rendering, as in the ROAM (Real-time Optimally Adapting Meshes) algorithm, which uses dual priority queues to manage split and merge operations on triangular meshes based on screen-space error metrics. Triangles exceeding an error threshold are prioritized for refinement in the split queue, while opportunities for coarsening are handled in the merge queue, enabling real-time updates for large-scale terrains viewed from varying distances. Developed in 1997, ROAM ensures frame-coherent rendering by processing high-priority updates first.[54]
Other practical uses
Priority queues play a crucial role in bandwidth management within computer networks, particularly for packet scheduling based on Quality of Service (QoS) levels. In routers and switches, they enable the classification and prioritization of traffic streams, ensuring that high-priority packets, such as those for voice or video, are transmitted ahead of lower-priority data. For instance, Weighted Fair Queuing (WFQ) employs a priority queue mechanism to approximate Generalized Processor Sharing (GPS), allocating bandwidth fairly according to assigned weights while preventing starvation of lower-priority flows.[55][56]
In operating systems, priority queues facilitate task scheduling by ordering processes based on their priority levels to optimize resource allocation and responsiveness. The Linux Completely Fair Scheduler (CFS), introduced in kernel version 2.6.23, uses a red-black tree structure as an efficient implementation of a priority queue, where virtual runtime serves as the key for ordering tasks to ensure fair CPU time distribution across processes.[57] This approach allows for O(log n) insertion and minimum extraction operations, making it suitable for dynamic workloads in multi-tasking environments.[58]
Priority queues are integral to numerical computations, such as nearest neighbor searches using k-d trees, where they manage the exploration of spatial partitions by prioritizing nodes based on distance bounds to the query point. In the Best-Bin-First (BBF) search algorithm for k-d trees, a priority queue orders candidate regions by their potential to contain nearer neighbors, improving efficiency in high-dimensional data retrieval tasks common in computer graphics and machine learning.[59] Similarly, in Monte Carlo simulations for molecular dynamics, priority queues schedule collision events by predicted timestamps, enabling event-driven processing that scales to large particle systems without exhaustive pairwise checks.[60]
In machine learning, particularly reinforcement learning, priority queues enhance training efficiency through prioritized experience replay. The 2015 Prioritized Experience Replay method, integrated with Deep Q-Networks (DQN), uses a sum-tree-based priority queue to sample transitions from the replay buffer based on their temporal-difference error, focusing on more informative experiences to accelerate learning and improve policy convergence.[61]
An practical example of priority queues in networking is their use in routers for congestion management, where low-priority packets are dropped first during buffer overflows to protect critical traffic. In Priority Queuing schemes, incoming packets are directed to separate queues by priority, and when queues exceed limits, the router selectively discards from the lowest-priority queue, maintaining throughput for high-priority flows like real-time applications.[62][63]
Advanced Variants
Parallel and concurrent priority queues
Parallel and concurrent priority queues extend the standard insert and extract-min operations to support multi-threaded access in shared-memory multiprocessor environments, addressing synchronization challenges such as contention and race conditions to ensure linearizability and scalability. These structures are essential for applications requiring dynamic prioritization under high concurrency, like task scheduling and parallel graph algorithms. Early designs in the 1980s and 1990s laid the foundation by adapting heap-based implementations for concurrent operations, while modern variants incorporate lock-free techniques and parallel batching for better performance on multicore and many-core systems. Recent advances as of 2025 include the Multi Bucket Queue (MBQ), which targets efficient concurrent priority scheduling by distributing tasks across multiple buckets to reduce contention and improve throughput in high-thread-count scenarios.[64][65]
Concurrent access mechanisms primarily fall into lock-based and lock-free categories. Lock-based approaches, such as fine-grained locking on individual heap nodes, minimize contention by acquiring locks only on affected paths during operations; for instance, the algorithm by Hunt et al. (1996) uses multiple per-node locks and a bit-reversal technique for bottom-up insertions to scatter accesses across the heap fringe, reducing hot-spot formation while supporting top-down deletions. This method avoids deadlocks through careful lock ordering and demonstrates improved throughput on small to large heaps under mixed workloads on shared-memory multiprocessors like the Silicon Graphics Challenge. In contrast, lock-free implementations rely on atomic compare-and-swap (CAS) operations to achieve progress without blocking, as in the skiplist-based priority queue by Sundell and Tsigas (2003), which supports fully concurrent inserts and delete-mins suitable for pre-emptive multi-threaded systems. These lock-free designs outperform lock-based ones for three or more threads across various platforms, including up to 64-processor SGI Origin 2000 systems, by eliminating priority inversion and deadlocks; however, they must mitigate issues like the ABA problem—where a reused memory location fools a CAS—through techniques such as pointer marking with deletion bits.[66][67]
Models for these queues typically operate in shared-memory settings with synchronization barriers to coordinate multi-processor access, as explored in 1990s work adapting array-based heaps for concurrent use. Building on foundational concurrent operations from Jones (1989), which enabled pipelined insertions and deletions on skew heaps, later algorithms like Hunt et al. (1996) further optimized for parallelism by allowing up to O(M) concurrent operations on a heap of size M, compared to O(log M) in prior sequential adaptations. Parallel operations often involve batch inserts and extracts to amortize overhead, using strategies like work-stealing in partitioned or multi-queue setups; for example, each thread maintains a local priority queue, inserting elements randomly and stealing high-priority tasks from others' queues when idle, which balances load dynamically in task-parallel schedulers. Partitioned heaps distribute elements across multiple sub-heaps to localize contention, facilitating efficient batch processing in shared-memory models.[66][68]
Performance metrics emphasize scalability to p processors, ideally achieving O((log n)/p) time per operation through reduced contention; the funnel-tree algorithm by Shavit and Zemach (1999) exemplifies this by scaling near-linearly to 128 processors on simulated shared-memory architectures, outperforming simple tree-based designs by up to 5x at high concurrency levels with more than 10 priority ranges. In practice, lock-free skiplist queues like Sundell and Tsigas (2003) show superior throughput under pre-emptive scheduling compared to fine-grained lock-based heaps. Gaps persist in highly parallel domains, such as post-2010 GPU ray-tracing, where priority queues must handle massive thread counts; implementations like the parallel heap by He et al. (2012) use wide nodes and SIMT execution on CUDA-enabled GPUs, yielding up to 30-fold speedups over optimized sequential or multicore versions for fine-grained dynamic scenes in ray-tracing and photon mapping. Some GPU approaches approximate priority queues via sorting networks for ray reordering, enhancing traversal efficiency in real-time rendering pipelines.[65][67][69][70]
Fibonacci and pairing heaps
Fibonacci heaps represent an advanced sequential priority queue variant designed to achieve optimal amortized time bounds for key operations, extending ideas from binary heaps but with more flexible tree structures. Recent theoretical work as of 2025 includes strict Fibonacci heaps, which provide pointer-based implementations matching the amortized bounds in the worst case, addressing long-standing gaps in deterministic performance guarantees.[71] Each tree in a Fibonacci heap is represented using circular doubly-linked lists for both the root list and the children of each node, allowing efficient splicing and linking without immediate restructuring.[35] This lazy merging approach delays consolidation of trees until necessary, typically during a delete-min operation, which helps maintain low amortized costs.[35]
The amortized analysis of Fibonacci heaps relies on a potential function defined as \Phi(H) = t(n) + 2m(n), where t(n) is the number of trees in the heap and m(n) is the number of marked non-root nodes, capturing the "debt" from lazy operations to bound future costs.[35] Core operations include linking two trees in constant time by attaching the root of one to the other based on key comparison, without changing ranks immediately.[35] Cutting a node from its parent takes amortized O(1) time, potentially triggering a cascading cut to unmarked ancestors if the node was marked, preserving the degree condition that limits tree heights.[35] Consolidation during delete-min merges trees of equal rank in O(log n) amortized time, ensuring the overall structure remains balanced over a sequence of operations.[35]
Pairing heaps, introduced as a simpler alternative, use a self-adjusting tree structure with a child-sibling representation, where each node points to its first child and next sibling, enabling efficient splicing for merging two heaps in O(1) time by linking roots directly. Recent analyses as of 2023 have refined the efficiency of self-adjusting variants like multipass pairing heaps and smooth heaps, confirming their practical amortized performance under varied workloads.[72] The delete-min operation typically employs a two-pass approach: first pairing sibling subtrees left-to-right, then recombining them right-to-left in a weight-biased manner, where heavier subtrees are linked to lighter ones to promote balance.[73] One-pass variants, such as front-to-back or back-to-front splicing, simplify the process but lack complete theoretical bounds, though they perform comparably in practice.[73]
Empirical studies confirm pairing heaps' practical efficiency, with the auxiliary two-pass variant showing the lowest work ratios—often below 2.0—in simulations involving up to 1.2 million operations mixing inserts, decrease-keys, and delete-mins, outperforming multipass variants under heavy decrease-key workloads.[74] Compared to Fibonacci heaps, pairing heaps achieve similar amortized bounds—O(1) for insert and decrease-key, O(log n) for delete-min—but with far less implementation complexity, using fewer pointers per node and avoiding explicit rank or mark fields.[73][74] Developed in the 1980s by Fredman, Sedgewick, Sleator, and Tarjan, pairing heaps excel in benchmarks due to their straightforward code and reduced overhead, often running faster than Fibonacci heaps despite the latter's stronger theoretical guarantees.[73][74]
Both structures inherit flexibility from binary heaps but introduce amortization to handle decrease-key efficiently; however, they suffer from high constant factors in operations and poor cache locality due to scattered pointer traversals in their linked-list representations, limiting real-world adoption without optimizations.[75]