Python Heap and Priority Queue Practice Problems & Exercises
Practice: Heap and Priority Queue
← Back to lessonEasy
Predict the output of every print call before running the code. Focus on what heap[0] holds after each operation.
import heapq
data = [5, 3, 8, 1, 4, 2, 7]
heapq.heapify(data)
print(f"After heapify: {data[0]}")
print(f"Pop 1: {heapq.heappop(data)}")
print(f"Pop 2: {heapq.heappop(data)}")
heapq.heappush(data, 0)
print(f"After push 0: {data[0]}")
print(f"Pop 3: {heapq.heappop(data)}")
print(f"Remaining min: {data[0]}")Solution
After heapify: 1
Pop 1: 1
Pop 2: 2
After push 0: 0
Pop 3: 0
Remaining min: 3
Key insights:
- After heapify: The list is rearranged so the minimum (1) sits at index 0. The internal order is a valid min-heap but NOT fully sorted.
- Pop 1: Removes and returns 1 (the minimum). Heap now has elements 2, 3, 4, 5, 7, 8.
- Pop 2: Removes and returns 2 (the new minimum). Heap now has 3, 4, 5, 7, 8.
- Push 0: Inserts 0 and sifts it up to the root since it is smaller than everything.
data[0]is now 0. - Pop 3: Removes and returns 0.
- Remaining min: The smallest remaining element is 3.
Takeaway: heappop() always returns the current minimum. The internal list layout between index 1 and the end is NOT sorted — only the root is guaranteed to be the minimum.
Expected Output
After heapify: 1
Pop 1: 1
Pop 2: 2
After push 0: 0
Pop 3: 0
Remaining min: 3Hints
Hint 1: heapify() transforms the list in-place so that heap[0] is always the minimum element.
Hint 2: heappop() removes and returns the smallest element. The next smallest then bubbles to index 0.
Hint 3: heappush() inserts the value and sifts it up to maintain the heap invariant.
Use Key insights:heapq.nsmallest to return the k smallest elements from a list. The result should be in ascending order.Solution
heapq.nsmallest(k, data) returns a sorted list of the k smallest elements.k is small relative to n.min() — O(n).
import heapq
def k_smallest(nums, k):
# Return the k smallest elements from nums in ascending order.
# Use heapq.nsmallest().
# TODO: implement
pass
# Test cases — do not modify
print(k_smallest([7, 2, 9, 1, 5, 3], 3))
print(k_smallest([10, 20, 30, 40, 50], 1))
print(k_smallest([5, 5, 5, 5], 2))
print(k_smallest(list(range(100, 0, -1)), 5))Expected Output
[1, 2, 3]
[10]
[5, 5]
[1, 2, 3, 4, 5]Hints
Hint 1: heapq.nsmallest(k, iterable) returns the k smallest elements as a sorted list.
Hint 2: This is more efficient than sorted(nums)[:k] when k is much smaller than len(nums).
Python's Key insights:heapq only provides a min-heap. Use the negation trick to simulate a max-heap: store -x so that the smallest negated value corresponds to the largest original value. Pop the three largest elements and return them in descending order.Solution
[-x for x in nums]. The list [-4, -1, -7, ...] forms a min-heap where -9 is the smallest, meaning 9 is the largest original value.heapify() for O(n) construction instead of n individual heappush() calls.-heapq.heappop(max_heap) restores the original positive value.heapq.
import heapq
def three_largest_via_max_heap(nums):
# Build a max-heap using the negation trick.
# Pop the 3 largest elements and return them
# as a list in descending order: [largest, 2nd, 3rd].
# TODO: implement
pass
# Test cases — do not modify
print(three_largest_via_max_heap([4, 1, 7, 3, 8, 5, 2, 9, 6]))
print(three_largest_via_max_heap([10, 10, 10, 5, 5]))
print(three_largest_via_max_heap([100, 200, 300]))Expected Output
[9, 8, 7]
[10, 10, 10]
[300, 200, 100]Hints
Hint 1: To simulate a max-heap, negate every value before pushing: heapq.heappush(heap, -x).
Hint 2: When you pop, negate the result back: largest = -heapq.heappop(heap).
Hint 3: You can also heapify a list of negated values for O(n) construction.
Predict the output and explain why both approaches produce the same logical result but differ in performance.
import heapq
data = [5, 3, 8, 1, 4, 2, 7, 6, 9, 0]
# Approach A: heapify (O(n))
heap_a = data[:]
heapq.heapify(heap_a)
print(f"heapify result min: {heap_a[0]}")
# Approach B: push one by one (O(n log n))
heap_b = []
for x in data:
heapq.heappush(heap_b, x)
print(f"push-loop result min: {heap_b[0]}")
# Both are valid heaps with the same elements
print(f"Same contents: {sorted(heap_a) == sorted(heap_b)}")
print("heapify is O(n), push-loop is O(n log n)")Solution
heapify result min: 1
push-loop result min: 1
Same contents: True
heapify is O(n), push-loop is O(n log n)
Key insights:
- Both approaches yield a valid min-heap with the same set of elements and the same minimum at index 0.
- The internal array layouts may differ — heap ordering is not unique. Multiple valid arrangements exist for the same set of elements.
heapify()is O(n) because it uses a bottom-up sift-down approach. Most nodes are leaves and require zero work.- Pushing n elements one at a time is O(n log n) because each
heappushmay sift up to the root — O(log n) per element. - Rule: Always use
heapify()when you have the full data upfront. Only useheappush()in a loop when elements arrive one at a time (streaming).
Expected Output
heapify result min: 1
push-loop result min: 1
Same contents: True
heapify is O(n), push-loop is O(n log n)Hints
Hint 1: Both approaches produce a valid min-heap, but heapify() is O(n) while pushing one by one is O(n log n).
Hint 2: heapify() works bottom-up — most nodes near the leaves do very little work.
Medium
Find the k-th largest element in a list using a min-heap of size Why this works:k. Do not use sorted() or heapq.nlargest(). This is a classic interview problem (LeetCode 215).Solution
heap[0] is the smallest of the k largest — which is the k-th largest by definition.
import heapq
def kth_largest(nums, k):
# Return the k-th largest element in nums.
# Use a min-heap of size k.
# Do NOT use sorted() or nlargest().
# TODO: implement
pass
# Test cases — do not modify
print(kth_largest([3, 2, 1, 5, 6, 4], 2))
print(kth_largest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4))
print(kth_largest([1], 1))
print(kth_largest([7, 7, 7, 7], 3))Expected Output
5
4
1
7Hints
Hint 1: Maintain a min-heap of size k. Push each element. When the heap exceeds size k, pop the smallest.
Hint 2: After processing all elements, heap[0] is the k-th largest — it is the smallest among the top-k.
Hint 3: Time complexity: O(n log k). Space: O(k).
Build a simple task scheduler using a heap of Key insights:(priority, task_name) tuples. Process tasks from highest priority (lowest number) to lowest priority. When two tasks share the same priority, they should come out in alphabetical order by name (tuple comparison handles this).Solution
priority, then by task_name as a tiebreaker.__lt__, equal priorities cause a TypeError. Use the counter trick (priority, counter, item) to avoid this.
import heapq
def process_tasks(tasks):
# tasks is a list of (priority, task_name) tuples.
# Lower priority number = higher importance.
# Process all tasks in priority order and return
# a list of task names in the order they were processed.
# TODO: implement using a heap
pass
# Test cases — do not modify
tasks1 = [(3, "email"), (1, "deploy"), (2, "review"), (1, "hotfix")]
print(process_tasks(tasks1))
tasks2 = [(5, "docs"), (5, "tests"), (5, "lint"), (1, "build")]
print(process_tasks(tasks2))Expected Output
['deploy', 'hotfix', 'review', 'email']
['build', 'docs', 'lint', 'tests']Hints
Hint 1: Push each (priority, task_name) tuple onto a heap. Python compares tuples element by element.
Hint 2: When priorities are equal, Python compares the second element (the string) — so equal-priority tasks sort alphabetically.
Hint 3: Pop all elements from the heap to get them in priority order.
Given an array where every element is at most k positions from its correct sorted position, sort it efficiently using a heap of size Why this works:k+1. This is faster than a full sort — O(n log k) instead of O(n log n).Solution
import heapq
def sort_nearly_sorted(nums, k):
# Each element in nums is at most k positions away from
# its correct sorted position.
# Sort the array efficiently using a min-heap of size k+1.
# Return the sorted list.
# TODO: implement
pass
# Test cases — do not modify
print(sort_nearly_sorted([3, 2, 1, 5, 4, 7, 6, 8], 2))
print(sort_nearly_sorted([6, 5, 3, 2, 8, 10, 9], 3))
print(sort_nearly_sorted([1, 2, 3, 4, 5], 0))Expected Output
[1, 2, 3, 4, 5, 6, 7, 8]
[2, 3, 5, 6, 8, 9, 10]
[1, 2, 3, 4, 5]Hints
Hint 1: Since each element is at most k away from its final position, you only need to consider a window of k+1 elements at a time.
Hint 2: Maintain a min-heap of size k+1. Push the first k+1 elements. Then for each remaining element, pop the min (it is the next sorted value) and push the new element.
Hint 3: After all elements are consumed, pop the remaining heap elements to finish the sorted output.
Given a list of integers, return the k most frequent elements. Use a heap-based approach — do not simply sort the frequency dictionary. This is LeetCode 347. Key insights:Solution
Counter: O(n).(frequency, element). When the heap exceeds size k, pop the smallest frequency — it cannot be in the top-k.heapq.nlargest(k, counts.items(), key=lambda x: x[1]) does the same in one line.
import heapq
from collections import Counter
def top_k_frequent(nums, k):
# Return the k most frequent elements in nums.
# If multiple elements have the same frequency,
# any order among them is acceptable.
# Use a heap — do NOT just sort the frequency list.
# TODO: implement
pass
# Test cases — do not modify
print(sorted(top_k_frequent([1,1,1,2,2,3], 2)))
print(sorted(top_k_frequent([4,4,4,4,5,5,6], 1)))
print(sorted(top_k_frequent([1,2,3,4,5], 3)))Expected Output
[1, 2]
[4]
[1, 2, 3]Hints
Hint 1: First count frequencies with collections.Counter. Then use a heap to find the top-k by frequency.
Hint 2: Maintain a min-heap of size k keyed by frequency: push (freq, element). If heap size exceeds k, pop the smallest frequency.
Hint 3: After processing all unique elements, the heap contains the k most frequent. Extract just the element values.
Hard
Merge k sorted lists into one sorted list using a min-heap. The heap should hold at most one element from each list at any time. This is LeetCode 23 and a common interview classic. How it works step-by-step (first test case): Complexity:Solution
[(1, 0, 0), (2, 1, 0), (3, 2, 0)](1, 0, 0) — value 1 from list 0. Push list 0's next element: (4, 0, 1).(2, 1, 0) — value 2 from list 1. Push (5, 1, 1).(3, 2, 0) — value 3 from list 2. Push (6, 2, 1).
(value, list_idx, elem_idx) ensures correct comparison: ties in value break by list index, then by element index — all integers, so no TypeError.
import heapq
def merge_k_sorted(lists):
# Merge k sorted lists into a single sorted list.
# Use a min-heap to track the current smallest element
# across all lists.
# Return the merged sorted list.
# TODO: implement
pass
# Test cases — do not modify
print(merge_k_sorted([[1, 4, 7], [2, 5, 8], [3, 6, 9]]))
print(merge_k_sorted([[1, 3, 5, 7], [2, 4, 6, 8], [0, 9, 10]]))
print(merge_k_sorted([[], [1], [0, 2]]))
print(merge_k_sorted([]))Expected Output
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 1, 2]
[]Hints
Hint 1: Initialize the heap with the first element from each non-empty list, along with the list index and element index: (value, list_idx, elem_idx).
Hint 2: Pop the smallest, append to result, then push the next element from the same list (if any).
Hint 3: The heap never exceeds size k (the number of lists), giving O(n log k) total time.
Design a MedianFinder class that supports adding numbers from a stream and finding the current median in O(1) time. Use the two-heap technique: a max-heap for the lower half and a min-heap for the upper half. This is LeetCode 295. How the two-heap approach works: Complexity:Solution
add_num: O(log n) — at most a constant number of heap pushes/pops.find_median: O(1) — just reading the roots.
import heapq
class MedianFinder:
# Maintain the running median of a stream of numbers.
# Use two heaps:
# - max_heap (negated): stores the smaller half
# - min_heap: stores the larger half
# Invariant: max_heap has the same size or one more element than min_heap.
#
# TODO: implement __init__, add_num, and find_median
def __init__(self):
pass
def add_num(self, num):
pass
def find_median(self):
# Return the median as a float.
pass
# Test cases — do not modify
mf = MedianFinder()
for val in [5, 2, 8, 1, 9]:
mf.add_num(val)
print(mf.find_median())
print("---")
mf2 = MedianFinder()
for val in [1, 2, 3, 4]:
mf2.add_num(val)
print(mf2.find_median())Expected Output
5.0
3.5
5.0
3.5
5.0
---
1.0
1.5
2.0
2.5Hints
Hint 1: The max_heap (negated) holds the lower half. The min_heap holds the upper half. The median is either the max of the lower half or the average of both tops.
Hint 2: After adding a number, rebalance: if the max of the lower half exceeds the min of the upper half, move elements between heaps.
Hint 3: Also rebalance sizes: the max_heap can have at most one more element than min_heap. If min_heap is bigger, move its top to max_heap.
Given a list of tasks (characters) and a cooldown interval How the algorithm works (first test case: A=3, B=3, n=2): Schedule: Complexity:n, compute the minimum time units needed to execute all tasks. The same task must wait at least n units between consecutive executions. Idle time is allowed. Use a max-heap to always schedule the most frequent remaining task. This is LeetCode 621.Solution
[-3, -3] (A and B each appear 3 times).[-3].[].A B _ A B _ A B = 8 units.
import heapq
from collections import Counter
def least_interval(tasks, n):
# Given a list of task characters and a cooldown interval n,
# return the minimum number of time units needed to finish
# all tasks. The same task must have at least n units of
# cooldown between consecutive executions.
# Tasks can be executed in any order. Idle slots are allowed.
#
# Example: tasks=["A","A","A","B","B","B"], n=2
# One valid schedule: A B _ A B _ A B -> 8 units
#
# TODO: implement using a max-heap
pass
# Test cases — do not modify
print(least_interval(["A","A","A","B","B","B"], 2))
print(least_interval(["A","A","A","B","B","B"], 0))
print(least_interval(["A","A","A","A","B","B","C","C"], 2))Expected Output
8
6
8Hints
Hint 1: Use a max-heap (negated counts) so the most frequent task is always scheduled first — this minimizes idle slots.
Hint 2: Each time unit, pop the most frequent task from the heap, decrement its count, and place it in a cooldown queue with its availability time.
Hint 3: After each time unit, check if any task in the cooldown queue is ready to re-enter the heap.
