Run it
Sorting everything against a heap of size k, at 200,000 items.
And the O(n) version, which removes the log factor without necessarily winning the clock.
Where the log k actually comes from: how little of the input ever enters the heap.
Why the root is the whole trick
A min-heap of size k has a useful property: its root is the weakest member of the best k seen so far. So the question "could this candidate belong in the answer?" is one comparison against the root, and if the answer is no the candidate is discarded without ever entering the heap.
The editor below counts it: with 200,000 items and k = 10, only 136 of them ever touched the heap. The other 199,864 cost one comparison each. That is where the log k comes from — not from clever bookkeeping, but from rejecting almost everything cheaply.
The three answers, and when each is right
Heap of size k — O(n log k). The default answer, and the one to give first. heapq.nlargest(k, ...) is this, already written.
Full sort — O(n log n). Better when k is a large fraction of n, because then log k is log n and the heap is paying overhead for nothing. Also better when n is small enough that constants dominate, which is more often than the complexity suggests: CPython's sort is C and a heap loop is Python.
Bucket sort — O(n). Available because the counts are bounded: a count cannot exceed n, so you can bucket by count and read the buckets from the top with no comparisons at all. It is the answer an interviewer is fishing for when they ask "can you do better than n log k?" — though note what the editor measures: it comes out level with nlargest rather than ahead, because removing a log factor does not beat a C implementation at this size. The complexity win is real; the wall-clock win needs a much larger n or k.
Quickselect, and why it is usually the wrong answer to give
There is an O(n) average-case option: partition around a pivot like quicksort but recurse into one side only, stopping when the pivot lands at position k. It is genuinely O(n) expected and O(n²) worst case, and numpy.partition implements it.
Mentioning it is good; reaching for it first is usually not. It mutates the input, it does not stream, the worst case needs median-of-medians to fix, and the constant factor means it rarely beats nlargest in practice. The strong answer is the heap, with quickselect named as the theoretical improvement.
What the question is testing
Whether you notice that the answer set is bounded. A great many problems say "the best k" and the instinct is to sort everything and take a slice — which computes a total order nobody asked for. Keeping only k candidates and rejecting against the weakest of them is the reusable move, and it reappears in streaming top-n, nearest-neighbour search and leaderboards.
The second thing is whether you ask about ties. "The k most frequent" is ambiguous when counts are equal, and saying so before writing is the difference between a correct answer and a complete one.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
Ties. "The k most frequent" is ambiguous when counts are equal. Ask, or state the tie-break - and note that the tuple in the heap already picks one for you, probably not the one you meant.
k larger than the number of distinct values. nlargest returns everything; a hand-rolled heap should too, rather than raising.
k = 0. An empty answer, and a loop that assumes the heap is non-empty will index a root that is not there.