Top K Elements
Whenever a problem asks for the ” largest”, ” most frequent”, ” closest”, or ”-th something”, your brain should immediately reach for one tool: a heap capped at size . You almost never need to sort the whole input to answer a question about only of its elements — and that gap between “sort everything” and “just track the top ” is worth a full complexity class in an interview.
What you’ll learn
- How to recognize the top-k cue in a problem statement.
- The reusable template: a min-heap that never grows past size .
heapq.nlargestheapq.nlargest/heapq.nsmallestheapq.nsmallest— the one-line version of the same idea.- Quickselect — the average-case alternative, and its trade-offs.
- Why a size- heap beats a full sort: vs. .
The cue: “top k”, “kth largest”, “k closest”
Any phrasing that only cares about elements out of — and doesn’t need the rest of the array in sorted order — is a signal to stop thinking “sort” and start thinking “heap of size ”. Sorting the entire array to read off the last elements does work to answer a question that only needs .
The pattern: a min-heap capped at size k
To find the largest values, keep a min-heap (not a max-heap) of
size . Counterintuitive at first, but the reason is simple: the heap’s
root (heap[0]heap[0]) is always the smallest of the k largest values seen so
far — exactly the one value you’d want to evict the instant something
bigger shows up.
import heapq
def top_k_largest(nums, k):
min_heap = []
for x in nums:
if len(min_heap) < k:
heapq.heappush(min_heap, x) # heap not full yet -- just add
elif x > min_heap[0]:
heapq.heapreplace(min_heap, x) # x beats the current worst of the top-k
return sorted(min_heap, reverse=True)
nums = [3, 2, 1, 5, 6, 4]
print(top_k_largest(nums, 2)) # expect [6, 5]import heapq
def top_k_largest(nums, k):
min_heap = []
for x in nums:
if len(min_heap) < k:
heapq.heappush(min_heap, x) # heap not full yet -- just add
elif x > min_heap[0]:
heapq.heapreplace(min_heap, x) # x beats the current worst of the top-k
return sorted(min_heap, reverse=True)
nums = [3, 2, 1, 5, 6, 4]
print(top_k_largest(nums, 2)) # expect [6, 5]At every point in the loop, min_heapmin_heap holds exactly the largest values
seen so far, in no particular internal order — only min_heap[0]min_heap[0] (the
smallest of them) is guaranteed to sit at the root.
How it works
Worked example: Top K Frequent Elements
Count occurrences with a CounterCounter, then let heapq.nlargestheapq.nlargest do the size-
heap work for you, ranking by count instead of by raw value.
import heapq
from collections import Counter
def top_k_frequent(nums, k):
counts = Counter(nums)
return heapq.nlargest(k, counts.keys(), key=counts.get)
nums = [1, 1, 1, 2, 2, 3]
print(top_k_frequent(nums, 2)) # expect [1, 2] -- 1 appears 3x, 2 appears 2ximport heapq
from collections import Counter
def top_k_frequent(nums, k):
counts = Counter(nums)
return heapq.nlargest(k, counts.keys(), key=counts.get)
nums = [1, 1, 1, 2, 2, 3]
print(top_k_frequent(nums, 2)) # expect [1, 2] -- 1 appears 3x, 2 appears 2xheapq.nlargest(k, iterable, key=...)heapq.nlargest(k, iterable, key=...) and heapq.nsmallest(k, iterable, key=...)heapq.nsmallest(k, iterable, key=...) are the built-in, one-line version of the exact pattern above —
reach for them whenever you don’t need to hand-roll the heap yourself.
Quickselect: trading heap overhead for average O(n)
If you only need the -th value itself (not the full sorted top-), the Quickselect algorithm — a partition step borrowed from quicksort — finds it in average time, faster than any heap-based approach for a single query.
import random
def kth_largest(nums, k):
target_index = len(nums) - k # k-th largest == (n-k)-th smallest, 0-indexed
def partition(lo, hi):
pivot_index = random.randint(lo, hi)
pivot = nums[pivot_index]
nums[pivot_index], nums[hi] = nums[hi], nums[pivot_index] # move pivot to the end
store = lo
for i in range(lo, hi):
if nums[i] < pivot:
nums[i], nums[store] = nums[store], nums[i]
store += 1
nums[store], nums[hi] = nums[hi], nums[store] # pivot lands in its final sorted spot
return store
lo, hi = 0, len(nums) - 1
while lo < hi:
p = partition(lo, hi)
if p == target_index:
return nums[p]
elif p < target_index:
lo = p + 1
else:
hi = p - 1
return nums[lo]
nums = [3, 2, 1, 5, 6, 4]
print(kth_largest(nums, 2)) # expect 5import random
def kth_largest(nums, k):
target_index = len(nums) - k # k-th largest == (n-k)-th smallest, 0-indexed
def partition(lo, hi):
pivot_index = random.randint(lo, hi)
pivot = nums[pivot_index]
nums[pivot_index], nums[hi] = nums[hi], nums[pivot_index] # move pivot to the end
store = lo
for i in range(lo, hi):
if nums[i] < pivot:
nums[i], nums[store] = nums[store], nums[i]
store += 1
nums[store], nums[hi] = nums[hi], nums[store] # pivot lands in its final sorted spot
return store
lo, hi = 0, len(nums) - 1
while lo < hi:
p = partition(lo, hi)
if p == target_index:
return nums[p]
elif p < target_index:
lo = p + 1
else:
hi = p - 1
return nums[lo]
nums = [3, 2, 1, 5, 6, 4]
print(kth_largest(nums, 2)) # expect 5Time and space complexity
| Approach | Time | Space |
|---|---|---|
| Sort everything, slice the top | ||
| Min-heap capped at size | ||
heapq.nlargestheapq.nlargest / nsmallestnsmallest | ||
| Quickselect (average case) | extra | |
| Quickselect (worst case) | extra |
When to use it
- The problem only cares about elements, and — a size- heap avoids paying for a full sort.
- The data arrives as a stream you can’t hold in memory all at once — a heap of size never grows past items.
- You need the top- ranked by a derived key (frequency, distance,
custom score) rather than by raw value — pass
key=key=tonlargestnlargest/nsmallestnsmallest. - Reach for Quickselect instead when you need exactly one -th value, the whole array already fits in memory, and average-case matters more than worst-case guarantees.
Practice — real LeetCode problems
Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 347 — Top K Frequent Elements · Medium
Problem. Return the kk most frequent elements, in any order. Your algorithm
must be better than .
Constraints. 1 <= len(nums) <= 10^51 <= len(nums) <= 10^5, kk is valid, and the answer is unique.
Examples. nums = [1,1,1,2,2,3], k = 2nums = [1,1,1,2,2,3], k = 2 gives [1,2][1,2] ·
nums = [1], k = 1nums = [1], k = 1 gives [1][1]
Editorial
Counting is ; the question is how you select the top kk.
| Method | Time | Note |
|---|---|---|
| Sort the counts | Explicitly ruled out | |
most_common(k)most_common(k) | Heap internally; the clean answer | |
Size-kk heap by hand | Same, spelled out | |
| Quickselect on counts | average | Randomised worst case |
| Bucket by frequency | worst case | Counts are bounded by nn |
Bucket sort is the strongest answer and the one worth being able to write:
buckets = [[] for _ in range(len(nums) + 1)]
for value, count in Counter(nums).items():
buckets[count].append(value)
out = []
for count in range(len(buckets) - 1, 0, -1):
for value in buckets[count]:
out.append(value)
if len(out) == k:
return outbuckets = [[] for _ in range(len(nums) + 1)]
for value, count in Counter(nums).items():
buckets[count].append(value)
out = []
for count in range(len(buckets) - 1, 0, -1):
for value in buckets[count]:
out.append(value)
if len(out) == k:
return outNo frequency can exceed len(nums)len(nums), so the value range is bounded and you can index
directly — a genuine with no randomisation. Recognising a bounded range as
licence to bucket is the transferable idea, and it is the same move as
LC 274 H-Index.
Follow-ups: “Truly ?” — bucket by frequency. “Ties?” — the problem guarantees uniqueness; LC 692 makes the tie-break lexicographic, which forces a custom key. “Streaming input?” — counts change over time, so you need a heap with lazy deletion, or a count-min sketch for approximate answers at scale.
LC 658 — Find K Closest Elements · Medium
Problem. Given a sorted array, an integer kk and a value xx, return the
kk closest elements to xx, sorted ascending. Ties prefer the smaller element.
Constraints. 1 <= k <= len(arr) <= 10^41 <= k <= len(arr) <= 10^4, arrarr sorted ascending,
-10^4 <= arr[i], x <= 10^4-10^4 <= arr[i], x <= 10^4.
Examples. arr = [1,2,3,4,5], k = 4, x = 3arr = [1,2,3,4,5], k = 4, x = 3 gives [1,2,3,4][1,2,3,4] ·
arr = [1,2,3,4,5], k = 4, x = -1arr = [1,2,3,4,5], k = 4, x = -1 gives [1,2,3,4][1,2,3,4]
Editorial
The crucial observation is that because the array is sorted, the kk closest
elements form a contiguous window. So instead of selecting kk individual
elements, you only need to locate one index: the window’s start.
Time — the binary search plus the slice. Space beyond the output.
The comparison x - arr[mid] > arr[mid + k] - xx - arr[mid] > arr[mid + k] - x asks: “is the element I would drop
from the left worse than the one I would gain on the right?” If so, slide right.
Written this way — rather than with abs()abs() — it also gets the tie rule right:
on a tie the condition is false, hi = midhi = mid, and the window stays left, preferring
smaller elements.
This is a genuine improvement over the heap approach. A size-kk heap over all
elements is and ignores the sortedness entirely; here the sortedness
buys you .
([1,2,3,4,5], 4, -1)([1,2,3,4,5], 4, -1) giving [1,2,3,4][1,2,3,4] is the out-of-range case: xx sits below
everything, so the leftmost window wins.
Follow-ups: “Do it with a heap?” — ; a valid answer that wastes the
sortedness, and worth naming the contrast. “Two pointers from the middle?” — also
: binary search for xx, then expand outward comparing distances. “Unsorted
input?” — then a heap or quickselect is the right tool.
LC 1481 — Least Number of Unique Integers after K Removals · Medium
Problem. Remove exactly kk elements from the array to minimise the number of
distinct integers remaining. Return that minimum.
Constraints. 1 <= len(arr) <= 10^51 <= len(arr) <= 10^5, 1 <= arr[i] <= 10^91 <= arr[i] <= 10^9,
0 <= k <= len(arr)0 <= k <= len(arr).
Examples. arr = [5,5,4], k = 1arr = [5,5,4], k = 1 gives 11 ·
arr = [4,3,1,1,3,3,2], k = 3arr = [4,3,1,1,3,3,2], k = 3 gives 22
Editorial
The greedy is rarest first, and the exchange argument is short: a distinct value only stops counting once all its occurrences are gone, so partial removals achieve nothing. Given a fixed budget, spending it on the cheapest complete eliminations maximises how many values disappear.
Time for mm distinct values. Space .
The loop stops at the first group it cannot fully afford, and everything from there on
survives — hence len(counts) - ilen(counts) - i. Reaching the end means the budget covered
everything, so 00 remain; ([1], 1)([1], 1) is that case.
([1,2,3], 0)([1,2,3], 0) giving 33 is the zero-budget case: nothing is removed.
Because counts are bounded by len(arr)len(arr), a bucket sort on the counts replaces the
sort with — the same bounded-range observation as LC 347 above.
A min-heap of counts is a third equivalent route and the one that connects to this
page.
Follow-ups: “Prove rarest-first is optimal” — the exchange argument; the likely question. ”?” — bucket the counts. “Maximise the distinct values remaining instead?” — remove from the most frequent, keeping one of each where possible. “What if removals were per-element costly?” — it becomes a knapsack.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 215 | Kth Largest Element in an Array | Medium | The exact size- min-heap template, or heapq.nlargestheapq.nlargest |
| 347 | Top K Frequent Elements | Medium | Count with CounterCounter, then heap on the counts (as above) |
| 973 | K Closest Points to Origin | Medium | Same pattern, ranked by squared distance to the origin instead of raw value |
| 451 | Sort Characters By Frequency | Medium | Count characters, then greedily emit from most to least frequent using a max-heap |
Recap
- The top-k cue: whenever a problem only needs of elements, resist sorting everything.
- A min-heap capped at size tracks the top- largest in total, using only extra space.
heapq.nlargestheapq.nlargest/nsmallestnsmallestgive you the same pattern in one line, with an optionalkey=key=for ranking by a derived value.- Quickselect trades the heap’s streaming ability for average-case , when you only need one -th value from data already in memory.
Next: K-way Merge — the heap pattern for combining many sorted sequences into one.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
