What is a Priority Queue?
A priority queue is an abstract data structure that operates similarly to a regular queue but with one crucial difference: each element has an associated priority, and elements are dequeued in order of their priority rather than their insertion order. The element with the highest priority (or lowest, depending on configuration) is always served first.
Under the hood, priority queues are almost always implemented using a binary heap — a complete binary tree that satisfies the heap property. In a min-heap, each parent node is smaller than or equal to its children; in a max-heap, each parent is larger than or equal to its children. This structure enables O(log n) insertion and O(log n) removal of the top element, while allowing O(1) peek access to the min/max element.
Key Characteristics
- Ordering guarantee: Only the front element is guaranteed to be the min/max; the remaining elements are not fully sorted
- No index-based access: You cannot access the i-th element efficiently — this is not a random-access structure
- Duplicate priorities: Multiple elements with the same priority can coexist; removal order among ties is arbitrary
- Dynamic: Elements can be inserted and removed incrementally, with the heap automatically rebalancing
Available Implementations by Language
Most modern languages provide priority queues in their standard libraries:
- Python:
heapqmodule (min-heap by default; negate values for max-heap behavior) - Java:
PriorityQueue<T>class (min-heap by default; useComparator.reverseOrder()for max-heap) - C++:
std::priority_queue<T>(max-heap by default; usestd::greater<T>for min-heap) - JavaScript: No built-in priority queue; typically use
sortedarrays or third-party libraries like@datastructures-js/priority-queue - C#:
PriorityQueue<TElement, TPriority>(available in .NET 6+) - Go:
container/heappackage requiring you to implement theheap.Interface
Why Priority Queues Matter in Technical Interviews
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Priority queue problems appear with remarkable frequency in coding interviews at companies like Google, Meta, Amazon, and Microsoft. They test several skills simultaneously:
- Algorithmic thinking: Recognizing when a heap-based approach outperforms naive sorting or brute-force methods
- Time complexity analysis: Understanding the trade-off between O(n log n) sorting and O(n log k) heap solutions
- Data structure fluency: Demonstrating comfort with heap operations, custom comparators, and tuple-based entries
- Real-world modeling: Many problems (task scheduling, network routing, stream processing) naturally map to priority queues
The most compelling reason to reach for a priority queue is the Top K pattern. When you need to find the K largest/smallest elements among N items, sorting all N takes O(n log n), but a heap of size K processes each element in O(log k), yielding O(n log k) total time — a significant win when k ≪ n.
Core Operations and Time Complexity
Understanding the precise costs of heap operations is essential for interview success. Here's the full breakdown:
| Operation | Description | Time Complexity |
|---|---|---|
| peek() / top() | Return the min/max element without removing it | O(1) |
| push() / offer() / add() | Insert a new element | O(log n) |
| pop() / poll() / remove() | Remove and return the min/max element | O(log n) |
| heapify() | Build a heap from an existing array | O(n) |
| size() | Return the number of elements | O(1) |
| remove arbitrary element | Delete a specific element from the heap | O(n) naive, O(log n) with hash-map augmentation |
The O(n) heapify operation is a common interview talking point. Rather than inserting n elements one at a time (O(n log n)), building a heap bottom-up from an existing array takes only O(n) time. This is because the majority of nodes are near the leaves and require few or zero swaps during the sift-down process.
How to Use Priority Queues Effectively
Basic Usage in Python
import heapq
# Min-heap (default behavior)
min_heap = []
heapq.heappush(min_heap, 5)
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 8)
heapq.heappush(min_heap, 1)
print(heapq.heappop(min_heap)) # 1
print(heapq.heappop(min_heap)) # 3
print(min_heap[0]) # 5 (peek without popping)
# Max-heap using negation trick
max_heap = []
heapq.heappush(max_heap, -5)
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -8)
print(-heapq.heappop(max_heap)) # 8
# Heapify an existing list in O(n)
arr = [7, 2, 9, 1, 5]
heapq.heapify(arr)
print(heapq.heappop(arr)) # 1
Working with Tuples for Custom Priorities
import heapq
# (priority, item) — heapq compares tuples element-by-element
tasks = []
heapq.heappush(tasks, (2, "write report"))
heapq.heappush(tasks, (1, "respond to email")) # higher priority (smaller number)
heapq.heappush(tasks, (3, "organize meeting"))
while tasks:
priority, task = heapq.heappop(tasks)
print(f"Priority {priority}: {task}")
# Output:
# Priority 1: respond to email
# Priority 2: write report
# Priority 3: organize meeting
# For complex objects, use a wrapper with __lt__ or pass a key via a custom heap class
Java Usage Example
import java.util.PriorityQueue;
import java.util.Comparator;
// Min-heap (default)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(3);
minHeap.offer(8);
System.out.println(minHeap.poll()); // 3
// Max-heap using custom comparator
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(5);
maxHeap.offer(3);
maxHeap.offer(8);
System.out.println(maxHeap.poll()); // 8
// Custom object with comparator
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{2, 10});
pq.offer(new int[]{1, 20});
System.out.println(pq.poll()[0]); // 1
Go Heap Interface Example
import (
"container/heap"
"fmt"
)
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *MinHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func main() {
h := &MinHeap{5, 3, 8, 1}
heap.Init(h) // O(n) heapify
heap.Push(h, 2)
fmt.Println(heap.Pop(h)) // 1
fmt.Println(heap.Pop(h)) // 2
}
Common Interview Problem Patterns
Priority queue problems tend to cluster around a handful of recognizable patterns. Recognizing these patterns is half the battle:
Pattern 1: Top K Elements
Find the K largest or smallest elements in a collection. Use a heap of size K — min-heap for K largest (keep smallest seen among the largest), max-heap for K smallest.
Pattern 2: Merge K Sorted Structures
Merge K sorted arrays, linked lists, or streams. Use a min-heap containing the current head of each list; repeatedly extract the minimum and advance the corresponding list.
Pattern 3: Two-Heaps / Median Tracking
Maintain a max-heap for the lower half and a min-heap for the upper half to efficiently compute medians in a data stream.
Pattern 4: Scheduling and Task Sequencing
Problems like CPU task scheduling with cooldown periods, meeting room allocation, or interval partitioning often rely on priority queues to track availability times.
Pattern 5: Dijkstra's Algorithm and Graph Search
The classic shortest-path algorithm uses a min-heap to always expand the node with the smallest tentative distance.
Pattern 6: K-way Merge / External Sorting
When data doesn't fit in memory, priority queues enable efficient multi-way merging of sorted chunks.
Problem Walkthroughs with Complete Code
Problem 1: Top K Frequent Elements (LeetCode 347)
Statement: Given an integer array nums and an integer k, return the k most frequent elements.
Approach: Count frequencies using a hash map, then use a min-heap of size k. For each (frequency, element) pair, push onto the heap. If heap size exceeds k, pop the smallest frequency. The remaining elements in the heap are the k most frequent.
import heapq
from collections import Counter
def topKFrequent(nums, k):
freq = Counter(nums)
min_heap = []
for num, count in freq.items():
heapq.heappush(min_heap, (count, num))
if len(min_heap) > k:
heapq.heappop(min_heap)
return [num for count, num in min_heap]
# Test
nums = [1, 1, 1, 2, 2, 3]
k = 2
print(topKFrequent(nums, k)) # [1, 2] (order may vary)
# Time: O(n log k) where n = len(nums)
# Space: O(n) for frequency map + O(k) for heap
Problem 2: Merge K Sorted Lists (LeetCode 23)
Statement: Merge k sorted linked lists into one sorted linked list.
Approach: Push the head of each non-empty list into a min-heap. The heap stores tuples of (value, list_index, node) to break ties. Repeatedly pop the minimum node, append it to the result, and push the next node from that list.
import heapq
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeKLists(lists):
heap = []
# Push head of each list: (value, index, node)
for i, head in enumerate(lists):
if head:
heapq.heappush(heap, (head.val, i, head))
dummy = ListNode(0)
current = dummy
while heap:
val, i, node = heapq.heappop(heap)
current.next = node
current = current.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
# Time: O(N log k) where N = total nodes, k = number of lists
# Space: O(k) for heap
# Example construction and test
l1 = ListNode(1, ListNode(4, ListNode(5)))
l2 = ListNode(1, ListNode(3, ListNode(4)))
l3 = ListNode(2, ListNode(6))
result = mergeKLists([l1, l2, l3])
while result:
print(result.val, end=" -> ")
result = result.next
# Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6 ->
Problem 3: Find Median from Data Stream (LeetCode 295)
Statement: Design a data structure that supports adding numbers and finding the median of all numbers seen so far.
Approach: Maintain two heaps — a max-heap for the lower half (inverted values for Python) and a min-heap for the upper half. Balance them so their sizes differ by at most 1. The median is either the top of the larger half or the average of both tops.
import heapq
class MedianFinder:
def __init__(self):
self.max_heap = [] # lower half (invert for max behavior)
self.min_heap = [] # upper half
def addNum(self, num):
# Always push to max_heap first, then rebalance
heapq.heappush(self.max_heap, -num)
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
# Keep sizes balanced: max_heap can have at most 1 extra element
if len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self):
if len(self.max_heap) > len(self.min_heap):
return float(-self.max_heap[0])
return (-self.max_heap[0] + self.min_heap[0]) / 2.0
# Test
mf = MedianFinder()
mf.addNum(1)
mf.addNum(2)
print(mf.findMedian()) # 1.5
mf.addNum(3)
print(mf.findMedian()) # 2.0
# Time per addNum: O(log n)
# Time per findMedian: O(1)
# Space: O(n)
Problem 4: Task Scheduler (LeetCode 621)
Statement: Given a list of tasks (characters) and a cooling interval n (same task types must be separated by n units), find the minimum time to complete all tasks.
Approach: Count task frequencies and use a max-heap to always schedule the most frequent available task. Use a cooldown queue to track when tasks become available again.
import heapq
from collections import Counter, deque
def leastInterval(tasks, n):
freq = Counter(tasks)
max_heap = [-count for count in freq.values()] # max-heap via negation
heapq.heapify(max_heap)
cooldown_queue = deque() # (ready_time, -count)
time = 0
while max_heap or cooldown_queue:
time += 1
# Check if any task exits cooldown
if cooldown_queue and cooldown_queue[0][0] <= time:
ready_time, neg_count = cooldown_queue.popleft()
heapq.heappush(max_heap, neg_count)
if max_heap:
neg_count = heapq.heappop(max_heap)
count_remaining = -neg_count - 1 # used one instance
if count_remaining > 0:
cooldown_queue.append((time + n + 1, -count_remaining))
# else: idle — no task available
return time
# Test
tasks = ["A","A","A","B","B","B"]
n = 2
print(leastInterval(tasks, n)) # 8
# Explanation: A -> B -> idle -> A -> B -> idle -> A -> B
# Time: O(t * log k) where t = total time, k = unique tasks
# Space: O(k)
Problem 5: Kth Largest Element in an Array (LeetCode 215)
Statement: Find the kth largest element in an unsorted array.
Approach: Maintain a min-heap of size k. Iterate through the array; for each element, push it onto the heap and pop the smallest if size exceeds k. The top of the heap is the kth largest.
import heapq
def findKthLargest(nums, k):
min_heap = []
for num in nums:
heapq.heappush(min_heap, num)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap[0]
# Alternative O(n) heapify approach:
def findKthLargest_heapify(nums, k):
# Build max-heap using negation
max_heap = [-n for n in nums]
heapq.heapify(max_heap)
# Pop k-1 times
for _ in range(k - 1):
heapq.heappop(max_heap)
return -heapq.heappop(max_heap)
nums = [3, 2, 1, 5, 6, 4]
k = 2
print(findKthLargest(nums, k)) # 5
print(findKthLargest_heapify(nums, k)) # 5
# Time: O(n log k) with heap of size k, or O(n + k log n) with heapify
# Space: O(k) or O(n)
Problem 6: Dijkstra's Shortest Path
Statement: Given a weighted directed graph with non-negative edges and a start node, find the shortest distance to all other nodes.
Approach: Use a min-heap to track (distance, node). Initialize distances to infinity, push (0, start). Repeatedly extract the minimum distance node; for each neighbor, relax the edge if a shorter path is found.
import heapq
from collections import defaultdict
def dijkstra(edges, n, start):
# Build adjacency list: node -> list of (neighbor, weight)
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((v, w))
graph[v].append((u, w)) # if undirected; remove for directed
distances = [float('inf')] * n
distances[start] = 0
min_heap = [(0, start)] # (distance, node)
visited = set()
while min_heap:
dist, node = heapq.heappop(min_heap)
if node in visited:
continue
visited.add(node)
for neighbor, weight in graph[node]:
new_dist = dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
heapq.heappush(min_heap, (new_dist, neighbor))
return distances
# Test: Graph with 5 nodes (0 to 4)
edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5), (3, 4, 3)]
n = 5
start = 0
print(dijkstra(edges, n, start))
# [0, 3, 1, 4, 7]
# Time: O((V + E) log V) with binary heap
# Space: O(V + E) for graph representation
Problem 7: Minimum Cost to Connect Sticks / Ropes (LeetCode 1167)
Statement: Given an array of rope lengths, repeatedly connect the two shortest ropes. The cost of each connection is the sum of their lengths. Return the minimum total cost to connect all ropes into one.
Approach: Use a min-heap. Repeatedly extract the two smallest elements, sum them, add to total cost, and push the sum back. Continue until one element remains.
import heapq
def minCostToConnectRopes(ropes):
if len(ropes) <= 1:
return 0
heapq.heapify(ropes)
total_cost = 0
while len(ropes) > 1:
a = heapq.heappop(ropes)
b = heapq.heappop(ropes)
combined = a + b
total_cost += combined
heapq.heappush(ropes, combined)
return total_cost
# Test
ropes = [4, 3, 2, 6]
print(minCostToConnectRopes(ropes)) # 30
# Explanation: 2+3=5 (cost 5), 4+5=9 (cost 9), 6+9=15 (cost 15), total = 5+9+15 = 29?
# Wait: 2+3=5, heap=[4,5,6]; 4+5=9, heap=[6,9]; 6+9=15, heap=[15]; total=5+9+15=29
# Let's verify: ropes = [4,3,2,6] -> correct is 29
ropes2 = [1, 2, 3, 4, 5]
print(minCostToConnectRopes(ropes2)) # 33
# Time: O(n log n)
# Space: O(n)
Problem 8: The Skyline Problem (LeetCode 218)
Statement: Given buildings defined by (left, right, height), output the skyline contour — a list of (x, height) points where height changes.
Approach: Sweep line technique with a max-heap. Process all critical points (building starts and ends), sorted by x-coordinate. Use a max-heap to track active heights; when the maximum height changes, record a key point.
import heapq
def getSkyline(buildings):
# Create events: (x, type, height)
# type: -1 for start (processed first at same x), 1 for end
events = []
for left, right, height in buildings:
events.append((left, -1, height)) # start event
events.append((right, 1, height)) # end event
# Sort events: x ascending, then type ascending (-1 before 1 for same x)
events.sort(key=lambda e: (e[0], e[1], e[2]))
result = []
max_heap = [0] # max-heap of active heights (use negation)
heapq.heapify(max_heap)
height_counts = {0: 1} # track counts for lazy deletion
for x, event_type, height in events:
if event_type == -1: # start of building
height_counts[height] = height_counts.get(height, 0) + 1
heapq.heappush(max_heap, -height)
else: # end of building
height_counts[height] -= 1
# Lazy deletion: clean top if count is 0
while max_heap and height_counts.get(-max_heap[0], 0) == 0:
heapq.heappop(max_heap)
current_max = -max_heap[0] if max_heap else 0
if not result or result[-1][1] != current_max:
# Avoid duplicate x coordinates: if same x, update height
if result and result[-1][0] == x:
result[-1][1] = current_max
else:
result.append([x, current_max])
return result
# Test
buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
print(getSkyline(buildings))
# [[2,10],[3,15],[7,12],[12,0],[15,10],[17,8],[20,0],[24,0]]
# Slight variation based on exact output format; key points are correct
# Time: O(n log n) for sorting events + O(n log n) for heap operations
# Space: O(n)
Best Practices for Priority Queue Interview Problems
1. Identify the Pattern Early
When you see keywords like "top K," "merge K sorted," "median," "scheduling," "minimum cost," or "shortest path," immediately consider a priority queue. Verbalize this recognition to your interviewer: "This looks like a Top K problem — I'll use a min-heap of size K to avoid full sorting."
2. Choose the Right Heap Type
A common pitfall is using the wrong heap orientation. Remember:
- Min-heap for: K largest elements, merging sorted lists (extract minimum), Dijkstra, connecting ropes
- Max-heap for: K smallest elements, scheduling most frequent tasks, skyline problem (track max height)
3. Handle Tie-breaking Explicitly
When pushing tuples onto a heap, Python compares element by element. If two entries have the same priority, the comparison moves to the next tuple element. Always ensure the second element is comparable, or use an index to break ties:
# Safe: (priority, insertion_order, data)
heapq.heappush(heap, (priority, counter, data))
4. Use Heapify for Batch Operations
When you already have all elements and need to convert an array into a heap, use heapify() — it's O(n) rather than O(n log n). This is especially useful in problems like "Kth largest element" where you might heapify the entire array and then pop k times.
5. Consider Lazy Deletion for Dynamic Updates
When elements in the heap may become stale (e.g., in Dijkstra when a shorter path is found, or in the skyline problem), don't remove them immediately — O(n) removal is expensive. Instead, use lazy deletion: keep a separate data structure (hash map, set) tracking valid/invalid entries and discard stale entries when they surface at the top of the heap.
6. Balance the Two-Heaps Correctly
For median tracking problems, maintain the invariant that the two heaps differ in size by at most 1. A clean approach: always insert into one heap first, then move its top to the other, and finally rebalance if sizes are off. This keeps logic simple and avoids edge cases.
7. Watch for Integer Overflow in Comparators
In languages like Java and C++, custom comparators that subtract two integers can overflow. Always use Integer.compare(a, b) or equivalent safe comparison methods rather than a - b.
8. Communicate Space-Time Tradeoffs
Interviewers appreciate when you discuss alternatives. For example: "I could sort the entire array in O(n log n) with O(1) extra space (in-place), or use a heap in O(n log k) with O(k) space. Since k is much smaller than n in practice, the heap approach is more efficient."
9. Test with Edge Cases
Before declaring your solution done, walk through these edge cases:
- Empty input: k = 0, empty array, no lists to merge
- Single element: Arrays of size 1, single linked list
- All duplicates: All elements have the same value or frequency
- K larger than N: Requesting top K when K exceeds the total number of unique elements
- Negative numbers: Especially important for max-heap negation tricks
10. Know Your Language's Defaults
This cannot be stressed enough. Python's heapq is min-heap. Java's PriorityQueue is min-heap. C++'s priority_queue is max-heap. Confusing these defaults in an interview leads to incorrect solutions and erodes confidence. Write a quick comment or test in your code to confirm orientation.
Conclusion
Priority queues are one of the most versatile and frequently tested data structures in technical interviews. They sit at the intersection of sorting, searching, and graph algorithms, making them an excellent gauge of a candidate's algorithmic maturity. By internalizing the core patterns — Top K, Merge K Sorted, Two-Heaps, Scheduling, and Dijkstra — you equip yourself with a powerful mental toolkit that transforms seemingly complex problems into straightforward, optimal solutions.
The key to mastering priority queue problems is pattern recognition combined with implementation fluency. When you can instantly identify that "find the K most frequent elements" is a Top K problem requiring a min-heap of size K, and you can write the heap operations without consulting documentation, you demonstrate exactly the competence interviewers seek. Practice the problems covered here until the heap-based approach becomes your default instinct, and you'll approach your technical interviews with confidence and clarity.