题目描述
思路解析动画文字版
为什么用「最小堆」?因为我们要不停淘汰「最该走的」,让它永远浮在堆顶,超员就弹它。难点在比较器是「双重」的:频次升序、同频字典序降序——这样弹掉的总是该淘汰那个。
数频次。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
数频次。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
数频次。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
频次数完。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
准备进堆。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
入堆 i(×2)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
i 入堆完成。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
入堆 love(×2)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
比较 love(×2) 与父 i(×2)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
上浮:love(×2) 换到上面。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
love 入堆完成。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
入堆 leetcode(×1)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
比较 leetcode(×1) 与父 love(×2)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
上浮:leetcode(×1) 换到上面。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
leetcode 入堆完成。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
弹出堆顶 leetcode(×1)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
末尾 love 补到堆顶。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
堆已恢复最小堆。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
淘汰 leetcode(×1),堆恢复大小 2。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
入堆 coding(×1)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
比较 coding(×1) 与父 love(×2)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
上浮:coding(×1) 换到上面。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
coding 入堆完成。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
弹出堆顶 coding(×1)。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
末尾 love 补到堆顶。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
堆已恢复最小堆。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
淘汰 coding(×1),堆恢复大小 2。节点显示「单词(×频次)」,堆按「频次 + 字典序」做最小堆——堆顶永远是最该淘汰的(频次最小,同频则字母最大),超过 k 个就弹掉它。
堆里最终留下 i(×2) 和 love(×2)(绿色)= 前 2 个高频单词。leetcode 和 coding 频次最低被弹出。两者同频 ×2,按字母序 i 排在 love 前 = 答案 ["i", "love"]。
边界都不破坏「最小堆 + 双重比较」的主逻辑,全并列时退化成纯字典序取前 k。
两个高频追问:桶排序优化、比较器方向写反的后果——并列样例是检验比较器的试金石。
参考代码
import heapqfrom collections import Counterdef topKFrequent(words, k): cnt = Counter(words) # 1) 数频次 # 最小堆:堆顶=最该淘汰者 # 频次小的先出 → 用 fr;同频字母大的先出 → 用 word # heapq 是小根堆:fr 直接用,word 取反序则用 (-fr 思路); # 这里把 key 设成 (fr, 反字典序) 让"最该淘汰"在堆顶。 class W: def __init__(s, w, f): s.w, s.f = w, f def __lt__(s, o): # s 比 o 更该淘汰? if s.f != o.f: return s.f < o.f # 频次小者先 return s.w > o.w # 同频字典序大者先 heap = [] for w, f in cnt.items(): heapq.heappush(heap, W(w, f)) if len(heap) > k: heapq.heappop(heap) # 弹掉最该淘汰者 # 取出后按 频次降序、同频字典序升序 排成答案 return [x.w for x in sorted(heap, key=lambda x:(-x.f, x.w))]复杂度
- 时间:O(n log k),n 个不同单词各做一次 O(log k) 的堆操作(比较含字符串比较)
- 空间:O(n + k),哈希表 O(n) 存频次,堆 O(k)
易错点
面试追问把动画讲成自己的话
追问能不能做到 O(n) 或 O(n log k) 以下?
追问PriorityQueue 的比较器写反了会怎样?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
重构字符串
LeetCode 767 · 中等 · 沿着 堆套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题