Skip to content

Top K Elements

Whenever a problem asks for the ”kk largest”, ”kk most frequent”, ”kk closest”, or ”kk-th something”, your brain should immediately reach for one tool: a heap capped at size kk. You almost never need to sort the whole input to answer a question about only kk of its elements — and that gap between “sort everything” and “just track the top kk” 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 kk.
  • heapq.nlargestheapq.nlargest / heapq.nsmallestheapq.nsmallest — the one-line version of the same idea.
  • Quickselect — the O(n)O(n) average-case alternative, and its trade-offs.
  • Why a size-kk heap beats a full sort: O(nlogk)O(n \log k) vs. O(nlogn)O(n \log n).

The cue: “top k”, “kth largest”, “k closest”

Any phrasing that only cares about kk elements out of nn — 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 kk”. Sorting the entire array to read off the last kk elements does O(nlogn)O(n \log n) work to answer a question that only needs O(nlogk)O(n \log k).

The pattern: a min-heap capped at size k

To find the kk largest values, keep a min-heap (not a max-heap) of size kk. 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.

top_k_pattern.py
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]
top_k_pattern.py
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 kk 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

sketch A min-heap capped at size k=3 p5.js
Each incoming number either fills an empty slot, replaces the heap's current minimum, or is discarded outright if it can't beat the worst of the top-k so far.

Worked example: Top K Frequent Elements

Count occurrences with a CounterCounter, then let heapq.nlargestheapq.nlargest do the size-kk heap work for you, ranking by count instead of by raw value.

top_k_frequent.py
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 2x
top_k_frequent.py
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 2x

heapq.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 kk-th value itself (not the full sorted top-kk), the Quickselect algorithm — a partition step borrowed from quicksort — finds it in O(n)O(n) average time, faster than any heap-based approach for a single query.

quickselect_kth_largest.py
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 5
quickselect_kth_largest.py
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 5

Time and space complexity

ApproachTimeSpace
Sort everything, slice the top kkO(nlogn)O(n \log n)O(n)O(n)
Min-heap capped at size kkO(nlogk)O(n \log k)O(k)O(k)
heapq.nlargestheapq.nlargest / nsmallestnsmallestO(nlogk)O(n \log k)O(k)O(k)
Quickselect (average case)O(n)O(n)O(1)O(1) extra
Quickselect (worst case)O(n2)O(n^2)O(1)O(1) extra

When to use it

  • The problem only cares about kk elements, and knk \ll n — a size-kk heap avoids paying for a full O(nlogn)O(n \log n) sort.
  • The data arrives as a stream you can’t hold in memory all at once — a heap of size kk never grows past kk items.
  • You need the top-kk ranked by a derived key (frequency, distance, custom score) rather than by raw value — pass key=key= to nlargestnlargest / nsmallestnsmallest.
  • Reach for Quickselect instead when you need exactly one kk-th value, the whole array already fits in memory, and average-case O(n)O(n) 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 O(nlogn)O(n \log n).

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 O(n)O(n); the question is how you select the top kk.

MethodTimeNote
Sort the countsO(nlogn)O(n \log n)Explicitly ruled out
most_common(k)most_common(k)O(nlogk)O(n \log k)Heap internally; the clean answer
Size-kk heap by handO(nlogk)O(n \log k)Same, spelled out
Quickselect on countsO(n)O(n) averageRandomised worst case
Bucket by frequencyO(n)O(n) worst caseCounts are bounded by nn

Bucket sort is the strongest answer and the one worth being able to write:

python
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 out
python
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 out

No frequency can exceed len(nums)len(nums), so the value range is bounded and you can index directly — a genuine O(n)O(n) 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 O(n)O(n)?” — 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 O(log(nk)+k)O(\log(n - k) + k) — the binary search plus the slice. Space O(1)O(1) 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 O(nlogk)O(n \log k) and ignores the sortedness entirely; here the sortedness buys you O(logn)O(\log n).

([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?” — O(nlogk)O(n \log k); a valid answer that wastes the sortedness, and worth naming the contrast. “Two pointers from the middle?” — also O(n)O(n): 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 O(n+mlogm)O(n + m \log m) for mm distinct values. Space O(m)O(m).

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 O(mlogm)O(m \log m) sort with O(n)O(n) — 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. ”O(n)O(n)?” — 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

#ProblemDifficultyThe twist
215Kth Largest Element in an ArrayMediumThe exact size-kk min-heap template, or heapq.nlargestheapq.nlargest
347Top K Frequent ElementsMediumCount with CounterCounter, then heap on the counts (as above)
973K Closest Points to OriginMediumSame pattern, ranked by squared distance to the origin instead of raw value
451Sort Characters By FrequencyMediumCount characters, then greedily emit from most to least frequent using a max-heap

Recap

  • The top-k cue: whenever a problem only needs kk of nn elements, resist sorting everything.
  • A min-heap capped at size kk tracks the top-kk largest in O(nlogk)O(n \log k) total, using only O(k)O(k) extra space.
  • heapq.nlargestheapq.nlargest / nsmallestnsmallest give you the same pattern in one line, with an optional key=key= for ranking by a derived value.
  • Quickselect trades the heap’s streaming ability for average-case O(n)O(n), when you only need one kk-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 coffee

Was this page helpful?

Let us know how we did