Loading content...
Every abstract data type is defined not by its implementation, but by its interface—the set of operations it supports and the behaviors those operations guarantee. For priority queues, this interface is elegantly minimal yet remarkably powerful.
At its core, a priority queue promises just two primary capabilities:
From these two operations, augmented by a few helper methods, we can build scheduling systems, power graph algorithms, implement compression, and solve countless other problems.
This page provides a comprehensive specification of the priority queue interface, examining each operation in depth, understanding its semantics, preconditions, postconditions, and proper usage patterns.
By the end of this page, you will understand every operation in the priority queue interface: insert (enqueue), extractMin/extractMax (dequeue), peek, isEmpty, and size. You'll know their time complexities, edge cases, error handling conventions, and how they combine in real usage patterns. This knowledge forms the foundation for using priority queues effectively in any programming language.
Before diving into individual operations, let's see the complete interface specification. This provides an overview that we'll then explore operation by operation.
Core Operations (Essential):
insert(element, priority) — Add element with given priorityextractMin() / extractMax() — Remove and return extreme priority elementpeek() / findMin() / findMax() — View extreme element without removingisEmpty() — Check if collection is emptysize() — Return number of elementsExtended Operations (Common but not universal):
decreaseKey(element, newPriority) — Lower an element's priority (used in Dijkstra's)increaseKey(element, newPriority) — Raise an element's priorityremove(element) — Remove arbitrary element (not just min/max)contains(element) — Check if element existsclear() — Remove all elements123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
/** * Complete Priority Queue Interface Specification * * This interface defines a min-priority queue where lower priority * values indicate higher importance (extracted first). * * @template T - The type of elements stored in the priority queue */interface PriorityQueue<T> { // ========== Core Operations ========== /** * Inserts an element with the specified priority. * * @param element - The element to insert * @param priority - The priority value (lower = more important) * @throws Error if priority is not a valid comparable value * * Time Complexity: O(log n) for heap-based implementations */ insert(element: T, priority: number): void; /** * Removes and returns the element with minimum priority. * * @returns The element with lowest priority value * @throws Error if the queue is empty * * Time Complexity: O(log n) for heap-based implementations */ extractMin(): T; /** * Returns the element with minimum priority without removing it. * * @returns The element with lowest priority value * @throws Error if the queue is empty * * Time Complexity: O(1) */ peek(): T; /** * Checks whether the priority queue contains any elements. * * @returns true if empty, false otherwise * * Time Complexity: O(1) */ isEmpty(): boolean; /** * Returns the number of elements in the priority queue. * * @returns Element count * * Time Complexity: O(1) */ size(): number; // ========== Extended Operations ========== /** * Decreases the priority of an existing element. * Used by algorithms like Dijkstra's shortest path. * * @param element - The element whose priority to decrease * @param newPriority - The new (lower) priority value * @throws Error if element not found or newPriority >= current priority * * Time Complexity: O(log n) for indexed binary heap * O(1) amortized for Fibonacci heap */ decreaseKey?(element: T, newPriority: number): void; /** * Removes a specific element from the queue (not necessarily the minimum). * * @param element - The element to remove * @returns true if element was found and removed, false otherwise * * Time Complexity: O(n) for basic heap (find) + O(log n) for removal * O(log n) for indexed heap */ remove?(element: T): boolean; /** * Checks whether a specific element exists in the queue. * * @param element - The element to search for * @returns true if element is in the queue, false otherwise * * Time Complexity: O(n) for basic heap, O(1) for indexed heap */ contains?(element: T): boolean; /** * Removes all elements from the priority queue. * * Time Complexity: O(1) or O(n) depending on memory management */ clear?(): void;}The insert operation (also called push, enqueue, or add in various libraries) is the primary way to add elements to a priority queue. Let's examine it comprehensively.
Operation Semantics:
insert(element: T, priority: P) → void
Preconditions:
Postconditions:
insert(element, priority)), others expect comparable elements where priority is inherent (insert(comparableElement)).123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Example 1: Basic insert with explicit priorityconst taskQueue = new PriorityQueue<string>(); taskQueue.insert("Email notification", 5); // Low priority (5)taskQueue.insert("Security alert", 1); // High priority (1)taskQueue.insert("Database backup", 3); // Medium priority (3) // Queue state: Security alert will be extracted first // Example 2: Insert with derived priorityinterface ScheduledTask { name: string; deadline: Date;} const scheduler = new PriorityQueue<ScheduledTask>(); const task1: ScheduledTask = { name: "Submit report", deadline: new Date("2024-01-15")}; // Priority is the deadline timestamp (earlier = more urgent)scheduler.insert(task1, task1.deadline.getTime()); // Example 3: Handling insert edge casesfunction safeInsert<T>( queue: PriorityQueue<T>, element: T, priority: number): boolean { // Validate priority if (!Number.isFinite(priority)) { console.error("Priority must be a finite number"); return false; } // Check for NaN if (Number.isNaN(priority)) { console.error("Priority cannot be NaN"); return false; } try { queue.insert(element, priority); return true; } catch (error) { console.error("Insert failed:", error); return false; }}If an element's priority is derived from a mutable property, changing that property after insertion breaks the heap invariant. The priority queue has no way to know the priority changed. Either use immutable priority values, or explicitly use decreaseKey/increaseKey operations when priorities change.
The extractMin (or extractMax) operation is the heart of the priority queue—it removes and returns the highest-priority element. This is where the priority queue delivers its core value proposition.
Operation Semantics:
extractMin() → T // For min-priority queue
extractMax() → T // For max-priority queue
Alternative Names:
poll() (Java)heappop() (Python's heapq)pop() or top() combined with removaldequeue() (drawing parallel to standard queues)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// Safe extraction patterns class SafePriorityQueue<T> { private heap: Array<{ element: T; priority: number }> = []; /** * Extract minimum - throws on empty queue * Use when: empty queue is a programming error */ extractMin(): T { if (this.isEmpty()) { throw new Error("Cannot extract from empty priority queue"); } return this.doExtract(); } /** * Try extract - returns undefined on empty queue * Use when: empty queue is a normal condition */ tryExtractMin(): T | undefined { if (this.isEmpty()) { return undefined; } return this.doExtract(); } /** * Extract with result wrapper * Use when: you want explicit success/failure handling */ extractMinResult(): { success: true; value: T } | { success: false } { if (this.isEmpty()) { return { success: false }; } return { success: true, value: this.doExtract() }; } private doExtract(): T { const min = this.heap[0].element; const last = this.heap.pop()!; if (this.heap.length > 0) { this.heap[0] = last; this.bubbleDown(0); } return min; } isEmpty(): boolean { return this.heap.length === 0; } private bubbleDown(index: number): void { // Implementation details... }} // Usage patternsconst pq = new SafePriorityQueue<string>();pq.insert("task1", 1);pq.insert("task2", 2); // Pattern 1: Check then extract (two operations, potential race condition)if (!pq.isEmpty()) { const task = pq.extractMin(); processTask(task);} // Pattern 2: Try extract (single operation, null check required)const maybeTask = pq.tryExtractMin();if (maybeTask !== undefined) { processTask(maybeTask);} // Pattern 3: Extract until empty (common loop pattern)while (!pq.isEmpty()) { const task = pq.extractMin(); processTask(task);} // Pattern 4: Process N items or until emptyfunction processUpToN<T>(queue: SafePriorityQueue<T>, n: number): T[] { const results: T[] = []; for (let i = 0; i < n && !queue.isEmpty(); i++) { results.push(queue.extractMin()); } return results;} function processTask(task: string): void { console.log(`Processing: ${task}`);}The sequence of elements returned by repeated extractMin calls gives elements in priority order. This is how heapsort works: insert all elements, then extract until empty. The extraction sequence is the sorted sequence. Time: O(n log n).
The peek operation (also called findMin, top, or front) allows inspection of the highest-priority element without removing it. This read-only operation is essential for decision-making without commitment.
Operation Semantics:
peek() → T // Returns minimum/maximum without removal
Why Peek Matters:
Peek enables important patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
interface Task { id: string; priority: number; deadline: Date;} class TaskScheduler { private queue: PriorityQueue<Task>; private currentTime: Date; constructor() { this.queue = new PriorityQueue<Task>(); this.currentTime = new Date(); } /** * Pattern 1: Conditional processing based on peek * Only process if the top task is ready */ processIfReady(): Task | null { if (this.queue.isEmpty()) { return null; } const nextTask = this.queue.peek(); // Check if task is ready to be processed if (nextTask.deadline <= this.currentTime) { // Deadline has passed, process immediately return this.queue.extractMin(); } // Not ready yet, don't extract return null; } /** * Pattern 2: Batch processing with priority threshold * Process all high-priority items (priority < threshold) */ processHighPriority(threshold: number): Task[] { const processed: Task[] = []; // Keep processing while queue has items below threshold while (!this.queue.isEmpty() && this.queue.peek().priority < threshold) { processed.push(this.queue.extractMin()); } return processed; } /** * Pattern 3: Time-until-next calculation * How long until the next task is due? */ timeUntilNext(): number | null { if (this.queue.isEmpty()) { return null; // No tasks pending } const nextTask = this.queue.peek(); const msUntilDeadline = nextTask.deadline.getTime() - this.currentTime.getTime(); return Math.max(0, msUntilDeadline); } /** * Pattern 4: Peek for logging/monitoring */ logQueueStatus(): void { if (this.queue.isEmpty()) { console.log("Queue is empty"); return; } const top = this.queue.peek(); console.log(`Queue size: ${this.queue.size()}`); console.log(`Next task: ${top.id} (priority ${top.priority})`); console.log(`Due: ${top.deadline.toISOString()}`); // Queue unchanged after logging }}For safety, some implementations return a copy or an immutable view from peek. This prevents accidental corruption of the heap. In performance-critical code, this copy overhead might matter. Know your library's behavior and consider defensive copying if returning mutable references.
While less glamorous than insert and extract, the isEmpty and size operations are essential for control flow and handling edge cases. Let's examine them with the rigor they deserve.
isEmpty Operation:
isEmpty() → boolean
size Operation:
size() → integer
Both are O(1) operations in all reasonable implementations—the size is tracked as elements are inserted and extracted.
Why Two Operations?
You might ask: "Why have isEmpty when we can just check size() === 0?" Several reasons:
isEmpty() clearly expresses intent. size() === 0 requires the reader to parse an equality check.isEmpty() returns a definite boolean. size() returns a number that must be compared.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Common patterns using isEmpty and size function drainQueue<T>(queue: PriorityQueue<T>): T[] { const results: T[] = []; while (!queue.isEmpty()) { results.push(queue.extractMin()); } return results;} function processWithLimit<T>( queue: PriorityQueue<T>, maxItems: number): T[] { const results: T[] = []; // Use size for limiting, isEmpty for existence check const toProcess = Math.min(maxItems, queue.size()); for (let i = 0; i < toProcess; i++) { results.push(queue.extractMin()); } return results;} function reportQueueHealth<T>(queue: PriorityQueue<T>): string { if (queue.isEmpty()) { return "Queue is empty - no pending work"; } const size = queue.size(); if (size < 10) { return `Queue healthy: ${size} items`; } else if (size < 100) { return `Queue growing: ${size} items - consider scaling`; } else { return `Queue critical: ${size} items - immediate attention needed`; }} // Pattern: Guard clause with isEmptyfunction processNext<T>(queue: PriorityQueue<T>): T | null { if (queue.isEmpty()) { return null; // Early return for empty queue } return queue.extractMin();} // Pattern: Batch size calculationfunction calculateBatchSize<T>( queue: PriorityQueue<T>, preferredBatch: number): number { // Take preferred batch size or remaining items, whichever is smaller return Math.min(preferredBatch, queue.size());} // Pattern: Progress reportingfunction processWithProgress<T>( queue: PriorityQueue<T>, process: (item: T) => void): void { const total = queue.size(); let processed = 0; while (!queue.isEmpty()) { process(queue.extractMin()); processed++; // Report progress every 10% if (processed % Math.ceil(total / 10) === 0) { console.log(`Progress: ${processed}/${total} (${ Math.round(processed / total * 100) }%)`); } } console.log("Processing complete!");}In concurrent environments, checking isEmpty then extracting is NOT atomic. Another thread could extract between your check and operation. Use thread-safe priority queue implementations or proper synchronization. Some libraries offer atomic tryPop or pollWithTimeout operations.
Beyond the core operations, many priority queue implementations offer extended operations that enable more sophisticated use cases. These are often crucial for specific algorithms.
decreaseKey (or increasePriority):
This operation lowers the priority value of an existing element (which, in a min-heap, means it becomes more important). It's essential for algorithms like Dijkstra's shortest path.
decreaseKey(element, newPriority) → void
Time Complexity:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
/** * Indexed Priority Queue with full extended operations */class IndexedPriorityQueue<K, V> { private heap: Array<{ key: K; value: V; priority: number }> = []; private keyToIndex: Map<K, number> = new Map(); // Core operations... insert(key: K, value: V, priority: number): void { /* ... */ } extractMin(): { key: K; value: V; priority: number } | undefined { /* ... */ } peek(): { key: K; value: V; priority: number } | undefined { /* ... */ } isEmpty(): boolean { return this.heap.length === 0; } size(): number { return this.heap.length; } /** * Decrease the priority of an existing element. * Critical for Dijkstra's algorithm. * * @throws Error if key not found or new priority is not lower */ decreaseKey(key: K, newPriority: number): void { const index = this.keyToIndex.get(key); if (index === undefined) { throw new Error(`Key not found: ${String(key)}`); } const currentPriority = this.heap[index].priority; if (newPriority >= currentPriority) { throw new Error( `New priority (${newPriority}) must be less than ` + `current (${currentPriority})` ); } this.heap[index].priority = newPriority; this.bubbleUp(index); // Priority improved, might need to rise } /** * Increase the priority of an existing element. * Less common, but useful for aging or demotion. */ increaseKey(key: K, newPriority: number): void { const index = this.keyToIndex.get(key); if (index === undefined) { throw new Error(`Key not found: ${String(key)}`); } const currentPriority = this.heap[index].priority; if (newPriority <= currentPriority) { throw new Error( `New priority (${newPriority}) must be greater than current (${currentPriority})` ); } this.heap[index].priority = newPriority; this.bubbleDown(index); // Priority worsened, might need to sink } /** * Check if a key exists in the queue. */ contains(key: K): boolean { return this.keyToIndex.has(key); } /** * Remove an arbitrary element (not just the minimum). */ remove(key: K): boolean { const index = this.keyToIndex.get(key); if (index === undefined) { return false; // Key not found } // Strategy: decrease key to -Infinity, then extract // This is cleaner than handling the general remove case this.heap[index].priority = Number.NEGATIVE_INFINITY; this.bubbleUp(index); // Now at root this.extractMin(); // Remove it return true; } /** * Get the current priority of a key. */ getPriority(key: K): number | undefined { const index = this.keyToIndex.get(key); return index !== undefined ? this.heap[index].priority : undefined; } /** * Insert or update: if key exists, update priority; else insert. */ upsert(key: K, value: V, priority: number): void { if (this.contains(key)) { const currentPriority = this.getPriority(key)!; if (priority < currentPriority) { this.decreaseKey(key, priority); } else if (priority > currentPriority) { this.increaseKey(key, priority); } // If equal, nothing to do } else { this.insert(key, value, priority); } } // Helper methods for heap maintenance private bubbleUp(index: number): void { /* ... */ } private bubbleDown(index: number): void { /* ... */ } private swap(i: number, j: number): void { /* ... */ }}Dijkstra's Algorithm — Why decreaseKey Matters:
In Dijkstra's shortest path algorithm, when we find a shorter path to a vertex, we need to update its tentative distance in the priority queue. Without decreaseKey:
With decreaseKey in an indexed heap:
This is why indexed priority queues (where we can look up elements by key in O(1)) are used for graph algorithms.
Not all standard library priority queues support decreaseKey. Java's PriorityQueue does not. Python's heapq is just functions on lists—no decreaseKey. You may need to implement an indexed priority queue yourself or use a third-party library for algorithms requiring this operation.
Different programming languages provide different APIs for priority queues. Understanding these variations helps you work effectively across languages and choose appropriate implementations.
Key Differences to Watch:
| Language | Class/Module | Default Order | Insert | Extract | Peek |
|---|---|---|---|---|---|
| Python | heapq | Min-heap | heappush(h, x) | heappop(h) | h[0] |
| Java | PriorityQueue | Min-heap | add(e) / offer(e) | poll() / remove() | peek() |
| C++ | priority_queue | Max-heap | push(x) | pop() + top() | top() |
| C# | PriorityQueue<T,P> | Min-heap | Enqueue(e, p) | Dequeue() | Peek() |
| Go | container/heap | Min-heap* | Push(h, x) | Pop(h) | h[0] / (*h)[0] |
| Rust | BinaryHeap | Max-heap | push(x) | pop() | peek() |
1234567891011121314151617181920212223242526272829303132333435363738394041
// JavaScript/TypeScript has NO built-in priority queue!// Common options:// 1. Implement your own// 2. Use a library (e.g., @datastructures-js/priority-queue) // Example with @datastructures-js/priority-queueimport { MinPriorityQueue, MaxPriorityQueue } from '@datastructures-js/priority-queue'; // Min priority queue (number elements)const minQueue = new MinPriorityQueue<number>();minQueue.enqueue(5);minQueue.enqueue(2);minQueue.enqueue(8); console.log(minQueue.dequeue()); // { priority: 2, element: 2 }console.log(minQueue.front()); // { priority: 5, element: 5 } // With custom priority functionconst taskQueue = new MinPriorityQueue<Task>(task => task.priority);taskQueue.enqueue({ id: "1", priority: 5 });taskQueue.enqueue({ id: "2", priority: 2 }); // Simple array-based implementation for quick needsclass SimplePQ<T> { private items: Array<{ element: T; priority: number }> = []; enqueue(element: T, priority: number): void { this.items.push({ element, priority }); this.items.sort((a, b) => a.priority - b.priority); } dequeue(): T | undefined { return this.items.shift()?.element; } // Note: O(n log n) insert due to sorting - not efficient! // Use proper heap for production}In C++ STL, priority_queue::pop() returns void, not the element. You must call top() first to get the value, then pop() to remove it. This design allows exception safety—if copying the return value throws, the queue state is unchanged.
We've comprehensively covered the priority queue interface—the operations that define this abstract data type regardless of implementation. Let's consolidate our understanding.
What's Next:
With the interface fully specified, it's natural to ask: how do we implement this interface efficiently? The next page previews the heap data structure—the elegant, array-based tree that makes priority queues practical. This preview sets the stage for the dedicated Heaps chapter where we'll explore heaps in complete detail.
You now have a complete understanding of the priority queue interface. You know every operation—its semantics, time complexity, edge cases, and proper usage patterns. This knowledge enables you to use priority queues effectively in any programming language and prepares you for understanding their implementation.