Pattern 12 · Heaps

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).

time O(n log k)space O(k)key move min-heap for largestpractice 10 problems
01 core concept

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).

1 / 8walkthrough — 3 largest of [3, 1, 5, 12, 2, 11] with a size-3 min-heap
3
1
5
12
2
11
k 3
heap []
The input stream. We want the 3 largest. Rule: push each element into a min-heap; if the size exceeds 3, pop the root (the smallest). From the next step on, the cells show the heap's actual list layout — index 0 is always the root.
02 heap direction

The rule everyone gets backwards

This is the most confusing part of the pattern. Burn it in:

The direction rule
K largest → MIN heap of size k (root = kth largest = smallest of the top k).
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.

Finding top 3 largest from [7, 10, 4, 3, 20, 15]: MIN heap (size 3): Visualization: After processing: 4 <-- root = kth largest / \ 10 7 Push 20 (20 > 4): 7 pop 4, push 20 / \ 10 20 Push 15 (15 > 7): 10 / \ 15 20 Answer: heap contains [10, 15, 20] = top 3 largest
03 python heapq

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.

heapq_cheatsheet.pypython
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.)
04 recognition

When to reach for it

signal in problemapproach
"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)
Before coding, walk the framework: 1. WHAT are we comparing? --> raw values, frequencies, distances, custom 2. Do we want LARGEST or SMALLEST? --> largest -> min heap; smallest -> max heap 3. HEAP SIZE: maintain exactly k elements --> push freely until size k --> after that: push + pop if new > root 4. CUSTOM comparison? --> use tuple: (priority, tiebreaker, value)
When NOT to use a heap
k close to n → the log k advantage vanishes; plain sorting at O(n log n) is fine and simpler. One-shot exact kth element → QuickSelect averages O(n). Frequencies with small value range → bucket sort gets O(n) with no heap at all.
05 templates

The code that covers 90% of problems

T1Top k largest

top_k_largest.pypython
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

top_k_smallest.pypython
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

kth_largest.py — LC 215python
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²).

kth_largest_quickselect.pypython
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

top_k_frequent.py — LC 347python
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:

top_k_frequent_bucket.pypython
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

k_closest.py — LC 973python
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.

merge_k_lists.py — LC 23python
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
Lists: A: [1,4,7] B: [2,5] C: [3,6] Step | Pop | Push | Heap Contents | Result ------|-----------|-----------|---------------------|-------- 0 | - | A:1,B:2,C:3 | [(1,A),(2,B),(3,C)] | - 1 | (1,A) | A:4 | [(2,B),(3,C),(4,A)] | 1 2 | (2,B) | B:5 | [(3,C),(4,A),(5,B)] | 1->2 3 | (3,C) | C:6 | [(4,A),(5,B),(6,C)] | 1->2->3 4 | (4,A) | A:7 | [(5,B),(6,C),(7,A)] | 1->2->3->4 5 | (5,B) | - | [(6,C),(7,A)] | ...->5 6 | (6,C) | - | [(7,A)] | ...->6 7 | (7,A) | - | [] | ...->7 Final: 1->2->3->4->5->6->7

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.

reorganize_string.py — LC 767python
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].

kth_largest_stream.py — LC 703python
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
1 / 6walkthrough — LC 703: k = 3, then add 2, 3, 5, 10
4root
5
8
k 3
3rd largest 4
Initialized with [4, 5, 8]. The heap is full at k = 3 and the root 4 is the current 3rd largest. Cells show the heap's list layout.

refComplexity reference

problemtimespacenotes
Kth Largest (heap)O(n log k)O(k)Maintain heap of size k
Kth Largest (QuickSelect)O(n) avgO(1)O(n²) worst case
Top K FrequentO(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 PointsO(n log k)O(k)Heap of size k
Merge K Sorted ListsO(N log k)O(k)N = total elements, k = lists
Reorganize StringO(n log A)O(A)A = alphabet size (26)
Kth Largest in StreamO(n log k)O(k)Per add: O(log k)
06 common mistakes

Where heaps go wrong

Mistake 1 — heap direction confusion
For k LARGEST, use a MIN heap. Repeat it to yourself. The min heap evicts the smallest, keeping only the largest k.
Mistake 2 — forgetting to negate for a max heap
Python only ships a min heap. Push -val, and negate again on pop — forgetting either half silently inverts your answer.
the fixpython
heapq.heappush(heap, -val)       # push negated
original = -heapq.heappop(heap)  # negate back on pop
Mistake 3 — comparing uncomparable objects
Pushing ListNode or custom objects crashes on ties. Wrap in a tuple with an index tiebreaker: (val, index, node) — the comparison never reaches the object.
Mistake 4 — nlargest/nsmallest in a loop
They build a full heap internally each call. For streaming or repeated queries, maintain your own size-k heap instead.
Mistake 5 — off-by-one on the answer
The kth largest is at heap[0] (the root), not heap[k-1]. The rest of the list is not sorted.
10 practice

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.

10 problems
easyLC 703
Maintain a min heap of size k. The pattern in its purest form.
mediumLC 215
Min heap of size k; then try QuickSelect for O(n) average.
mediumLC 347
Counter + heap, or bucket sort for O(n).
mediumLC 973
Max heap by distance (negate). Skip the sqrt.
mediumLC 767
Max heap + greedy placement with a one-step hold-out.
mediumLC 451
Counter + max heap.
mediumLC 373
Min heap of pairs — push successors lazily.
mediumLC 378
Min heap of row heads, or binary search on the value.
hardLC 23
Min heap of heads + index tiebreaker.
hardLC 295
Two heaps: max heap of the low half, min heap of the high half.