Priority Queue Data Structure
Definition and Concepts
A priority queue is an abstract data structure that operates similarly to a regular queue but assigns a priority to each element. Elements with higher priority are dequeued before those with lower priority, regardless of their order of insertion. You can visualize a priority queue as a hospital emergency room where patients with more severe conditions are treated first, even if they arrived later. The primary operations include enqueue, which adds an element with a specified priority, and dequeue, which removes and returns the element with the highest priority. Additional operations include peek, which views the highest-priority element without removing it, and isEmpty, which checks if the priority queue is empty. Priority queues are typically implemented using a heap (often a binary heap) to ensure efficient priority-based operations.
Why Use It?
A priority queue is useful when you need to process elements based on their priority rather than their arrival order. It ensures that the most critical or highest-priority elements are handled first, making it ideal for scheduling, resource allocation, and optimization problems. Priority queues are efficient because they use a heap structure, which provides logarithmic time complexity for most operations.
Where to Use? (Real-Life Examples)
- Task Scheduling in Operating Systems: Operating systems use priority queues to schedule processes based on their priority levels, ensuring critical tasks are executed first.
- Hospital Emergency Systems: Emergency rooms prioritize patients based on the severity of their condition, using a priority queue to manage treatment order.
- Dijkstra’s Algorithm: Priority queues are used in graph algorithms like Dijkstra’s to select the next node with the smallest distance.
- Huffman Coding: Priority queues are used to build optimal prefix codes for data compression by repeatedly selecting the two nodes with the smallest frequencies.
Explain Operations
- Enqueue: This operation adds an element with a specified priority to the priority queue. The element is inserted into the heap, and the heap property is restored by bubbling up. It has a time complexity of O(log n).
- Dequeue: This operation removes and returns the element with the highest priority (the root in a min-heap). The last element is moved to the root, and the heap property is restored by bubbling down. It has a time complexity of O(log n).
- Peek: This operation returns the highest-priority element (the root) without removing it. It has a time complexity of O(1).
- isEmpty: This operation checks whether the priority queue is empty. It has a time complexity of O(1).
- Size: This operation returns the number of elements in the priority queue. It has a time complexity of O(1).
Java Implementation
The following Java code implements a priority queue using a binary min-heap, where smaller values have higher priority.
public class PriorityQueue {
private int[] heap; // Array to store the heap elements
private int maxSize; // Maximum size of the priority queue
private int size; // Current number of elements in the priority queue
// Constructor to initialize the priority queue with a given size
public PriorityQueue(int capacity) {
maxSize = capacity;
heap = new int[maxSize];
size = 0; // Priority queue is initially empty
}
// Enqueue: Adds an element to the priority queue
public void enqueue(int value) {
if (size >= maxSize) { // Checks if the priority queue is full
throw new IllegalStateException("The priority queue is full.");
}
heap[size] = value; // Adds the new element at the end
bubbleUp(size); // Restores heap property by bubbling up
size++; // Increments the size
}
// Dequeue: Removes and returns the highest-priority element
public int dequeue() {
if (isEmpty()) { // Checks if the priority queue is empty
throw new IllegalStateException("The priority queue is empty.");
}
int result = heap[0]; // Stores the root (highest-priority element)
heap[0] = heap[size - 1]; // Moves the last element to the root
size--; // Decrements the size
if (size > 0) { // Avoids bubbling down if the heap is empty
bubbleDown(0); // Restores heap property by bubbling down
}
return result;
}
// Peek: Returns the highest-priority element without removing it
public int peek() {
if (isEmpty()) { // Checks if the priority queue is empty
throw new IllegalStateException("The priority queue is empty.");
}
return heap[0]; // Returns the root element
}
// isEmpty: Checks if the priority queue is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of elements in the priority queue
public int size() {
return size;
}
// Helper method to bubble up an element to restore min-heap property
private void bubbleUp(int index) {
int parent = (index - 1) / 2; // Calculates parent index
while (index > 0 && heap[index] < heap[parent]) { // Compares with parent
swap(index, parent); // Swaps if child is smaller
index = parent; // Updates index to parent
parent = (index - 1) / 2; // Recalculates parent
}
}
// Helper method to bubble down an element to restore min-heap property
private void bubbleDown(int index) {
int minIndex = index; // Tracks the smallest element
while (true) {
int leftChild = 2 * index + 1; // Left child index
int rightChild = 2 * index + 2; // Right child index
if (leftChild < size && heap[leftChild] < heap[minIndex]) {
minIndex = leftChild; // Updates if left child is smaller
}
if (rightChild < size && heap[rightChild] < heap[minIndex]) {
minIndex = rightChild; // Updates if right child is smaller
}
if (minIndex == index) break; // Stops if no smaller child found
swap(index, minIndex); // Swaps with the smaller child
index = minIndex; // Updates index to continue bubbling down
}
}
// Helper method to swap two elements in the heap
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
How It Works
- Initialization: The constructor initializes an array of size
maxSizeand setssizeto 0, indicating an empty priority queue. - Enqueue Operation:
- The method checks if the priority queue is full by comparing
sizetomaxSize. - It adds the new element at the end of the heap (
heap[size]), callsbubbleUpto restore the min-heap property by moving the element up if it’s smaller than its parent, and incrementssize. - If the queue is full, it throws an exception.
- The method checks if the priority queue is full by comparing
- Dequeue Operation:
- The method verifies that the priority queue is not empty using
isEmpty(). - It saves the root element (
heap[0]), moves the last element (heap[size-1]) to the root, decrementssize, and callsbubbleDownto restore the min-heap property by moving the root down if it’s larger than its children. - If the queue is empty, it throws an exception.
- The method verifies that the priority queue is not empty using
- Peek Operation: The method returns the root element (
heap[0]) without modifying the heap. - isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty priority queue, and false otherwise. - Size Operation: The method returns
size, which represents the number of elements in the priority queue. - BubbleUp Helper: This method moves a newly added element up the heap by comparing it with its parent and swapping if it’s smaller, continuing until the heap property is restored.
- BubbleDown Helper: This method moves the root element down by comparing it with its children, swapping with the smaller child if necessary, and continuing until the heap property is restored.
- Swap Helper: This method exchanges two elements in the heap array.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(log n) | O(1) |
| Dequeue | O(log n) | O(1) |
| Peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Key Differences / Notes
- Min-Heap vs. Max-Heap:
- The implementation above uses a min-heap, where smaller values have higher priority. A max-heap can be implemented by reversing the comparison logic, where larger values have higher priority.
- Array-Based vs. Other Implementations:
- The array-based heap implementation is memory-efficient and provides O(log n) for enqueue and dequeue operations.
- Other implementations, like a binary search tree or linked list, are less common due to higher complexity or overhead.
- Thread Safety: The provided implementation is not thread-safe. For concurrent applications, use Java’s
PriorityBlockingQueueor synchronize access. - Java’s Built-in Priority Queue: Java provides
java.util.PriorityQueue, which is a min-heap by default but can be customized for max-heap behavior using a comparator.
✅ Tip: Use a min-heap for scenarios where the smallest element has the highest priority (e.g., shortest path algorithms) and a max-heap for scenarios where the largest element has priority (e.g., task prioritization).
⚠ Warning: Ensure the priority queue’s capacity is sufficient for your use case to avoid overflow errors in the array-based implementation. For dynamic sizing, consider Java’s
PriorityQueueclass.
Exercises
- Task Scheduler: Write a Java program that uses the priority queue implementation above to simulate a task scheduler. Enqueue tasks with priorities (represented as integers) and dequeue them in order of highest priority, printing each task as it’s processed.
- Kth Largest Element: Create a program that uses a priority queue to find the kth largest element in an array of integers. Test it with at least three different arrays and k values.
- Min-Heap to Max-Heap: Modify the priority queue implementation to support a max-heap (where larger values have higher priority). Test the modified version with enqueue and dequeue operations.
- Dijkstra’s Algorithm: Implement Dijkstra’s algorithm for a weighted graph using the priority queue. Represent the graph as an adjacency list and compute the shortest paths from a source node.
- Merge K Sorted Lists: Write a program that merges k sorted arrays into a single sorted array using a priority queue. Enqueue the first element of each array with its array index and value, and repeatedly dequeue and enqueue the next element from the same array.