Intermediate: core algorithms · Heaps and greedy algorithms · topic 37 of 61 · 40 min
Heap patterns: top-k and streams
Explanation
Top k: why a heap of size k
Sorting all n values and taking the first k costs O(n log n) and needs all data in memory. When k is small, there is a better way. Keep a min-heap with at most k items. For each new value, push it; if the heap now has k + 1 items, pop the smallest. At the end the heap holds exactly the k largest values, and its root heap[0] is the k-th largest.
Why a MIN-heap for the LARGEST values? The root is the smallest of the current top k, the first one to be replaced when a bigger value arrives. Each push and pop is O(log k), so the total is O(n log k) time and O(k) space. That works on a stream too: you never need more than k values in memory.
import heapq
def k_largest(nums, k):
heap = []
for x in nums:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # drop the smallest of the k + 1
return sorted(heap, reverse=True)
print(k_largest([3, 2, 1, 5, 6, 4], 2)) # Output: [6, 5]K closest points and other "best k by score" problems
When items have a score, compare by the score. For the k points closest to the origin, the score is x² + y² (no square root needed, because squaring keeps the order). heapq.nsmallest(k, points, key=...) is the quick answer. Written by hand, keep a MAX-heap of size k on distance (push the negative distance) and pop the farthest whenever the heap grows beyond k.
points = [(1, 3), (-2, 2), (5, 8), (0, 1)]
print(heapq.nsmallest(2, points, key=lambda p: p[0] * p[0] + p[1] * p[1]))
# Output: [(0, 1), (-2, 2)]Top k frequent items combines Module 3 and this topic: count with collections.Counter in O(n), then pick the k items with the highest counts with a heap. Always say how ties are broken, for example "equal counts: smaller value first", and make your key match it.
Merge k sorted lists
You have k sorted lists with N values in total, for example sorted logs from k servers. Put the FIRST item of every list into a min-heap as (value, list number, position). Pop the smallest, add it to the output, and push the next item from the same list. The heap never holds more than k items, so each step is O(log k): O(N log k) in total. The list number in the tuple also breaks ties, so equal values never fall back to comparing anything strange.
def merge_k(lists):
heap = [(lst[0], i, 0) for i, lst in enumerate(lists) if lst]
heapq.heapify(heap)
out = []
while heap:
val, i, j = heapq.heappop(heap)
out.append(val)
if j + 1 < len(lists[i]):
heapq.heappush(heap, (lists[i][j + 1], i, j + 1))
return out
print(merge_k([[1, 4, 5], [1, 3, 4], [2, 6]])) # Output: [1, 1, 2, 3, 4, 4, 5, 6]Median of a data stream: two heaps
The median is the middle value of the sorted data (the average of the two middle values when the count is even). Re-sorting after every new value costs O(n log n) each time. Instead keep two heaps: low, a max-heap (stored negated) with the smaller half, and high, a min-heap with the larger half. Keep their sizes equal, or let low hold one extra. Then the median is the top of low, or the average of both tops.
To add a number: push it into low, move low's largest into high (so every value in low stays ≤ every value in high), and if high is now bigger than low, move high's smallest back. Each add is O(log n) and reading the median is O(1).
class MedianFinder:
def __init__(self):
self.low = [] # max-heap of the smaller half (negated)
self.high = [] # min-heap of the larger half
def add_num(self, num):
heapq.heappush(self.low, -num)
heapq.heappush(self.high, -heapq.heappop(self.low))
if len(self.high) > len(self.low):
heapq.heappush(self.low, -heapq.heappop(self.high))
def find_median(self):
if len(self.low) > len(self.high):
return float(-self.low[0])
return (-self.low[0] + self.high[0]) / 2
m = MedianFinder()
for x in [5, 15, 1, 3]:
m.add_num(x)
print(m.find_median(), end=" ") # Output: 5.0 10.0 5.0 4.0Task scheduling
Schedulers often keep jobs in a heap keyed by time or priority: the next job to run is always at the root. A common interview version gives tasks with counts and a cooldown between two runs of the same task, and asks for the shortest schedule. The greedy plan is to always run the remaining task with the HIGHEST count that is not cooling down, using a max-heap of counts and a queue of tasks waiting out their cooldown.
Common mistakes
- Using a max-heap of all n items to find the k largest. It works but costs O(n + k log n) memory and time on a stream. Fix: a min-heap of size k.
- Returning the heap list as the answer when the order matters. The heap is not sorted. Fix: sort the k items, or pop them in order.
- Leaving ties unstated in "top k frequent". Fix: state the rule and use a key like (-count, value).
- In merge k, pushing (value, list) tuples: equal values then compare the lists themselves. Fix: include the list index.
- In the two-heap median, letting the heaps get out of balance by more than one, or forgetting to negate when reading low. Fix: rebalance after every add, and test with 1, 2 and 3 values.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Heap patterns: top-k and streams · 1/8
This lesson shows the heap patterns that appear again and again: top k, merging sorted lists, and the median of a stream.
Prefer a video by a teacher? Find free videos on “top k elements heap kth largest merge k sorted lists median data stream two heaps” on YouTube ↗
In real life: Trending items and live delivery times
A food delivery app may show the "top 10 dishes ordered in your area this hour". Counting orders and keeping the 10 best with a small heap works even when the order stream never stops. For a live dashboard, the operations team may watch the median delivery time rather than the average, because a few very late orders pull the average up; two heaps update the median after every delivery in O(log n).
import heapq
from collections import Counter
orders = ["biryani", "dosa", "biryani", "momos", "dosa", "biryani", "thali", "momos", "dosa"]
counts = Counter(orders)
print(heapq.nlargest(2, counts.items(), key=lambda item: item[1]))Practice (0/4 done)
Write kth_largest(nums, k) that returns the k-th largest value in nums (1 ≤ k ≤ len(nums)). Duplicates count separately: in [3, 3, 1] the 2nd largest is 3. Use a min-heap of size k, O(n log k). Example: kth_largest([3, 2, 1, 5, 6, 4], 2) returns 5.
Write top_k_frequent(nums, k) that returns the k values that appear most often, ordered from the most frequent to the least. TIE RULE: when two values have the same count, the SMALLER value comes first. k is between 1 and the number of distinct values. Example: top_k_frequent([1, 1, 1, 2, 2, 3], 2) returns [1, 2].
Write merge_k_sorted(lists) where lists is a list of lists of integers, each sorted in non-decreasing order (some may be empty). Return ONE sorted list with all the values. Use a heap of (value, list index, position) so the cost is O(N log k). Do not just join everything and sort. Example: merge_k_sorted([[1, 4, 5], [1, 3, 4], [2, 6]]) returns [1, 1, 2, 3, 4, 4, 5, 6].
Build a class MedianFinder with add_num(num) and find_median(). find_median() returns the median of all numbers added so far as a float (the average of the two middle values when the count is even). It is only called after at least one add_num. Use two heaps so add_num is O(log n) and find_median is O(1). Example: after adding 5 and 15, find_median() returns 10.0.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Greedy algorithms: the ideaHeaps and greedy algorithms · same module
- Interval problemsHeaps and greedy algorithms · same module
- Shortest paths: Dijkstra and friendsGraphs
- Stack and heapC Programming (from zero to systems) · Dynamic memory
- Memory 101: the stack and the heapRust Programming · Ownership: Rust's big idea
- Counting and totals: aggregate functionsBackend Development · Databases and SQL basics
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
To keep the 10 LARGEST values from a long stream, which heap do you keep?
Answer: A min-heap of size 10
The min-heap's root is the weakest of the current top 10, so it is the one to drop when a bigger value comes.
What is the time to find the k largest of n values with a size-k heap?
Answer: O(n log k)
Each of the n values costs at most one push and one pop on a heap of size k.
Merging k sorted lists with N values in total using a heap costs…
Answer: O(N log k)
The heap holds at most k items, and each of the N values is pushed and popped once.
In the two-heap median, what does the max-heap "low" hold?
Answer: The smaller half of the numbers
low keeps the smaller half so its top is the largest of the small values, one of the middle values.
For the k closest points to the origin, why compare x² + y² instead of the true distance?
Answer: Squaring keeps the same order and avoids a slow, inexact square root
The square root is increasing, so comparing squared distances gives the same order.
Pop on a binary heap moves the last item to the root and then…Related
Answer: sifts it down, swapping with the smaller child while it is bigger
The moved item may be too big for the root, so it swaps downward with its smaller child until the heap property holds.
Quick sort always picks the last item as pivot. Which input is slowest?Related
Answer: A list that is already sorted
Each pivot is the largest item, so one side is empty every time: n levels, O(n²).
Top-k words with the rule "higher count first, then alphabetical". Which sort key?Related
Answer: key=lambda w: (-counts[w], w)
The minus sign sorts counts from high to low; the word breaks ties alphabetically.
What is the time of Dijkstra with a binary heap?Related
Answer: O((V + E) log V)
Up to E pushes and pops, each costing O(log V).
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Heaps and greedy algorithms” before its exam: Greedy algorithms: the idea, Interval problems, More greedy classics.
- Module 8 exam: Heaps and greedy algorithms · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Graphs. 61 topics left in the course before the final exam.