Skip to main content

Welcome to a Grasp lesson

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

Lesson 40

Heaps and Priority Queues

Recommended resources

Hello! Welcome to the final lesson in our "Core Data Structures" module.

In the last lesson, we built a hash table, a data structure optimized for lightning-fast average-case insertion, deletion, and search operations. We saw how a clever mapping function (the hash function) can turn a key into an array index for O(1) lookups.

Today, we'll explore another specialized tree-based data structure: the heap. Heaps are the perfect tool for building an efficient priority queue, an abstract data structure that's crucial in many algorithms, such as scheduling tasks in an operating system, finding the shortest path in a graph (like in GPS navigation), and more.

Your learning outcome for this lesson is to: Implement a heap and use it to build a priority queue.

We will cover:

  • The difference between a regular queue and a priority queue.
  • What a heap is, its core properties (heap property and complete binary tree), and how it's represented using an array.
  • The essential heap operations: insert (sift-up) and extract_max (sift-down).
  • How to implement a heap in Python and use it as an efficient priority queue.

1. What is a Priority Queue?

You're familiar with a standard queue, which operates on a "First-In, First-Out" (FIFO) principle. A priority queue is different. Instead of serving elements based on their arrival time, it serves them based on their priority.

A priority queue is an abstract data type with two main operations:

  • Enqueue (Insert): Add an element to the queue with an associated priority.
  • Dequeue (Extract): Remove the element with the highest priority from the queue.

If multiple elements share the same highest priority, they are typically served in the order they were added.

You could implement a priority queue with a simple list, but it would be inefficient. A heap is the ideal data structure for this job because it provides a great balance of performance for both insertion and extraction.

Priority Queue Data Structure

To understand why heaps are so well-suited for this, let's look at a quick comparison. This article from Programiz clearly defines a priority queue and presents a table comparing the time complexities of different implementations.

Read the sections 'What is a Priority Queue?', 'Difference between Priority Queue and Normal Queue', and 'Implementation of Priority Queue'. Pay close attention to the complexity comparison table. Notice how the Binary Heap offers efficient logarithmic time for both insert and delete operations.

As you saw in the article, a Binary Heap provides time for both insertion and deletion, making it far more efficient than a linked list or an unsorted array for a large number of elements.


2. The Heap Data Structure

A heap is a specialized binary tree that satisfies two key properties:

  1. Heap Property: For a Max-Heap, the value of any parent node is greater than or equal to the values of its children. This ensures the largest element is always at the root. For a Min-Heap, the parent's value is less than or equal to its children's values, keeping the smallest element at the root. We will focus on the Max-Heap.

  2. Completeness Property: A heap must be a complete binary tree. This means all levels of the tree are fully filled, except possibly for the last level, which is filled from left to right without any gaps.

The following video provides an excellent introduction to these concepts.

2.6.3 Heap - Heap Sort - Heapify - Priority Queues

This video from Abdul Bari's highly-regarded series on algorithms clearly defines and illustrates the two types of heaps and their properties.

Watch from 14:11 to 16:13. This short segment will give you a solid definition of Max-Heaps and Min-Heaps and reinforce the 'complete binary tree' property.

Array Representation

The 'completeness' property is brilliant because it allows us to represent a heap compactly in an array, without needing pointers like in other tree structures. The mapping is done by level-order traversal.

Max-Heap Tree and Array Representation
This image shows how a Max-Heap, which is conceptually a tree, is stored in a simple array. The nodes are placed in the array level by level, from left to right. This structure allows us to easily calculate the position of a node's parent and children.

For a node at index i in the array:

  • Its parent is at index: (i - 1) // 2
  • Its left child is at index: 2 * i + 1
  • Its right child is at index: 2 * i + 2

This array-based storage is key to the heap's efficiency.


3. Core Heap Operations

To use a heap as a priority queue, we need to be able to insert elements and extract the maximum element while maintaining the heap properties. This is done with two main procedures: sift-up and sift-down.

Insertion and the Sift-Up Operation

When we insert a new element, we must preserve both the completeness and the heap property.

  1. Maintain Completeness: Add the new element to the first available spot, which is at the end of the array. The tree is still complete.
  2. Restore Heap Property: The new element might be larger than its parent, violating the max-heap property. To fix this, we sift it up the tree. We compare the new element with its parent and swap them if the child is larger. We repeat this process until the element reaches the root or its parent is larger than it.
Min-Heap Sift-Up Operation (Enqueue Step)
This image shows the sift-up process in a min-heap. After inserting '1', it is smaller than its parent '5', so they are swapped. The process would continue until the heap property is restored. The logic is identical for a max-heap, but we'd swap if the child is larger than the parent.

Extracting the Maximum and the Sift-Down Operation

The maximum element in a max-heap is always at the root (index 0). Extracting it is also a two-step process.

  1. Remove the Root: To avoid leaving a hole at the root, we swap the root element with the last element in the heap. Then, we can simply pop the last element off the array. The tree is now smaller but still complete.
  2. Restore Heap Property: The new root (the old last element) is likely much smaller than its children, violating the max-heap property. To fix this, we sift it down. We compare the new root with its children and swap it with the larger of the two. We repeat this process until the element is larger than both its children or it becomes a leaf node.

The following video visually demonstrates both the insertion (sift-up) and extraction (sift-down) operations.

Heaps Visually Explained (Priority Queues)

The video 'Heaps Visually Explained' provides a concise and clear animation of the insertion and deletion operations, including the Python code for each.

Watch from 02:02 to 07:00. This covers both the heapify up (sift-up) for insertion and the heapify down (sift-down) for deletion. Pay attention to how the array is manipulated in sync with the tree visualization.

Because the height of a complete binary tree with n nodes is , both the sift-up and sift-down operations take at most time.


4. Implementation: A Heap-Based Priority Queue

Now let's put it all together into a Python class. Our MaxHeap class will effectively be a max-priority queue.

  • insert(item) will correspond to enqueue.
  • extract_max() will correspond to dequeue.
class MaxHeap:
    def __init__(self):
        # Initialize an empty list to store the heap elements.
        self.heap = []

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

    def _left_child_index(self, i):
        return 2 * i + 1

    def _right_child_index(self, i):
        return 2 * i + 2

    def _swap(self, i, j):
        """Swaps two elements in the heap."""
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]

    def _sift_up(self, i):
        """Moves an element up to its correct position."""
        parent = self._parent_index(i)
        # While the node is not the root and is greater than its parent
        while i > 0 and self.heap[i] > self.heap[parent]:
            self._swap(i, parent)
            i = parent
            parent = self._parent_index(i)

    def _sift_down(self, i):
        """Moves an element down to its correct position."""
        max_index = i
        size = len(self.heap)

        while True:
            left = self._left_child_index(i)
            right = self._right_child_index(i)
            
            # Check if left child exists and is greater than current max
            if left < size and self.heap[left] > self.heap[max_index]:
                max_index = left

            # Check if right child exists and is greater than current max
            if right < size and self.heap[right] > self.heap[max_index]:
                max_index = right
            
            # If the largest element is still the current node, we're done
            if i == max_index:
                break
            
            self._swap(i, max_index)
            i = max_index

    def insert(self, value):
        """Inserts a new value into the heap (enqueue)."""
        self.heap.append(value)
        self._sift_up(len(self.heap) - 1)

    def extract_max(self):
        """Removes and returns the maximum element (dequeue)."""
        if not self.heap:
            return None

        size = len(self.heap)
        if size == 1:
            return self.heap.pop()

        # Swap root with the last element
        self._swap(0, size - 1)
        max_value = self.heap.pop()
        
        # Restore the heap property from the root
        self._sift_down(0)
        
        return max_value
    
    def peek_max(self):
        """Returns the maximum element without removing it."""
        return self.heap[0] if self.heap else None

# --- Let's test our Priority Queue ---
pq = MaxHeap()

# Insert elements (enqueue)
print("Inserting 45, 20, 14, 12, 31...")
pq.insert(45)
pq.insert(20)
pq.insert(14)
pq.insert(12)
pq.insert(31)

print("Heap array after insertions:", pq.heap)
print("Highest priority element (peek):", pq.peek_max())

# Extract max element (dequeue)
print("\nExtracting max:", pq.extract_max())
print("Heap array after extraction:", pq.heap)
print("New highest priority element:", pq.peek_max())

print("\nInserting 50...")
pq.insert(50)
print("Heap array after inserting 50:", pq.heap)
print("New highest priority element:", pq.peek_max())

print("\nExtracting max:", pq.extract_max())
print("Extracting max:", pq.extract_max())
print("Final heap array:", pq.heap)
Test your understanding!

Start with an empty MaxHeap. Trace the state of the heap list after performing the following operations in sequence:

  1. pq.insert(10)
  2. pq.insert(5)
  3. pq.insert(17)
  4. pq.insert(8)
  5. pq.insert(25)

What does the final pq.heap list look like? Then, what does it look like after one call to pq.extract_max()?

Show answer

Insertions:

  1. insert(10): [10]
  2. insert(5): [10, 5] (5 is smaller than 10, so no sift-up needed)
  3. insert(17):
    • Append 17: [10, 5, 17]
    • Sift-up 17: swap with parent 10. [17, 5, 10]
  4. insert(8):
    • Append 8: [17, 5, 10, 8]
    • Sift-up 8: compare with parent 5. Swap. [17, 8, 10, 5]
  5. insert(25):
    • Append 25: [17, 8, 10, 5, 25]
    • Sift-up 25: compare with parent 8. Swap. [17, 25, 10, 5, 8]
    • Sift-up 25: compare with parent 17. Swap. [25, 17, 10, 5, 8]

Final heap after insertions: [25, 17, 10, 5, 8]

Extraction:

  1. extract_max():
    • Swap root (25) with last element (8): [8, 17, 10, 5, 25]
    • Pop 25. The heap is now [8, 17, 10, 5].
    • Sift-down the new root (8):
      • Compare 8 with its children 17 and 10. The larger is 17.
      • Swap 8 and 17. [17, 8, 10, 5]
    • The sift-down for 8 is now complete as it has no more children.

Final heap after extraction: [17, 8, 10, 5]


Conclusion

Congratulations on completing the "Core Data Structures" module! You have now implemented a heap and used it to build a priority queue, one of the most useful applications of this data structure.

Key Takeaways:

  • A Priority Queue is an abstract data type that serves elements based on priority, unlike a standard FIFO queue.
  • A Heap is the most efficient data structure for implementing a priority queue, offering time for both insertions and extractions.
  • A heap must satisfy the heap property (max or min) and be a complete binary tree.
  • The completeness property allows a heap to be efficiently stored in an array.
  • Insertion uses a sift-up operation, and extracting the max/min uses a sift-down operation to maintain the heap properties.

Preview of the Next Lesson:

You've built a strong foundation in data structures. Now it's time to use them to solve problems. Our next module, "Algorithms: Sorting, Searching, and Design," will focus on fundamental algorithmic techniques. In the very first lesson, we will start with the basics of searching: you will implement linear search and binary search algorithms and analyze their complexities. This will be your first step into the world of algorithm design and analysis.