Introduction
Heaps and priority queues are powerful data structures that efficiently maintain ordered collections where you frequently need to access the minimum or maximum element. From finding top K elements to implementing Dijkstra’s algorithm, these structures are essential tools in every programmer’s toolkit.
In this comprehensive guide, we’ll explore classic heap and priority queue problems with clean, optimized solutions in both C# and Python.
What is a Heap?
A heap is a specialized tree-based data structure that satisfies the heap property:
Min Heap: Parent node is always smaller than or equal to its children (root is minimum)
Max Heap: Parent node is always greater than or equal to its children (root is maximum)
Key Operations
- Insert: Add element while maintaining heap property – O(log n)
- Extract Min/Max: Remove and return root element – O(log n)
- Peek: View root element without removing – O(1)
- Heapify: Convert array into heap – O(n)
Common Use Cases
- Finding top K elements efficiently
- Priority-based task scheduling
- Graph algorithms (Dijkstra’s, Prim’s)
- Median maintenance in streaming data
- Merge K sorted lists
Problem 1: Kth Largest Element in a Stream
Difficulty: Easy
Tags: Heap, Design, Priority Queue
Problem Statement
Design a class to find the kth largest element in a stream. Note that it is the kth largest element in sorted order, not the kth distinct element.
Implement KthLargest class:
KthLargest(int k, int[] nums): Initializes the object with integer k and stream of integers numsint add(int val): Appends integer val to the stream and returns the kth largest element
Example
Input:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
Output:
[null, 4, 5, 5, 8, 8]
Approach: Min Heap of Size K
The key insight is to maintain a min heap of exactly K elements. The root of this heap will always be the kth largest element because:
- The heap contains the K largest elements seen so far
- The smallest of these K elements is the kth largest overall
C# Solution
public class KthLargest
{
private PriorityQueue<int, int> minHeap;
private int k;
public KthLargest(int k, int[] nums)
{
this.k = k;
this.minHeap = new PriorityQueue<int, int>();
// Add all initial numbers
foreach (int num in nums)
{
Add(num);
}
}
public int Add(int val)
{
// Add new value to heap
minHeap.Enqueue(val, val);
// Keep only k largest elements
if (minHeap.Count > k)
{
minHeap.Dequeue();
}
// The root is the kth largest
return minHeap.Peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.Add(val);
*/
Python Solution
import heapq
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.k = k
self.heap = nums
# Transform list into a heap in O(n) time
heapq.heapify(self.heap)
# Keep only k largest elements
while len(self.heap) > k:
heapq.heappop(self.heap)
def add(self, val: int) -> int:
heapq.heappush(self.heap, val)
# Maintain heap size of k
if len(self.heap) > self.k:
heapq.heappop(self.heap)
# Root is the kth largest element
return self.heap[0]
# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)
Time Complexity:
- Constructor: O(n log k) where n is length of nums
- Add: O(log k)
Space Complexity: O(k)
Key Insights
- Min heap keeps K largest elements, smallest at root
- When heap exceeds K elements, remove the minimum
- The kth largest is always at the root of our min heap
- Much more efficient than sorting after each addition
Problem 2: Top K Frequent Elements
Difficulty: Medium
Tags: Heap, Hash Table, Sorting
Problem Statement
Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order.
Example
Input: nums = [1,1,1,2,2,3], k = 2
Output: [1,2]
Input: nums = [1], k = 1
Output: [1]
Approach: Hash Map + Heap
- Count frequencies using a hash map
- Use a heap to find k elements with highest frequencies
C# Solution
public class Solution
{
public int[] TopKFrequent(int[] nums, int k)
{
// Count frequencies
Dictionary<int, int> frequencyMap = new Dictionary<int, int>();
foreach (int num in nums)
{
if (frequencyMap.ContainsKey(num))
{
frequencyMap[num]++;
}
else
{
frequencyMap[num] = 1;
}
}
// Use max heap based on frequency
PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>(
Comparer<int>.Create((a, b) => b.CompareTo(a))
);
foreach (var pair in frequencyMap)
{
maxHeap.Enqueue(pair.Key, pair.Value);
}
// Extract top k elements
int[] result = new int[k];
for (int i = 0; i < k; i++)
{
result[i] = maxHeap.Dequeue();
}
return result;
}
}
Python Solution
from collections import Counter
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# Count frequencies
frequency_map = Counter(nums)
# Use heapq.nlargest to find k elements with highest frequencies
# key function specifies we're comparing by frequency
return heapq.nlargest(k, frequency_map.keys(),
key=lambda x: frequency_map[x])
Time Complexity: O(n + m log k) where n is array length, m is unique elements
Space Complexity: O(m) for the frequency map
Alternative Approach: Bucket Sort
For O(n) time complexity when k is close to number of unique elements:
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
frequency_map = Counter(nums)
# Create buckets where index = frequency
buckets = [[] for _ in range(len(nums) + 1)]
for num, freq in frequency_map.items():
buckets[freq].append(num)
# Collect k most frequent elements
result = []
for i in range(len(buckets) - 1, -1, -1):
result.extend(buckets[i])
if len(result) >= k:
return result[:k]
return result
Problem 3: Find K Pairs with Smallest Sums
Difficulty: Medium
Tags: Heap, Array, Two Pointers
Problem Statement
You are given two integer arrays nums1 and nums2 sorted in ascending order and an integer k.
Define a pair (u, v) which consists of one element from the first array and one element from the second array.
Return the k pairs with the smallest sums.
Example
Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
Output: [[1,2],[1,4],[1,6]]
Explanation: The first 3 pairs are:
[1,2], sum = 3
[1,4], sum = 5
[1,6], sum = 7
Approach: Min Heap with Smart Generation
Instead of generating all pairs (which could be huge), use a min heap to generate pairs on-demand, starting with the smallest possible sums.
C# Solution
public class Solution
{
public IList<IList<int>> KSmallestPairs(int[] nums1, int[] nums2, int k)
{
var result = new List<IList<int>>();
if (nums1.Length == 0 || nums2.Length == 0 || k == 0)
return result;
// Min heap: (sum, index1, index2)
var minHeap = new PriorityQueue<(int i, int j), int>();
// Initialize heap with pairs using first element of nums2
for (int i = 0; i < Math.Min(k, nums1.Length); i++)
{
minHeap.Enqueue((i, 0), nums1[i] + nums2[0]);
}
// Extract k smallest pairs
while (k > 0 && minHeap.Count > 0)
{
var (i, j) = minHeap.Dequeue();
result.Add(new List<int> { nums1[i], nums2[j] });
// Add next pair from nums2 for this nums1 element
if (j + 1 < nums2.Length)
{
minHeap.Enqueue((i, j + 1), nums1[i] + nums2[j + 1]);
}
k--;
}
return result;
}
}
Python Solution
import heapq
class Solution:
def kSmallestPairs(self, nums1: List[int], nums2: List[int],
k: int) -> List[List[int]]:
if not nums1 or not nums2:
return []
# Min heap: (sum, index1, index2)
min_heap = []
# Initialize with pairs using first element of nums2
for i in range(min(k, len(nums1))):
heapq.heappush(min_heap, (nums1[i] + nums2[0], i, 0))
result = []
# Extract k smallest pairs
while k > 0 and min_heap:
sum_val, i, j = heapq.heappop(min_heap)
result.append([nums1[i], nums2[j]])
# Add next pair from nums2 for this nums1 element
if j + 1 < len(nums2):
heapq.heappush(min_heap,
(nums1[i] + nums2[j + 1], i, j + 1))
k -= 1
return result
Time Complexity: O(k log k)
Space Complexity: O(k)
Key Insights
- Don’t generate all possible pairs (could be O(n²))
- Start with smallest possible pairs and expand systematically
- Each extracted pair suggests only one new candidate pair
- Leverages the sorted property of input arrays
Key Takeaways
Heap Patterns
- Top K Elements: Use heap of size K for efficient tracking
- K-way Merge: Use heap to merge multiple sorted sequences
- Running Median: Use two heaps (max heap + min heap)
- Frequency-based: Combine hash map with heap
Choosing Heap Type
- Min Heap for K largest: Keep K largest, remove smallest
- Max Heap for K smallest: Keep K smallest, remove largest
- Both heaps: For median or balanced partitioning
Best Practices
- Consider heap when you need repeated min/max operations
- Heapify is O(n), faster than n insertions for initialization
- Python’s heapq only provides min heap (negate values for max heap)
- C#’s PriorityQueue allows custom comparers
- Always check if heap is empty before peek/pop
When to Use Heaps
Perfect for:
- Finding top/bottom K elements
- Maintaining a running minimum/maximum
- Priority-based processing
- K-way merge operations
Not ideal for:
- When you need to search for arbitrary elements
- Sorted traversal of all elements
- When insert/delete frequency is low
Practice Problems
Ready to master heaps? Try these:
- Merge K Sorted Lists – Classic K-way merge
- Find Median from Data Stream – Two heap technique
- Sliding Window Median – Advanced heap management
- Meeting Rooms II – Interval scheduling with heap
- Reorganize String – Greedy with max heap
Conclusion
Heaps and priority queues are indispensable for efficiently solving problems involving ordered data and top K queries. Understanding when to use min vs max heaps, and how to combine them with other data structures like hash maps, will significantly expand your problem-solving capabilities.
The key is recognizing patterns: when you need repeated access to minimum/maximum elements, think heap!
What’s Next? In our next article, we’ll explore trees and binary search trees, building on our understanding of heap structures.
Happy coding! 🚀
Have questions or suggestions? Drop a comment below!
