Skip to main content

Welcome to a Grasp lesson

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

Lesson 19

Priority Queues for Selection and Scheduling

Recommended resources

Hello! Welcome to your next lesson.

In our previous session, you learned how to implement a priority queue by leveraging the power of a binary heap. You saw that by composing a heap within a PriorityQueue class, we get a clean, abstract interface with efficient O(log n) performance for both enqueuing and dequeuing elements.

Today, we will apply this powerful abstract data type to solve concrete problems. This lesson directly addresses your goal of building an "architectural mindset" by showing you how to recognize problem patterns and map them to the appropriate data structure. Your learning outcome is to apply a priority queue to solve a selection or scheduling problem. We will tackle two classic examples: finding the k-th largest element in a collection and scheduling tasks with cooldowns.

1. The Selection Problem: Finding the k-th Largest Element

A common requirement in data analysis and monitoring is to find the "Top-K" items in a large dataset—for instance, the top 10 most visited pages on a website or the 5 most expensive transactions in a day. The core of this problem is finding the k-th largest element.

Let's formally define the problem: Given an unsorted array of numbers nums and an integer k, find the k-th largest element in the sorted order.

For example, if nums = [3, 2, 1, 5, 6, 4] and k = 2, the sorted array is [1, 2, 3, 4, 5, 6]. The largest element is 6, and the second-largest is 5. So the answer is 5.

A simple brute-force solution would be to sort the array in O(n log n) time and pick the element at index n-k. But can we do better? Since we only need one specific element, sorting the entire array seems wasteful. This is where a priority queue shines.

The Min-Heap Strategy

The most efficient and elegant solution uses a min-heap that is kept at a maximum size of k. This might seem counterintuitive—using a min-heap to find the largest elements—but the logic is sound.

The strategy is as follows:

  1. Create an empty min-heap.
  2. Iterate through each number in the input array.
  3. If the heap has fewer than k elements, push the current number onto it.
  4. If the heap already has k elements, compare the current number with the smallest element in the heap (the heap's root, which is peek() in O(1) time).
    • If the current number is larger than the heap's root, it means the current number belongs in our set of the top k elements seen so far. To make room, we extract_min() from the heap and insert() the current number.
    • If the current number is smaller or equal, we do nothing and move to the next number.
  5. After iterating through all n numbers, the heap contains the k largest elements from the entire array. The root of the min-heap is the smallest of these, which is precisely the k-th largest element.
LeetCode Problem 703: Kth Largest Element in a Stream with Min-Heap Hint
This image illustrates a similar problem, "Kth Largest Element in a Stream." The core logic is identical, and it correctly identifies a min-heap priority queue as the ideal data structure.

This video provides an excellent walkthrough of this strategy, along with the complexity analysis.

Kth Largest Element in an Array - Leetcode 215 - Heaps (Python)

The following video, 'Kth Largest Element in an Array' by Greg Hogg, explains two heap-based solutions. We will focus on the more efficient min-heap approach, which is a classic pattern for solving Top-K problems.

Watch the segment explaining the min-heap version from 06:10 to 10:20. Pay close attention to the logic of why maintaining a min-heap of size K correctly identifies the Kth largest element. Then, watch the code implementation from 10:20 to 11:35 to see how it's done in Python using the heapq library.

To solidify your understanding, the following article provides a clear textual explanation and summary of the key insights.

Leetcode 215. Kth Largest Element in an Array

This article, 'Leetcode 215. Kth Largest Element in an Array,' provides another excellent breakdown of the problem and the min-heap solution.

Read the sections 'Optimal Solution' and 'What We've Learned'. The first section provides the Python code and a clear explanation. The second section summarizes the key takeaways, which are crucial for recognizing this pattern in the future.

Complexity Analysis

  • Time Complexity: O(n log k). We iterate through n elements. For each element, we might perform a heap operation (heappush, heapreplace). Since the heap's size is capped at k, each operation takes O(log k) time.
  • Space Complexity: O(k). We need to store at most k elements in our priority queue.

This is a significant improvement over the O(n log n) sorting solution, especially when k is much smaller than n, which is a common scenario in real-world applications (e.g., finding the top 100 out of a million).

Test your understanding!

You are tasked with finding the 3rd largest element (k=3) in the array [9, 4, 7, 1, 8, 5]. You are using the min-heap strategy. What are the contents of the heap after processing the number 8? And what is the final result?

Show answer
  1. Start with an empty min-heap [].
  2. Process 9: [9] (heap size < 3)
  3. Process 4: [4, 9] (heap size < 3)
  4. Process 7: [4, 9, 7] (heap size is now 3)
  5. Process 1: 1 is not greater than the heap's root (4). Do nothing. Heap: [4, 9, 7].
  6. Process 8: 8 is greater than the heap's root (4). Pop 4 and push 8. The heap becomes [7, 9, 8] after re-heapifying.
    • The contents of the heap after processing 8 are [7, 9, 8].
  7. Process 5: 5 is not greater than the heap's root (7). Do nothing.

After iterating through the array, the final heap is [7, 9, 8]. The root is 7.

Final result: The 3rd largest element is 7.


2. The Scheduling Problem: The Task Scheduler

Priority queues are fundamental to scheduling algorithms. Operating systems use them to decide which process to run next, and network routers use them to prioritize certain types of data packets.

Priority Scheduling Algorithm Illustration
This image illustrates the core concept of priority scheduling, where processes are executed based on an assigned priority. A priority queue is the natural data structure to manage such a system.

Let's explore a concrete problem: the Task Scheduler.
Imagine you have a CPU that needs to process a list of tasks, e.g., ['A', 'A', 'A', 'B', 'B', 'C']. Between processing two identical tasks (e.g., two 'A's), there must be a "cooldown" period of n time units. In these n units, the CPU can either process a different task or remain idle. The goal is to find the minimum total time required to finish all tasks.

The Max-Heap Strategy

To minimize total time, we must minimize idle time. The best way to do this is to adopt a greedy strategy: at any given moment, process the most frequent task that is currently available (i.e., not in a cooldown period).

A max-heap is the perfect data structure to instantly get the "most frequent task."

However, we also need to manage the cooldowns. A simple queue can help us track which tasks are temporarily unavailable and when they will be ready again.

Here is the algorithm:

  1. Count task frequencies: Use a hash map to count the occurrences of each task.
  2. Build a max-heap: Push the frequencies of all tasks into a max-heap. (In Python's heapq, which is a min-heap, you can achieve this by pushing the negative counts).
  3. Initialize a cooldown queue: Create a queue (like Python's deque) to hold tasks that are cooling down. It will store pairs of (remaining_count, available_time).
  4. Simulate time: Loop one time unit at a time.
    a. Increment the current time.
    b. Check the cooldown queue: if the task at the front of the queue has its available_time equal to the current time, pop it and add its count back to the max-heap.
    c. If the max-heap is not empty, pop the highest-frequency task. You've just "processed" it. Decrement its count.
    d. If its count is still greater than 0, add it to the cooldown queue with an available_time of current_time + n.
    e. If the max-heap was empty, the CPU was idle for this time unit.
  5. Continue the loop until both the max-heap and the cooldown queue are empty. The final time is your answer.

This algorithm elegantly models the real-world problem of resource scheduling with constraints.

Task Scheduler - Leetcode 621 - Python

The video 'Task Scheduler' by NeetCode provides a fantastic, step-by-step walkthrough of this exact greedy algorithm, showing how a max-heap and a queue work together.

First, watch from 05:46 to 11:47 to understand the high-level algorithm involving the max-heap and the cooldown queue. Then, watch the code walkthrough from 11:47 to 16:34 to see how this logic is translated into a working solution. Focus on the interplay between the two data structures.


Conclusion

In this lesson, you saw how the priority queue, which you implemented in the last session, serves as a powerful tool for solving complex problems. By choosing the right data structure, you can design algorithms that are not only correct but also efficient.

Key Takeaways:

  • Problem Pattern Recognition: Identifying that a problem requires repeatedly finding an element with the highest/lowest value is a strong signal to use a priority queue.
  • Top-K Selection: For finding the k-th largest elements, the O(n log k) pattern of using a min-heap of size k is a standard and highly effective technique.
  • Priority Scheduling: For scheduling problems that use a greedy approach (e.g., "process the most important job next"), a max-heap is the ideal tool to manage priorities, often paired with another structure like a queue to handle constraints like cooldowns.
  • Architectural Thinking: Mapping a problem's abstract requirements—like "prioritization," "selection," or "scheduling"—to concrete data structures is a cornerstone of software design and architecture.

Preview of the Next Lesson:
We have now explored a wide array of linear and tree-based data structures. Our next topic introduces one of the most versatile and powerful concepts in computer science: graphs. In the upcoming lesson, we will learn the two primary ways to represent graphs in code—adjacency matrices and adjacency lists—and analyze the critical trade-offs between them. This will be our first step into the world of graph algorithms, which are used to model everything from social networks to navigation systems.