Return the k most frequent words, sorted by descending count with ties broken lexicographically (smaller word first). Count, then sift the top k with a carefully-ordered size-k min-heapin O(n log k) — the entire trick is the comparator's tie-break.
Given a list of words and an integer k, return the k most frequent words. The answer must be sorted by frequency from highest to lowest; words with the same frequency are ordered by their lexicographic (alphabetical) order. Example: words = ["i","love","leetcode","i","love","coding"], k = 2 → i and love each appear 2× (everything else 1×), so the answer is ["i", "love"] (i before love alphabetically).
Map<word, count>.a is worse than b" (sinks toward the top of the min-heap) as: freq[a] < freq[b], OR equal frequency AND a > b alphabetically. The worst entry is evicted first.k, pop the worst. The heap settles holding exactly the k best.reverse() to get best-to-worst — the required output order."banana" over "apple" on a tie. The final reverse() is also easy to forget.Min-heap (optimal for small k): count, then push the distinct words into a size-k min-heap whose comparator evicts lower-frequency (and, on ties, alphabetically-larger) words. Each of the d distinct words costs O(log k), so it's O(n + d log k) ≈ O(n log k). Drain the heap and reverse.
Sort: the simplest correct idea — dump the distinct words into an array, sort by frequency descending then word ascending, and take the first k. Easy to write but O(n log n).
Bucket sort: bucket words by their count (counts are bounded by n), then walk buckets high → low; inside each bucket sort the tied words alphabetically. O(n + n log n) in the worst case because of the within-bucket sorts.
2 most frequent words (ties broken alphabetically). Phase 1: count every word in one pass.1▶function topKFrequent(words: string[], k: number): string[] {2 // 1) Count every word in one pass.3▶ const freq = new Map<string, number>();4 for (const w of words) {5 freq.set(w, (freq.get(w) ?? 0) + 1);6 }78 // 2) Size-k MIN-heap. The "smallest" entry is the one we want to9 // evict first: lower frequency loses; on a tie, the10 // LEXICOGRAPHICALLY LARGER word loses (so it sinks to the top).11 const worseThan = (a: string, b: string): boolean =>12 freq.get(a)! < freq.get(b)! ||13 (freq.get(a)! === freq.get(b)! && a > b);1415 const heap: string[] = []; // binary min-heap kept at <= k entries16 const up = (i: number) => {17 while (i > 0) {18 const p = (i - 1) >> 1;19 if (!worseThan(heap[i], heap[p])) break;20 [heap[i], heap[p]] = [heap[p], heap[i]];21 i = p;22 }23 };24 const down = (i: number) => {25 const n = heap.length;26 for (;;) {27 let s = i;28 const l = 2 * i + 1, r = 2 * i + 2;29 if (l < n && worseThan(heap[l], heap[s])) s = l;30 if (r < n && worseThan(heap[r], heap[s])) s = r;31 if (s === i) break;32 [heap[i], heap[s]] = [heap[s], heap[i]];33 i = s;34 }35 };3637 for (const w of freq.keys()) {38 heap.push(w);39 up(heap.length - 1);40 if (heap.length > k) { // overflow: drop the worst entry41 [heap[0], heap[heap.length - 1]] = [heap[heap.length - 1], heap[0]];42 heap.pop();43 down(0);44 }45 }4647 // 3) Heap holds the k best, worst-first. Pop all, then reverse.48 const res: string[] = [];49 while (heap.length) {50 res.push(heap[0]);51 heap[0] = heap[heap.length - 1];52 heap.pop();53 down(0);54 }55 return res.reverse();56}
function topKFrequent(words: string[], k: number): string[] {
// 1) Count every word in one pass.
const freq = new Map<string, number>();
for (const w of words) {
freq.set(w, (freq.get(w) ?? 0) + 1);
}
// 2) Size-k MIN-heap. The "smallest" entry is the one we want to
// evict first: lower frequency loses; on a tie, the
// LEXICOGRAPHICALLY LARGER word loses (so it sinks to the top).
const worseThan = (a: string, b: string): boolean =>
freq.get(a)! < freq.get(b)! ||
(freq.get(a)! === freq.get(b)! && a > b);
const heap: string[] = []; // binary min-heap kept at <= k entries
const up = (i: number) => {
while (i > 0) {
const p = (i - 1) >> 1;
if (!worseThan(heap[i], heap[p])) break;
[heap[i], heap[p]] = [heap[p], heap[i]];
i = p;
}
};
const down = (i: number) => {
const n = heap.length;
for (;;) {
let s = i;
const l = 2 * i + 1, r = 2 * i + 2;
if (l < n && worseThan(heap[l], heap[s])) s = l;
if (r < n && worseThan(heap[r], heap[s])) s = r;
if (s === i) break;
[heap[i], heap[s]] = [heap[s], heap[i]];
i = s;
}
};
for (const w of freq.keys()) {
heap.push(w);
up(heap.length - 1);
if (heap.length > k) { // overflow: drop the worst entry
[heap[0], heap[heap.length - 1]] = [heap[heap.length - 1], heap[0]];
heap.pop();
down(0);
}
}
// 3) Heap holds the k best, worst-first. Pop all, then reverse.
const res: string[] = [];
while (heap.length) {
res.push(heap[0]);
heap[0] = heap[heap.length - 1];
heap.pop();
down(0);
}
return res.reverse();
}words builds a Map from each word to its frequency. The ?? 0 seeds first-time keys.worseThan(a, b) is true when a should sink toward the top of the min-heap (i.e. be evicted first): lower frequency, or — on equal frequency — the alphabetically larger word. This flips the tie-break versus the final answer on purpose.upsifts a freshly-pushed entry toward the root while it's "worse than" its parent; down sinks the root after we replace it. Both compare with worseThan, so the root is always the single entry we most want to discard.k, swap the worst (root) to the end, pop it, and sink the new root. The heap is invariably the k best seen so far.reverse() to produce the required frequency-descending, alphabetically-ascending order.d distinct words, each heap push/evict is O(log k), so sifting is O(d log k) and draining is O(k log k); total O(n + d log k) ≈ O(n log k) time and O(n) space (the count map). The pure-sort approach is O(n log n) time.(freq desc, word asc) and take the first k — O(n log n).function topKFrequent(words: string[], k: number): string[] {
// 1) Count every word in one pass.
const freq = new Map<string, number>();
for (const w of words) {
freq.set(w, (freq.get(w) ?? 0) + 1);
}
// 2) Size-k MIN-heap. The "smallest" entry is the one we want to
// evict first: lower frequency loses; on a tie, the
// LEXICOGRAPHICALLY LARGER word loses (so it sinks to the top).
const worseThan = (a: string, b: string): boolean =>
freq.get(a)! < freq.get(b)! ||
(freq.get(a)! === freq.get(b)! && a > b);
const heap: string[] = []; // binary min-heap kept at <= k entries
const up = (i: number) => {
while (i > 0) {
const p = (i - 1) >> 1;
if (!worseThan(heap[i], heap[p])) break;
[heap[i], heap[p]] = [heap[p], heap[i]];
i = p;
}
};
const down = (i: number) => {
const n = heap.length;
for (;;) {
let s = i;
const l = 2 * i + 1, r = 2 * i + 2;
if (l < n && worseThan(heap[l], heap[s])) s = l;
if (r < n && worseThan(heap[r], heap[s])) s = r;
if (s === i) break;
[heap[i], heap[s]] = [heap[s], heap[i]];
i = s;
}
};
for (const w of freq.keys()) {
heap.push(w);
up(heap.length - 1);
if (heap.length > k) { // overflow: drop the worst entry
[heap[0], heap[heap.length - 1]] = [heap[heap.length - 1], heap[0]];
heap.pop();
down(0);
}
}
// 3) Heap holds the k best, worst-first. Pop all, then reverse.
const res: string[] = [];
while (heap.length) {
res.push(heap[0]);
heap[0] = heap[heap.length - 1];
heap.pop();
down(0);
}
return res.reverse();
}d distinct words, each heap push/evict is O(log k), so sifting is O(d log k) and draining is O(k log k); total O(n + d log k) ≈ O(n log k) time and O(n) space (the count map). The pure-sort approach is O(n log n) time.The most direct correct approach: tally every word, sort the distinct words by descending frequency with ties broken alphabetically, and slice off the first k. No heap, and the comparator reads exactly like the required answer order.
function topKFrequent(words: string[], k: number): string[] {
// 1) Count every word in one pass.
const freq = new Map<string, number>();
for (const w of words) {
freq.set(w, (freq.get(w) ?? 0) + 1);
}
// 2) Sort distinct words: higher frequency first, ties alphabetical.
const distinct = [...freq.keys()];
distinct.sort((a, b) =>
freq.get(b)! - freq.get(a)! || (a < b ? -1 : a > b ? 1 : 0),
);
// 3) The first k are the answer, already in the required order.
return distinct.slice(0, k);
}O(n log n). When k is small relative to the number of distinct words, the size-k min-heap is faster at O(n log k).Counts are bounded by n, so bucket words by their frequency and walk buckets from highest to lowest; sort each bucket's tied words alphabetically before harvesting.
function topKFrequent(words: string[], k: number): string[] {
const freq = new Map<string, number>();
for (const w of words) freq.set(w, (freq.get(w) ?? 0) + 1);
// buckets[c] = all words seen exactly c times.
const buckets: string[][] = Array.from({ length: words.length + 1 }, () => []);
for (const [w, c] of freq) buckets[c].push(w);
const res: string[] = [];
for (let c = buckets.length - 1; c >= 1 && res.length < k; c--) {
if (buckets[c].length === 0) continue;
buckets[c].sort(); // ties: lexicographic ascending
for (const w of buckets[c]) {
res.push(w);
if (res.length === k) return res;
}
}
return res;
}O(n log n). It avoids any heap bookkeeping and is clean when ties are few.| "k most frequent words" | count map → size-k min-heap (O(n log k)) |
| ties broken alphabetically | flip tie-break in the min-heap comparator |
| want the simplest correct code | sort by (freq desc, word asc), take k |
| "top k" / "k closest" / "kth largest" | size-k heap, evict the worst |
const freq = new Map<string, number>();
for (const w of words) freq.set(w, (freq.get(w) ?? 0) + 1);
// min-heap: evict lower freq first; on a tie, evict the LARGER word
const worse = (a, b) =>
freq.get(a)! < freq.get(b)! ||
(freq.get(a)! === freq.get(b)! && a > b);
const heap = []; // keep size <= k
for (const w of freq.keys()) {
push(w); // sift up by 'worse'
if (heap.length > k) popMin(); // drop the worst
}
return drainAll().reverse(); // worst-first -> best-last -> reverse