FDEInterviews logo
Coding & DSA / 05
easy★ EssentialMetaGleanOpenAI

Top-K frequent elements

Top-K is the most reused primitive in FDE interviews: it reappears inside log parsers, analytics questions, and retrieval ranking. Three solutions exist; knowing which one to lead with is the real test.

Updated Aug 2026 · Grounded in real Forward Deployed Engineer interview loops and written to a senior-engineer editorial bar.

TL;DR: Count with a Counter, then select the top k with a size-k heap via heapq.nlargest at O(n log k). Mention bucket sort by count for O(n) when frequencies are bounded.

How to approach it Two phases: count, then select. Counting is always a hashmap (Counter). Selection has three options, and a strong candidate names all three with costs before picking: full sort O(n log n), heap of size k O(n log k), bucket sort by count O(n). Ask the calibrating question: how big is k relative to n? For "top 10 of 10 million log lines" the heap is the obvious choice, and that's exactly the shape of the log-parsing questions later in these loops.

A strong answer Lead with the heap version since it generalizes to streams and big data:

import heapq
from collections import Counter

def top_k_frequent(nums: list[int], k: int) -> list[int]:
    counts = Counter(nums)
    # nlargest keeps a min-heap of size k internally: O(n log k)
    return [x for x, _ in heapq.nlargest(k, counts.items(), key=lambda kv: kv[1])]

Knowing that heapq.nlargest exists, instead of hand-rolling push/pop, is an idiomatic-Python signal these loops reward. If asked for the O(n) version, sketch bucket sort: index buckets by count (count is at most n), then walk from the highest bucket down:

def top_k_frequent_buckets(nums: list[int], k: int) -> list[int]:
    counts = Counter(nums)
    buckets: list[list[int]] = [[] for _ in range(len(nums) + 1)]
    for x, c in counts.items():
        buckets[c].append(x)
    out = []
    for c in range(len(buckets) - 1, 0, -1):
        out.extend(buckets[c])
        if len(out) >= k:
            return out[:k]
    return out

What nlargest does internally is worth tracing once, because "min-heap of size k" confuses people until they see that the smallest element guards the door. Take k = 2 over counted items a:5, b:2, c:8, d:1, e:7 (traced by running it):

IncomingHeap before (min on left)DecisionHeap after
a:5emptyfill[a:5]
b:2[a:5]fill[b:2, a:5]
c:8[b:2, a:5]8 > 2, replace min[a:5, c:8]
d:1[a:5, c:8]1 < 5, skip[a:5, c:8]
e:7[a:5, c:8]7 > 5, replace min[e:7, c:8]

Each newcomer fights only the weakest current member, one O(log k) operation, and everything that cannot beat the weakest is discarded in O(1). That is why the memory never exceeds k and why the pattern survives streams: nothing about the loop needs to see the data twice. It is the same shape as "keep the 10 slowest queries seen so far" in a log tail, which is where this warm-up actually reappears in FDE work.

Tests, including the tie case (clarify: is any tie order acceptable?):

assert top_k_frequent([1, 1, 1, 2, 2, 3], 2) == [1, 2]
assert top_k_frequent([7], 1) == [7]
assert set(top_k_frequent([1, 2], 2)) == {1, 2}        # tie, order unspecified
assert top_k_frequent([], 0) == []

The three selection approaches, and when each one wins:

ApproachTimeSpaceReach for it when
Full sort by countO(n log n)O(n)k is close to n, or you want the full ranking anyway
Min-heap of size kO(n log k)O(k)Default. k is much smaller than n (top 10 of 10M)
Bucket by frequencyO(n)O(n)Frequencies are bounded and you want linear time

What interviewers probe next (1) Deterministic tie-breaking? Change the key to (count, value) or (count, -first_seen_index), and note that the requirement should come from the user, not from you. (2) Streaming, can't hold all counts? Honest answer first: exact top-K over an unbounded stream needs all counts; then name Count-Min Sketch / lossy counting as approximations, one sentence each, don't derive them. (3) Top-K per group (per hour, per user): dict of Counters, then per-group nlargest; this is the bridge to the log-parser question and saying so shows you see the pattern. (4) Why not Counter.most_common(k)? It's fine: it does sort-or-heap internally; knowing it exists is a plus, knowing its cost is a bigger plus.

Common mistakes Sorting the entire count table when k is tiny and saying nothing about it: the answer "sort works, heap is better when k is much smaller than n" costs one sentence. Building a max-heap of all n items (O(n) memory) when a size-k min-heap was the point. Hand-implementing heap mechanics badly under time pressure instead of using heapq's batteries. And forgetting that ties exist until a test fails: clarifying tie semantics up front is the cheap, senior-looking move.

That one was free — and so are 10 answers per topic without an account. Signing in doubles that to 20, opens the Plus lessons in the courses, and remembers which topics you keep getting wrong.no card · Google sign-in · nothing to cancel
HOW DID IT GO?
0
READING SIGNED OUT

Signing in doubles your free answers, from 10 to 20 per topic, and the site starts remembering you: mastery per topic, bookmarks, and a next-focus recommendation. Free, no card.

Sign in free
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

The crisp move is to lead with the size-k heap at O(n log k) rather than sorting all counts at O(n log n), then mention bucket sort as the O(n) option when frequencies are bounded; naming all three and picking one is the signal. The follow-up that exposes a shallow answer is 'what if the stream is unbounded?', where you cannot sort at all and the heap or a count-min sketch becomes the only viable approach.

DISCUSSION · 0

No comments yet — be the first to share your approach.