Top-K Elements
Find the k largest, smallest, or most frequent elements without sorting everything — keep a heap capped at size k and let it evict the losers. O(n log n) becomes O(n log k).
A heap that never grows past k
Sorting the whole input to grab the top 3 is wasteful. Instead, push every element into a heap and pop whenever the size exceeds k. The heap permanently holds the k best candidates seen so far — each element costs O(log k), not O(log n).
The rule everyone gets backwards
This is the most confusing part of the pattern. Burn it in:
K smallest → MAX heap of size k (root = kth smallest = largest of the bottom k).
Why a min heap for largest? The min heap's root is the smallest element in the heap. With k elements maintained, that root is the kth largest overall. Any new element bigger than the root deserves a spot in the top k — so pop the root, push the newcomer. The heap's weakest member guards the door.
heapq in 20 lines
Python's heapq is a min heap only, operating in place on a plain list.
Max-heap behavior comes from negating values; custom ordering comes from tuples.
import heapq
# heapq is a MIN heap by default
nums = [3, 1, 4, 1, 5]
heapq.heapify(nums) # O(n) - transforms list in-place
heapq.heappush(nums, 2) # O(log n)
smallest = heapq.heappop(nums) # O(log n) - pops SMALLEST
# MAX heap trick: NEGATE all values
max_heap = []
heapq.heappush(max_heap, -5) # push negative
heapq.heappush(max_heap, -3)
val = -heapq.heappop(max_heap) # negate on pop -> gives 5 (largest)
# Convenience functions
heapq.nlargest(k, iterable) # returns k largest (uses min heap internally)
heapq.nsmallest(k, iterable) # returns k smallest (uses max heap internally)
# Tuple comparison: compares first element, then second for ties
heapq.heappush(heap, (priority, value))
# Useful for custom ordering (frequency, distance, etc.)
When to reach for it
| signal in problem | approach |
|---|---|
| "k largest / k smallest" | Min/max heap of size k |
| "kth largest / kth smallest" | Heap root after processing |
| "k most frequent" | Count frequencies, then heap |
| "k closest points" | Heap by distance |
| "merge k sorted lists/arrays" | Min heap of heads |
| "streaming data, maintain top k" | Heap of size k |
| "median from data stream" | Two heaps (max + min) |
The code that covers 90% of problems
T1Top k largest
import heapq
def top_k_largest(nums, k):
"""Returns k largest elements using a min heap of size k."""
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap) # remove smallest -> keeps k largest
return heap # heap[0] is the kth largest
T2Top k smallest
def top_k_smallest(nums, k):
"""Returns k smallest elements using a max heap of size k."""
heap = [] # max heap via negation
for num in nums:
heapq.heappush(heap, -num)
if len(heap) > k:
heapq.heappop(heap) # remove largest (most negative)
return [-x for x in heap]
T3Kth largest element — LC 215
def findKthLargest(nums, k):
# Approach 1: Min heap of size k
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap)
return heap[0] # root = kth largest
# Approach 2: One-liner
# return heapq.nlargest(k, nums)[-1]
The alternative when you need the exact kth element once: QuickSelect — average O(n), worst case O(n²).
import random
def findKthLargest_quickselect(nums, k):
"""Find kth largest = (n-k)th smallest in 0-indexed."""
target = len(nums) - k
def quickselect(left, right):
pivot_idx = random.randint(left, right)
nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]
pivot = nums[right]
store = left
for i in range(left, right):
if nums[i] < pivot:
nums[i], nums[store] = nums[store], nums[i]
store += 1
nums[store], nums[right] = nums[right], nums[store]
if store == target:
return nums[store]
elif store < target:
return quickselect(store + 1, right)
else:
return quickselect(left, store - 1)
return quickselect(0, len(nums) - 1)
T4Top k frequent — LC 347
from collections import Counter
def topKFrequent(nums, k):
# Count frequencies, then use heap
count = Counter(nums)
# Min heap of size k based on frequency
return heapq.nlargest(k, count.keys(), key=count.get)
# OR manual heap approach:
# 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]
The O(n) alternative — bucket sort, where the bucket index is the frequency:
def topKFrequent_bucket(nums, k):
count = Counter(nums)
# Bucket index = frequency, bucket value = list of nums with that freq
buckets = [[] for _ in range(len(nums) + 1)]
for num, freq in count.items():
buckets[freq].append(num)
result = []
for freq in range(len(buckets) - 1, 0, -1):
for num in buckets[freq]:
result.append(num)
if len(result) == k:
return result
return result
T5K closest points — LC 973
def kClosest(points, k):
# Max heap of size k (negate distance to simulate max heap)
heap = []
for x, y in points:
dist = -(x * x + y * y) # negate for max heap
heapq.heappush(heap, (dist, x, y))
if len(heap) > k:
heapq.heappop(heap) # remove farthest
return [[x, y] for _, x, y in heap]
T6Merge k sorted lists — LC 23
The heap holds one node per list — the current head of each. Pop the global
minimum, push its successor. The index tiebreaker keeps Python from comparing
ListNode objects.
def mergeKLists(lists):
"""Merge k sorted linked lists using a min heap."""
heap = []
# Push the head of each list
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
dummy = ListNode(0)
curr = dummy
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
# 'i' as tiebreaker prevents comparing ListNode objects
return dummy.next
T7Reorganize string — LC 767
Max heap by frequency + a one-step delay: after using a character, hold it out for one round before re-adding it, so no two identical characters end up adjacent.
from collections import Counter
def reorganizeString(s):
count = Counter(s)
max_heap = [(-freq, ch) for ch, freq in count.items()]
heapq.heapify(max_heap)
result = []
prev_freq, prev_ch = 0, ""
while max_heap:
freq, ch = heapq.heappop(max_heap)
# Re-add previous character if it still has count
if prev_freq < 0:
heapq.heappush(max_heap, (prev_freq, prev_ch))
result.append(ch)
prev_freq = freq + 1 # used one (freq is negative)
prev_ch = ch
return "".join(result) if len(result) == len(s) else ""
T8Kth largest in a stream — LC 703
The size-k min-heap shines with streaming data: each add is
O(log k), and the answer is always sitting at heap[0].
class KthLargest:
def __init__(self, k, nums):
self.k = k
self.heap = []
for num in nums:
self.add(num)
def add(self, val):
heapq.heappush(self.heap, val)
if len(self.heap) > self.k:
heapq.heappop(self.heap)
return self.heap[0] # root = kth largest
refComplexity reference
| problem | time | space | notes |
|---|---|---|---|
| Kth Largest (heap) | O(n log k) | O(k) | Maintain heap of size k |
| Kth Largest (QuickSelect) | O(n) avg | O(1) | O(n²) worst case |
| Top K Frequent | O(n log k) | O(n) | O(n) counting + O(n log k) |
| Top K Frequent (bucket) | O(n) | O(n) | Bucket sort approach |
| K Closest Points | O(n log k) | O(k) | Heap of size k |
| Merge K Sorted Lists | O(N log k) | O(k) | N = total elements, k = lists |
| Reorganize String | O(n log A) | O(A) | A = alphabet size (26) |
| Kth Largest in Stream | O(n log k) | O(k) | Per add: O(log k) |
Where heaps go wrong
-val, and negate again on pop —
forgetting either half silently inverts your answer.
heapq.heappush(heap, -val) # push negated
original = -heapq.heappop(heap) # negate back on pop
ListNode or custom objects crashes on ties. Wrap in a tuple with an index
tiebreaker: (val, index, node) — the comparison never reaches the object.
heap[0] (the root), not heap[k-1].
The rest of the list is not sorted.
Practice ladder
In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.