Heaps & Priority Queues¶
Python heapq Module¶
Python only provides a min heap.
import heapq
nums = [3, 1, 4, 1, 5, 9, 2, 6]
heapq.heapify(nums) # O(n) - in-place
heapq.heappush(nums, 7) # O(log n)
smallest = heapq.heappop(nums) # O(log n)
smallest = nums[0] # O(1) - peek
# Push and pop in one operation
result = heapq.heappushpop(nums, 8) # O(log n)
result = heapq.heapreplace(nums, 8) # O(log n) - pop then push
# Get n smallest/largest
heapq.nsmallest(3, nums) # O(n log k)
heapq.nlargest(3, nums) # O(n log k)
| Operation | Time |
|---|---|
| heapify | O(n) |
| heappush | O(log n) |
| heappop | O(log n) |
| peek (heap[0]) | O(1) |
| nsmallest(k) | O(n log k) |
Max Heap Trick¶
Negate values to simulate a max heap:
max_heap = []
heapq.heappush(max_heap, -5)
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -7)
max_val = -heapq.heappop(max_heap) # 7
Custom Objects in Heap¶
# Tuples compared element by element
heap = []
heapq.heappush(heap, (priority, item))
# Dataclass with ordering
from dataclasses import dataclass, field
from typing import Any
@dataclass(order=True)
class PrioritizedItem:
priority: int
item: Any = field(compare=False)
Pattern 1: K-th Largest/Smallest Element (LC 215)¶
Approach 1: Min heap of size k -- keep k largest elements, root is k-th largest.
def findKthLargest(nums: List[int], k: int) -> int:
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap)
return heap[0]
Time: O(n log k) | Space: O(k)
Approach 2: Heapify -- faster when k is close to n.
def findKthLargest(nums: List[int], k: int) -> int:
nums = [-num for num in nums]
heapq.heapify(nums)
for _ in range(k - 1):
heapq.heappop(nums)
return -heapq.heappop(nums)
Time: O(n + k log n) | Space: O(1) if mutating input
Approach 3: QuickSelect -- optimal average case.
def findKthLargest(nums: List[int], k: int) -> int:
k = len(nums) - k # Convert to k-th smallest
def quickselect(left, right):
pivot = nums[right]
i = left
for j in range(left, right):
if nums[j] <= pivot:
nums[i], nums[j] = nums[j], nums[i]
i += 1
nums[i], nums[right] = nums[right], nums[i]
if i == k:
return nums[i]
elif i < k:
return quickselect(i + 1, right)
else:
return quickselect(left, i - 1)
return quickselect(0, len(nums) - 1)
Time: O(n) average, O(n^2) worst | Space: O(1)
Pattern 2: Top K Frequent Elements (LC 347)¶
Heap approach:
def topKFrequent(nums: List[int], k: int) -> List[int]:
from collections import Counter
count = Counter(nums)
heap = []
for num, freq in count.items():
heapq.heappush(heap, (freq, num))
if len(heap) > k:
heapq.heappop(heap)
return [num for freq, num in heap]
Time: O(n log k) | Space: O(n)
Bucket sort approach (optimal):
def topKFrequent(nums: List[int], k: int) -> List[int]:
from collections import Counter
count = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
for num, freq in count.items():
buckets[freq].append(num)
result = []
for i in range(len(buckets) - 1, 0, -1):
result.extend(buckets[i])
if len(result) >= k:
return result[:k]
Time: O(n) | Space: O(n)
Pattern 3: K Closest Points to Origin (LC 973)¶
Use a max heap of size k. If a new point is closer than the farthest in the heap, swap it in.
def kClosest(points: List[List[int]], k: int) -> List[List[int]]:
# Max heap of size k (negate distance)
heap = []
for x, y in points:
dist = -(x * x + y * y) # Negate for max heap
if len(heap) < k:
heapq.heappush(heap, (dist, x, y))
elif dist > heap[0][0]:
heapq.heapreplace(heap, (dist, x, y))
return [[x, y] for _, x, y in heap]
Time: O(n log k) | Space: O(k)
Pattern 4: K-Way Merge / Merge K Sorted Lists (LC 23)¶
def mergeKLists(lists: List[ListNode]) -> ListNode:
heap = []
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
dummy = ListNode(0)
current = dummy
while heap:
val, i, node = heapq.heappop(heap)
current.next = node
current = current.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
Time: O(n log k) where n = total nodes, k = number of lists | Space: O(k)
The index i serves as a tiebreaker since ListNode objects are not comparable.
Pattern 5: Running Median (LC 295)¶
Two heaps: max heap for the smaller half, min heap for the larger half.
class MedianFinder:
def __init__(self):
self.small = [] # Max heap (negated)
self.large = [] # Min heap
def addNum(self, num: int) -> None:
heapq.heappush(self.small, -num)
# Ensure max of small <= min of large
if (self.small and self.large and
-self.small[0] > self.large[0]):
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
# Balance sizes (small can have 1 more)
if len(self.small) > len(self.large) + 1:
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
elif len(self.large) > len(self.small):
val = heapq.heappop(self.large)
heapq.heappush(self.small, -val)
def findMedian(self) -> float:
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2
Time: O(log n) per add, O(1) per findMedian | Space: O(n)
Pattern 6: Reorganize / Rearrange (LC 767)¶
Place most frequent characters first, ensuring no two adjacent characters are the same.
def reorganizeString(s: str) -> str:
from collections import Counter
count = Counter(s)
max_heap = [(-freq, char) for char, freq in count.items()]
heapq.heapify(max_heap)
result = []
prev_freq, prev_char = 0, ''
while max_heap:
freq, char = heapq.heappop(max_heap)
result.append(char)
if prev_freq < 0:
heapq.heappush(max_heap, (prev_freq, prev_char))
prev_freq, prev_char = freq + 1, char # freq is negative
result_str = ''.join(result)
return result_str if len(result_str) == len(s) else ""
Time: O(n log k) where k = unique characters | Space: O(k)
Pattern 7: Task Scheduler (LC 621)¶
Schedule tasks with cooldown period n between same tasks. Greedy: always pick the task with the highest remaining count.
def leastInterval(tasks: List[str], n: int) -> int:
from collections import Counter
count = Counter(tasks)
max_heap = [-freq for freq in count.values()]
heapq.heapify(max_heap)
time = 0
cooldown = deque() # (available_time, neg_count)
while max_heap or cooldown:
time += 1
if max_heap:
freq = heapq.heappop(max_heap) + 1 # Execute one (freq is negative)
if freq < 0:
cooldown.append((time + n, freq))
if cooldown and cooldown[0][0] == time:
heapq.heappush(max_heap, cooldown.popleft()[1])
return time
Time: O(n * k) where k = unique tasks | Space: O(k)
For the heap-free formula approach: result = max(len(tasks), (max_freq - 1) * (n + 1) + count_of_max_freq).
Advanced¶
Smallest Range Covering K Lists (LC 632)¶
def smallestRange(nums: List[List[int]]) -> List[int]:
heap = [(row[0], i, 0) for i, row in enumerate(nums)]
heapq.heapify(heap)
curr_max = max(row[0] for row in nums)
best_range = [float('-inf'), float('inf')]
while heap:
curr_min, list_idx, elem_idx = heapq.heappop(heap)
if curr_max - curr_min < best_range[1] - best_range[0]:
best_range = [curr_min, curr_max]
if elem_idx + 1 < len(nums[list_idx]):
next_val = nums[list_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
curr_max = max(curr_max, next_val)
else:
break # One list exhausted, can't cover all
return best_range
Time: O(n log k) where n = total elements, k = number of lists | Space: O(k)
Lazy Deletion Pattern¶
When you cannot efficiently update entries already in the heap (e.g., changing priorities), use lazy deletion: mark entries as invalid and skip them when popped.
class LazyHeap:
def __init__(self):
self.heap = []
self.deleted = set()
def push(self, item):
heapq.heappush(self.heap, item)
def remove(self, item):
self.deleted.add(item)
def pop(self):
while self.heap:
item = heapq.heappop(self.heap)
if item not in self.deleted:
return item
self.deleted.discard(item)
return None
def peek(self):
while self.heap and self.heap[0] in self.deleted:
self.deleted.discard(heapq.heappop(self.heap))
return self.heap[0] if self.heap else None
This is useful for problems like Sliding Window Median where you need to remove arbitrary elements from a heap.
Note: For interval scheduling problems using heaps (e.g., Meeting Rooms II / LC 253), see intervals.md.
Complexity Reference¶
| Pattern | Time | Space |
|---|---|---|
| K-th element (heap of size k) | O(n log k) | O(k) |
| K-th element (quickselect) | O(n) avg | O(1) |
| Top K frequent (heap) | O(n log k) | O(n) |
| Top K frequent (bucket sort) | O(n) | O(n) |
| K-way merge | O(n log k) | O(k) |
| Running median | O(log n) per add | O(n) |
| Task scheduler | O(n) | O(k) |
Common Mistakes¶
-
Forgetting to heapify -- pushing to an unsorted list and then popping gives wrong results. Always
heapifyfirst or build viaheappush. -
Confusing k largest vs k smallest -- K largest: use min heap of size k. K smallest: use max heap of size k.
-
Tuple comparison issues -- Python compares tuples element by element. If priorities tie and the second element is not comparable (e.g., ListNode), add a tiebreaker index.
-
Modifying heap directly -- never insert/remove from the list directly. Always use
heapqfunctions. -
Using max heap without negation -- Python has no built-in max heap. Always negate values.