Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Heap-based Priority Queues and Top-K Problems

Recommended resources

Hello! Let's dive into one of the most practical applications of the heap data structure.

In our previous lessons, you've mastered the mechanics of a binary heap. You learned how to efficiently construct one from an array in time and how to maintain its properties using heapify (sift-down) operations. You then applied this to build heap sort, an efficient, in-place sorting algorithm.

Today, we'll build on that foundation to explore the Priority Queue. You will learn to implement a priority queue using a binary heap and solve top-K element problems. A priority queue is an abstract data type that's a cornerstone in many algorithms, and the "Top K" pattern is one of the most common problem types you'll encounter in technical interviews.

1. What is a Priority Queue?

A standard queue follows a First-In, First-Out (FIFO) principle. A Priority Queue (PQ) is different: when you dequeue an element, you don't get the one that has been waiting the longest, but the one with the highest (or lowest) priority.

The main operations are:

  • add(element) or enqueue(element): Adds an element to the queue.
  • remove() or dequeue(): Removes and returns the element with the highest priority.
  • peek(): Returns the highest priority element without removing it.

Think about a hospital emergency room: patients are treated based on the severity of their condition (priority), not just their arrival time. This is a perfect analogy for a priority queue.

So, how can we build one?

  • Unsorted Array/List: add is fast (), but finding the highest priority element to remove requires a full scan ().
  • Sorted Array/List: remove is fast (), but add requires finding the correct position and shifting elements ().

Neither is ideal. We need a structure that balances both operations. This is where the heap shines. A binary heap is the perfect underlying data structure for a priority queue, offering time for both insertion and deletion.

  • A Min-Heap naturally implements a Min-Priority Queue (where remove returns the smallest element).
  • A Max-Heap naturally implements a Max-Priority Queue (where remove returns the largest element).

The video below gives an excellent introduction to why heaps are the right choice for this job.

Introduction to Heap Data Structure + Priority Queue + Heapsort Tutorial

The video 'Introduction to Heap Data Structure + Priority Queue' by Kunal Kushwaha explains the motivation for using heaps to efficiently retrieve the element with the highest or lowest priority.

Watch the first part from 02:00 to 10:15, which discusses why we need a data structure like a heap for fast retrieval of conditional items. Then, skip ahead and watch from 54:48 to 58:03, where he explicitly defines what a Priority Queue is and why the heap implementation is superior to a linked list approach.


2. Priority Queue Operations via Heap Operations

The core operations of a priority queue map directly to the heap operations you already know.

  • add(element): This is equivalent to inserting an element into the heap. We add the element to the end of our array (maintaining the complete binary tree shape) and then perform an upheap (also called sift-up or swim-up) operation to restore the heap property.
  • remove(): This is equivalent to removing the root of the heap. We swap the root with the last element, remove the old root (which is now at the end), and then perform a downheap (also called sift-down or heapify) on the new root to restore the heap property.
  • peek(): This is a simple operation that just returns the element at the root of the heap (index 0 of the array).

The following video segments and image illustrate these fundamental mechanics.

Introduction to Heap Data Structure + Priority Queue + Heapsort Tutorial

Let's revisit the same video to see the upheap and downheap operations demonstrated. This will solidify how add and remove work in a heap-based priority queue.

Watch the 'insert' explanation from 21:34 to 28:12, which shows the upheap process. Then, watch the 'remove' explanation from 28:12 to 32:09, which demonstrates the downheap process.

Heap Operations: Insert and Remove Maximum
This diagram illustrates the core mechanics of a max-heap. **Insertion** (top row) uses the 'swim-up' operation, equivalent to a PQ's `add`. **Removing the maximum** (bottom row) uses the 'sink-down' operation, equivalent to a PQ's `remove`.

3. Using Java's PriorityQueue

For interviews and competitive programming, you'll rarely implement a priority queue from scratch. Instead, you'll use Java's built-in java.util.PriorityQueue. It's crucial to know how to use it effectively.

  • By default, PriorityQueue is a Min-Heap. This means poll() will always return the smallest element.
  • To create a Max-Heap, you need to provide a custom Comparator. The easiest way is using Collections.reverseOrder() or a lambda expression (a, b) -> b - a.

The article below provides excellent, concise code snippets for common PriorityQueue use cases.

Mastering the Top ‘K’ Elements Pattern – With Java Code

The article 'Mastering the Top ‘K’ Elements Pattern' from Dev.to shows how to use Java's PriorityQueue as both a min-heap and a max-heap.

Read the sections 'Key Data Structure – Heap (PriorityQueue)' and 'Bonus – Custom MaxHeap in Java'. This will show you the standard syntax for creating both min-heaps and max-heaps.

Here's a quick summary of the code:

// Min-heap (default behavior)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();

// Max-heap (using a lambda comparator)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);

// Max-heap (using Collections.reverseOrder())
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>(Collections.reverseOrder());

// Key methods:
minHeap.offer(10); // Add an element
minHeap.add(5);    // Same as offer

int smallest = minHeap.poll(); // Remove and return the head (5)
int nextSmallest = minHeap.peek(); // Get the head without removing (10)

4. The "Top K Elements" Pattern

Now for the main application. A very common class of interview problems asks you to find the "Top K" largest, smallest, or most frequent items in a large dataset.

The naive approach is to sort the entire dataset () and pick the first or last K elements. A priority queue offers a much better solution with time complexity.

The logic is clever and a bit counter-intuitive at first:

  • To find the K largest elements, use a Min-Heap of size K.
  • To find the K smallest elements, use a Max-Heap of size K.

Let's see why this works for finding the K largest elements:

  1. Initialize a min-heap of size K.
  2. Iterate through all N elements of the input array.
  3. For each element, offer it to the min-heap.
  4. If the heap's size grows larger than K, poll() an element. Since it's a min-heap, you are removing the smallest element from the heap.
  5. After iterating through all N elements, the heap contains exactly the K largest elements from the entire array.

This process acts as a filter. The heap maintains the "top K candidates" seen so far. When a new, larger element arrives, it's added, and the smallest of the candidates is kicked out, ensuring the K largest are always retained.

This short video explains the pattern beautifully.

Top K Elements in 6 minutes | LeetCode Pattern

The video 'Top K Elements in 6 minutes' from AlgoMasterIO clearly explains the pattern, its complexity, and the logic of using a min-heap to find the K largest elements.

Watch from the beginning to 03:14. Pay close attention to the explanation of the min-heap approach (starting at 01:57), as this is the most efficient and common way to solve this problem.


5. Applying the Pattern: Solved Problems

Let's put this theory into practice with some classic interview problems.

Problem 1: Top K Frequent Elements (LeetCode 347)

Given an array of integers nums and an integer k, return the k most frequent elements.

This is a two-step problem:

  1. Count Frequencies: Use a HashMap to count the occurrences of each number. This takes time.
  2. Find Top K Frequencies: Use the "Top K" pattern on the frequencies. We want the k elements with the largest frequencies, so we will use a min-heap of size k. The heap will store pairs of (number, frequency), ordered by frequency.

Top K Elements in 6 minutes | LeetCode Pattern

Let's see how the 'Top K Frequent Elements' problem is solved using this pattern.

Watch from 03:29 to 04:45. The video walks through the logic of using a HashMap and then a min-heap.

Here is what the Java implementation looks like. Notice the custom comparator for the PriorityQueue.

public int[] topKFrequent(int[] nums, int k) {
    // 1. Count frequencies
    Map<Integer, Integer> freqMap = new HashMap<>();
    for (int num : nums) {
        freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
    }

    // 2. Use a min-heap to find top k frequent elements
    // The heap stores Map.Entry objects, compared by their value (frequency)
    PriorityQueue<Map.Entry<Integer, Integer>> minHeap = 
        new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue));

    for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
        minHeap.offer(entry);
        if (minHeap.size() > k) {
            minHeap.poll(); // Remove the entry with the lowest frequency
        }
    }

    // 3. Extract the result from the heap
    int[] result = new int[k];
    for (int i = k - 1; i >= 0; i--) {
        result[i] = minHeap.poll().getKey();
    }
    
    return result;
}

Problem 2: K Closest Points to Origin (LeetCode 973)

Given an array of points and an integer k, find the k closest points to the origin (0, 0).

Here, "priority" is the distance from the origin. We want the k points with the smallest distance. Following our pattern, we should use a max-heap of size k.

  1. Initialize a max-heap. The comparator will compare points based on their distance from the origin. To avoid floating-point precision issues and sqrt calculations, we can compare squared distances (x^2 + y^2).
  2. Iterate through the points. For each point, offer it to the max-heap.
  3. If the heap size exceeds k, poll() the point with the largest distance, which is the root of the max-heap.
  4. After the loop, the heap contains the k closest points.

Top K Elements in 6 minutes | LeetCode Pattern

The same video also covers the 'K Closest Points to Origin' problem, demonstrating how to use a max-heap with a custom comparator.

Watch from 04:45 to 05:58. This shows the application of the max-heap for a 'K smallest' type of problem.

Test your understanding!

You are given a stream of incoming stock prices. You need to design a data structure that, at any point, can efficiently return the 10th highest stock price seen so far. Which data structure would you use, and how would you configure it?

Show answer

You would use a min-heap of size 10.

Reasoning:
The problem asks for the 10th highest price, which is a variation of the "Top K" problem where K=10.

  • We need to maintain the 10 largest elements seen so far.
  • A min-heap of size 10 will store these 10 largest elements. The root of the heap (peek()) will always be the smallest of this group, which is precisely the 10th highest price.
  • When a new price arrives, you add it to the heap. If the heap size becomes 11, you poll() the minimum, maintaining the size at 10. This ensures the heap always contains the top 10 prices.

Conclusion

Excellent work! You've not only grasped the concept of a Priority Queue but also learned how to apply it to a powerful and common interview pattern.

Key Takeaways:

  • A Priority Queue is an abstract data type that serves elements based on priority, not arrival order.
  • The Binary Heap is the ideal data structure for implementing a Priority Queue, providing for insertions and deletions.
  • Java's java.util.PriorityQueue is a min-heap by default but can be converted to a max-heap with a custom Comparator.
  • The "Top K Elements" pattern efficiently solves problems needing the K largest/smallest items in time.
    • To find the K largest items, use a min-heap of size K.
    • To find the K smallest items, use a max-heap of size K.

Preview of the Next Lesson:
We will now shift our focus to another incredibly important data structure for interview preparation: the Hash Table. In the next lesson, we will explore the fundamentals of hashing, including hash functions and collision resolution strategies like chaining and open addressing. You'll see how hashing enables constant-time average performance for lookups, insertions, and deletions.