bigonotes
DSA: Patterns & PythonHeap / Priority Queue

Heap Basics & Top K

How heapq works, the negation trick for max-heaps, and why a size-k heap finds the top K in O(N log K).

7 min readLast updated

Reach for a heap when you need to rank entries on the go and keep fetching the top one (the smallest or the largest). Sorting ranks everything once; a heap keeps the ranking correct while items arrive and leave. The most common use is Top K: keep only the K best items seen so far and throw the rest away.

The idea#

Python's heapq works on a plain list and is a min-heap only: the smallest item always sits at heap[0]. These are the calls you will use most:

Operation Cost Notes
heap[0] O(1)O(1) peek at the smallest
heappush(h, x), heappop(h) O(log⁡n)O(\log n) sift up / sift down
heapify(lst) O(n)O(n) in place, not O(nlog⁡n)O(n \log n)
heapreplace(h, x) O(log⁡n)O(\log n) pop the smallest, then push x
heappushpop(h, x) O(log⁡n)O(\log n) push x, then pop the smallest

Before Python 3.14 there is no public max-heap API, and LeetCode runs older versions. To track the largest values, negate them on the way in and negate them back on the way out.

The heap you need is the opposite of what the question asks for, which feels backward at first:

  • K largest → a min-heap of size K. The smallest item in the heap is the "weakest link". When a bigger item arrives, evict the weakest link.
  • K smallest → a max-heap (negated values) of size K. Evict the biggest.

Each of the N items costs at most one O(log⁡K)O(\log K) heap operation, so the total is O(Nlog⁡K)O(N \log K) time and O(K)O(K) space. When K is small this beats sorting (O(Nlog⁡N)O(N \log N)), and it works on a stream you can't hold in memory.

How to spot it#

These cues point to a heap, and to the size-k version in particular.

  • "Kth largest / smallest", "top K", "K closest", "K most frequent".
  • Repeatedly take the largest (or smallest) item, change it, and put it back (a simulation).
  • Data arrives as a stream and you must answer after every insert.
  • N is large but K is small, or you can't store everything.

Template#

Here is the weakest-link rule as code: a size-k min-heap that keeps the K largest items by any key. Flip the sign of the key to get the K smallest.

heap = empty min-heap of (key, index, item)   # heap[0] is the weakest
for each item, with its index:
    entry = (key of item, index, item)        # index breaks ties
    if len(heap) < k:
        push entry onto heap
    else if entry > heap[0]:
        replace heap[0] with entry            # evict the weakest: O(log k)
return the items in heap                      # any order

After the loop, heap[0] is the Kth largest. When the key is a distance, compare squared distances: if A<BA < B then A2<B2A^2 < B^2, so x2+y2x^2 + y^2 gives the same order with no square root, float cost or precision issue.

Other shapes#

Two more shapes solve some Top K problems faster or with ordered output.

Heapify, then pop K#

When all the data is already in memory, heapify the whole list (O(N)O(N)) and pop K times (O(Klog⁡N)O(K \log N)). The total, O(N+Klog⁡N)O(N + K \log N), is much better than pushing items one at a time (O(Nlog⁡N)O(N \log N)). It is also the easy way to get ordered output, because pops come out in order and tuple comparison breaks ties.

Bucket sort for frequencies#

For "top K by frequency", counts can't exceed N, so you can skip the heap. Put each value in the bucket for its count, then walk the buckets from the highest count down.

count each value
buckets = list of N + 1 empty lists          # index = frequency
for each (value, freq) in counts:
    append value to buckets[freq]
result = empty list
for freq from N down to 1:
    for each value in buckets[freq]:
        append value to result
        if len(result) == k: return result

The nested loop looks like O(N2)O(N^2), but it isn't: each unique value sits in exactly one bucket and is visited once, so the walk is O(N)O(N). It does not give alphabetical order within a bucket.

Counting sort for Kth largest#

If values sit in a small range, count them in an array indexed by num - min_value and walk down from the maximum until K items are used up. That's O(N+range)O(N + \text{range}) with no heap at all.

Which heap pattern?#

Most heap problems fall into one of these shapes.

Goal Tool Why
K largest min-heap of size K pop the small ones, keep the giants
K smallest max-heap of size K pop the big ones, keep the tiny ones
Median of a stream two heaps one for the small half, one for the big half
Merge K sorted sources min-heap of heads next smallest among K candidates
Schedule tasks heap + waiting queue always run the most urgent or frequent task first

A size-k heap (Top K) and a k-way merge are different patterns: one caps the heap at K items, the other keeps one item per source.

Pitfalls#

These are the slips that come up most often with heaps.

  • Heapifying everything and calling it "size K". It's correct, but costs O(N)O(N) memory. When the heap starts from initial data, trim it to K there too, not only on later inserts.
  • Forgetting to negate back when you read from a negated max-heap.
  • Tuples with non-comparable items. (priority, obj) raises TypeError on a tie when obj has no ordering (a ListNode, a dict). Add an index as a tie-breaker, or wrap the object in a class with __lt__.
  • Picking the wrong heap for Top K. K largest needs a min-heap, not a max-heap.
  • Re-sorting in a loop. Sorting again every round of a simulation costs O(n2log⁡n)O(n^2 \log n); a heap makes it O(nlog⁡n)O(n \log n).

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below. Start with the first three.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Last Stone Weight

Each round needs the two largest stones, so use a max-heap of negated weights. Sorting every round is O(n2log⁡n)O(n^2 \log n); the heap makes it O(nlog⁡n)O(n \log n).

Hint: Kth Largest Element in an Array

Keep a min-heap of size K. After the scan, the heap top is the answer. If values sit in a small range, counting sort skips the heap.

Hint: Kth Largest Element in a Stream

Keep a min-heap of size K across calls; each add returns the top. Trim the heap to K in the constructor too, not only in add, so it never grows past K.

Hint: Top K Frequent Elements

Count first. Then either keep a size-K heap keyed by count, or bucket the values by count and walk down from the highest count for O(N)O(N).

Hint: K Closest Points to Origin

Use the key −(x2+y2)-(x^2 + y^2), so "largest key" means "nearest". Skip the square root.

Hint: Sort Characters By Frequency

Bucket sort by count works in O(N)O(N). Walk the buckets from the highest count down and repeat each character by its count.

Hint: Top K Frequent Words

Ties must come out in alphabetical order, so bucket sort is not enough. Heapify (-freq, word) pairs and pop K times: the highest count comes first, and tuple comparison breaks ties alphabetically.

Key takeaways#

  • heapq is a min-heap; negate values for a max-heap, and negate back when you read.
  • K largest uses a min-heap of size K (the top is the weakest link), giving O(Nlog⁡K)O(N \log K).
  • heapify is O(N)O(N), so heapify-then-pop-K is O(N+Klog⁡N)O(N + K \log N).
  • Frequency problems can use bucket sort in O(N)O(N) unless ties need ordering.
  • Put an index in the tuple (or define __lt__) whenever the payload can't be compared.
Revision notes · 2 min

Core idea#

Use a heap to rank entries as they arrive and keep fetching the top one. For Top K, keep only the K best items seen so far in a size-K heap.

heapq costs#

Operation Cost
heap[0] (peek smallest) O(1)O(1)
heappush, heappop O(log⁡n)O(\log n)
heapify(lst) O(n)O(n), in place
heapreplace (pop, then push) O(log⁡n)O(\log n)
heappushpop (push, then pop) O(log⁡n)O(\log n)

Key points#

  • heapq is a min-heap (public max-heap functions arrived only in 3.14). For a max-heap, negate values in and back out.
  • K largest: a min-heap of size K. The top is the weakest link. Evict it when a bigger item arrives.
  • K smallest: a max-heap (negated) of size K.
  • Size-K heap: O(Nlog⁡K)O(N \log K) time, O(K)O(K) space. It works on a stream.
  • All data in memory: heapify, then pop K times. O(N+Klog⁡N)O(N + K \log N), and the output is ordered.
  • K closest points: compare x2+y2x^2 + y^2. No math.sqrt needed.
  • Top K by frequency: bucket sort by count in O(N)O(N). It gives no alphabetical order inside a bucket.
  • Small value range: counting sort in O(N+range)O(N + \text{range}).

Template#

heap = empty min-heap of (key, index, item)
for each item, with its index:
    entry = (key of item, index, item)     # index breaks ties
    if len(heap) < k:
        push entry onto heap
    else if entry > heap[0]:
        replace heap[0] with entry         # evict the weakest
return the items in heap

Pitfalls#

  • Heapifying everything and calling it "size K". In Kth Largest in a Stream, trim to K in the constructor too.
  • Forgetting to negate back.
  • (priority, obj) with a non-comparable obj. A tie raises TypeError. Add an index or define __lt__.
  • A max-heap for K largest. It needs a min-heap.

Say it in an interview#

  • "For the K largest, keep a min-heap of size K, so the top is the one to evict."
  • "That is O(N log K) time and O(K) space."

Found a mistake? Email ask@bigonotes.com