Skip to main content

Welcome to a Grasp lesson

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

Lesson 37

Heap-based Priority Queues

Recommended resources

Hello! Welcome back to our module on Heaps and Priority Queues.

In the last lesson, we dug into the mechanics of heaps, understanding their core properties and implementing the essential _sift_up and _sift_down helper methods. We established that these operations allow us to maintain the heap property in time.

Today, we'll put that foundation to use. We are going to implement a Priority Queue, an abstract data structure that is fundamental to many algorithms and a common feature in technical interviews. By the end of this lesson, you will see how a heap provides a highly efficient underlying structure for a priority queue.

1. What is a Priority Queue?

A standard queue operates on a "first-in, first-out" (FIFO) principle. A Priority Queue (PQ) is different: it's an abstract data type where each element has an associated "priority." When you extract an element, you always get the one with the highest priority, regardless of when it was added.

  • In a min-priority queue, the element with the lowest value has the highest priority.
  • In a max-priority queue, the element with the highest value has the highest priority.

This concept appears everywhere:

  • Operating Systems: Scheduling processes based on their priority level.
  • Networking: Routing data packets that have urgent flags.
  • AI: In algorithms like A* search, a priority queue is used to explore the most promising paths first.
Priority Queue Representation using a Heap
CAPTION: This image shows how a set of tasks, each with a priority, can be organized into a max-heap. The task with the highest priority (`10`) is at the root, making it instantly accessible. This is the core idea behind using a heap to implement a priority queue.

The main operations of a priority queue are:

  1. Insert/Enqueue: Add a new element to the queue.
  2. Extract-Highest-Priority/Dequeue: Remove and return the element with the highest priority.
  3. Peek: Look at the highest-priority element without removing it.

The critical question for an interview is: what's the most efficient way to build this?


2. Why a Heap is the Ideal Implementation

Let's consider other data structures you know. How would they perform as a priority queue?

  • Unsorted Array/List: Insertion is . But to find the highest-priority item, you must search the entire list, which takes time.
  • Sorted Array/List: Finding the highest-priority item is (it's at one of the ends). However, inserting an element while keeping the array sorted requires shifting elements, which takes time.

Neither of these is ideal. We want our operations to be faster. This is where heaps shine. To understand the motivation better, let's watch a video that frames this problem perfectly.

Introduction to Heap Data Structure + Priority Queue + Heapsort Tutorial

This video from Kunal Kushwaha does an excellent job of explaining why we need heaps for this problem. He contrasts it with other data structures to show the performance trade-offs.

Watch the segment from 02:00 to 10:15. Focus on the core argument: how can we get the highest/lowest priority item in O(1) time while keeping insertions efficient? This section sets up the exact problem that heaps solve.

As the video argues, a heap provides the perfect balance:

  • Peek: , because the highest-priority element is always the root.
  • Insert: .
  • Extract: .

This is a massive improvement over the complexities of the array-based approaches.


3. Implementing a Priority Queue with Our Heap

Let's now complete the MinHeap class we started in the previous lesson to make it a fully functional min-priority queue. We'll add the public-facing push (insert) and pop (extract-min) methods that use the _sift_up and _sift_down helpers you've already learned.

# Building on the class from the previous lesson
class MinPriorityQueue:
    def __init__(self):
        # The heap is stored as a list (dynamic array)
        self.heap = []

    def push(self, val):
        """Adds an element to the priority queue. O(log N)"""
        # 1. Add the element to the end to maintain the 'complete tree' shape.
        self.heap.append(val)
        # 2. Sift it up to restore the heap property.
        self._sift_up(len(self.heap) - 1)

    def pop(self):
        """Removes and returns the element with the highest priority (the minimum). O(log N)"""
        if not self.heap:
            return None
        
        # 1. The root is the min element we need to return.
        min_val = self.heap[0]
        
        # 2. To fill the hole at the root, move the last element there.
        last_val = self.heap.pop() # Removes and returns the last element
        
        # If there are still elements left after popping...
        if self.heap:
            self.heap[0] = last_val
            # 3. Sift the new root down to restore the heap property.
            self._sift_down(0)
            
        return min_val

    def peek(self):
        """Returns the smallest element without removing it. O(1)"""
        return self.heap[0] if self.heap else None

    # --- Helper methods from the previous lesson ---

    def _parent_index(self, i):
        return (i - 1) // 2

    def _sift_up(self, i):
        # Iterative implementation is common in interviews
        parent_idx = self._parent_index(i)
        while i > 0 and self.heap[i] < self.heap[parent_idx]:
            self.heap[i], self.heap[parent_idx] = self.heap[parent_idx], self.heap[i]
            i = parent_idx
            parent_idx = self._parent_index(i)

    def _sift_down(self, i):
        while True:
            smallest = i
            left_child_idx = 2 * i + 1
            right_child_idx = 2 * i + 2

            if left_child_idx < len(self.heap) and self.heap[left_child_idx] < self.heap[smallest]:
                smallest = left_child_idx
            
            if right_child_idx < len(self.heap) and self.heap[right_child_idx] < self.heap[smallest]:
                smallest = right_child_idx

            if smallest != i:
                self.heap[i], self.heap[smallest] = self.heap[smallest], self.heap[i]
                i = smallest
            else:
                break # The heap property is restored for this subtree

This class is now a complete implementation of a min-priority queue. To see a slightly different but equally valid implementation and review the core logic, you can study the following resource.

Heaps and Priority Queues: A Complete Guide

This article provides another clean, from-scratch implementation of a Min-Heap in Python. Seeing the same logic in a different presentation can help solidify your understanding.

Read the section titled 'Building Your Own Heap: A Min-Heap Implementation from Scratch'. Compare its push and pop methods with the ones we just built. Notice how they both rely on heapify-up (sift-up) and heapify-down (sift-down) to function.

Test your understanding!

You are given an empty MaxPriorityQueue and the following sequence of operations: push(10), push(20), push(5), pop(), push(15), pop(). What is the value returned by the second pop() operation?

Show answer

Let's trace the state of the max-heap (represented as an array):

  1. push(10): [10]
  2. push(20): [10, 20] -> Sift-up 20 -> [20, 10]
  3. push(5): [20, 10, 5] (no sift-up needed)
  4. pop():
    • The max value 20 is to be returned.
    • The last element 5 moves to the root: [5, 10].
    • Sift-down 5: Swap with larger child 10 -> [10, 5].
    • Returns 20. Heap is now [10, 5].
  5. push(15): [10, 5, 15] -> Sift-up 15 -> [15, 5, 10]
  6. pop():
    • The max value 15 is to be returned.
    • The last element 10 moves to the root: [10, 5].
    • Sift-down 10: It's already larger than its child 5. No swap needed.
    • Returns 15.

The value returned by the second pop() operation is 15.


4. Priority Queues in Standard Libraries

While knowing how to build a PQ from scratch is vital for demonstrating deep understanding in an interview, you'll almost always use a built-in library for actual problem-solving.

Python: The heapq Module

Python's standard library provides the heapq module, which implements a min-heap. You use it directly on a standard Python list.

A key interview trick is to implement a max-heap using heapq. Since heapq is a min-heap, it always keeps the smallest element at the top. To simulate a max-heap, you can simply insert the negation of your numbers. The smallest negative number will correspond to the largest positive number.

The following resource gives a practical guide to using heapq for both min- and max-priority queues.

How to Use a Priority Queue in Python

This guide from DigitalOcean covers the practical use of Python's heapq module. It's essential for interview preparation.

Read the sections 'How to Implement a Priority Queue Using heapq', 'What is a Min-Heap vs Max-Heap?', and 'How to Implement a Max-Heap using heapq?'. Pay close attention to the trick of using negative values to simulate a max-heap.

Java: The PriorityQueue Class

For your Java proficiency, it's good to know that Java has a PriorityQueue class built-in.

PriorityQueue in Java – Min-Heap Implementation Explained
CAPTION: A summary of Java's `PriorityQueue` class. It's a min-heap by default, uses methods like `offer()` and `poll()`, and can be configured as a max-heap by providing a custom `Comparator`.

By default, it's a min-priority queue. To make it a max-priority queue, you provide a Comparator that reverses the natural ordering. For example: new PriorityQueue<>(Collections.reverseOrder()); or new PriorityQueue<>((a, b) -> b - a);.


Conclusion

You've successfully connected the low-level heap mechanics to the high-level concept of a priority queue. This is a critical link to understand for solving a wide range of algorithmic problems.

Key Takeaways:

  • A Priority Queue is an abstract data type that serves elements based on priority, not insertion order.
  • A heap is the most efficient data structure for implementing a priority queue, offering for insertions and extractions, and for peeking.
  • The heap's sift-up and sift-down operations are the engines that power the priority queue's push and pop methods.
  • For interviews, you should be familiar with both the from-scratch implementation and the standard library versions like Python's heapq and Java's PriorityQueue.

In our next lesson, we will apply what we've learned to a classic and very common interview pattern: solving "Top K" element problems using heaps. You'll see just how powerful this data structure can be.