Skip to content
DSA Patterns

Learn

Top 'K' Elements

The top-K pattern: why a size-K heap of the opposite type beats sorting, when quickselect gets you O(n), and the bucket-sort trick for frequency questions.

3 min readUpdated 2 Sept 2026

#Heap#Sorting#Quickselect#Priority Queue

Any question containing "the k largest", "the k most frequent", "the k closest" is this pattern. Sorting answers all of them in O(n log n) and will be accepted — the interview is about knowing that O(n log k) and sometimes O(n) are available, and why.

The counter-intuitive core

To keep the K largest elements, use a MIN-heap of size K.

That inversion trips people up every time, and the reasoning is short: the heap's root is the worst thing you are currently keeping. Each new element only needs comparing against that one value — better, so evict the root and push; worse, so discard. A max-heap would put the best element at the root, which tells you nothing about what to throw away.

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 we are holding
    return heap

The heap never exceeds k entries, so each push and pop is O(log k) and the whole scan is O(n log k) with O(k) space. On a stream, or on n in the millions with k in the tens, that gap is the answer.

Python's heapq is a min-heap only. For a max-heap, push negated values — and remember to negate back on the way out.

Frequency questions

Top K Frequent Elements adds a counting pass, then applies the same heap to the

(count, value) pairs:

from collections import Counter

def top_k_frequent(nums, k):
    counts = Counter(nums)
    return heapq.nlargest(k, counts.keys(), key=counts.get)

But this problem has a better answer, and it is the one to reach for: bucket sort. A frequency can never exceed n, so index an array by count and walk it downward.

def top_k_frequent_buckets(nums, k):
    counts = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for value, count in counts.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
    return out

O(n) time. Whenever the key you are sorting by is a bounded integer, bucketing beats a heap — that generalisation is worth more than the specific problem.

Quickselect: O(n) average

Kth Largest Element in an Array is the problem where the interviewer wants to hear

"quickselect". Partition like quicksort, but recurse into only the side containing the answer:

import random

def quickselect(nums, k):
    """kth largest = index len(nums) - k in sorted order."""
    target = len(nums) - k
    lo, hi = 0, len(nums) - 1

    while True:
        pivot_index = random.randint(lo, hi)
        nums[pivot_index], nums[hi] = nums[hi], nums[pivot_index]
        store = lo
        for i in range(lo, hi):
            if nums[i] < nums[hi]:
                nums[i], nums[store] = nums[store], nums[i]
                store += 1
        nums[store], nums[hi] = nums[hi], nums[store]

        if store == target:
            return nums[store]
        if store < target:
            lo = store + 1
        else:
            hi = store - 1

Recursing into one side gives n + n/2 + n/4 + … = 2n, so O(n) average. The random pivot is not optional — without it, sorted input degrades to O(n²), and that is the follow-up question.

Picking the approach

SituationUseCost
Streaming, or k ≪ nSize-K heapO(n log k)
One-shot, array in memoryQuickselectO(n) average
Sorting key is a bounded integerBucket sortO(n)
K close to n, or k unknownJust sortO(n log n)

K Closest Points to Origin is the heap with squared distance as the key — do not take

the square root, it costs time and changes no ordering. Task Scheduler is a max-heap on remaining counts, or the closed-form idle-slot formula if you spot it.

Complexity

Size-K heap: O(n log k) time, O(k) space. Quickselect: O(n) average, O(n²) worst, O(1) space, but it mutates the input. Bucket sort: O(n) both. Being able to name all three and justify the choice is what the question is actually testing.

Mistakes that cost the round

  • Using a max-heap for the K largest. The root must be the item you are most willing

to evict.

  • Letting the heap grow to n. heappush then heappop at size k + 1, not at the

end — otherwise it is O(n log n) with extra steps.

  • Forgetting to re-negate after the negation trick for a max-heap.
  • Quickselect with a fixed pivot, which is O(n²) on sorted input.
  • Taking square roots in K Closest Points.

What to drill

  1. Kth Largest Element in an Array — heap, then quickselect.
  2. Top K Frequent Elements — heap, then bucket sort.
  3. K Closest Points to Origin — the heap with a custom key.
  4. Task Scheduler — max-heap scheduling, or the formula.

All on the 22 DSA Patterns sheet. The two heaps pattern is the next step up.

Frequently asked

Why use a min-heap to find the K largest elements?

Because the root of a size-K min-heap is the smallest element you are currently keeping — the one to evict when something better arrives. Each new element is compared against that single value in O(log k). A max-heap would surface the best element instead, which tells you nothing about what to discard.

Is quickselect better than a heap for top K?

For a one-shot query on an in-memory array, yes: O(n) average versus O(n log k). But it mutates the input, degrades to O(n²) without a random pivot, and cannot handle a stream. A size-K heap is the right answer when data arrives incrementally or k is much smaller than n.

How do you find top K frequent elements in O(n)?

Bucket sort. Count frequencies, then index an array of lists by frequency — no count can exceed n, so the array is bounded. Walk it from the highest count downward and collect until you have K. That avoids the log factor a heap or a sort would add.

Related