数组中第 K 个最大元素 图解题解
这道题到底在问什么
- 输入
- nums = [5,2,8,1,9,3,7,4,6] k = 3
- 输出
- 7(降序 9,8,7,… 第 3 个)
最优解:为什么这么做
一句话答案:LeetCode 215 数组中第 K 个最大元素的稳妥解法是容量为 k 的小根堆:扫描数组,始终只保留目前最大的 k 个数,堆顶是这 k 个里最小的,也就是当前第 k 大;新数比堆顶大才换顶,否则丢弃。总时间 O(n log k)、空间 O(k),k 远小于 n 时优于整体排序的 O(n log n)。
第 k 大到底指什么
题目要的是把数组降序排列后第 k 个位置上的值,重复的数照常计数,不是第 k 个不同的值。以示例 [5,2,8,1,9,3,7,4,6]、k 取 3 为例,降序前三名是 9、8、7,第 3 大就是 7。理解这一点后,问题可以重述成:从 n 个数里挑出最大的 k 个,报出其中垫底的那一个。
为什么不直接排序取第 k 个
整体排序当然能做:排完取相应下标,时间 O(n log n)。但它做了多余的工作——我们只关心最大的 k 个数是谁,排序却把全部 n 个数的相对顺序都理清楚了,k 远小于 n 时浪费明显。更要命的是排序要求一次拿到全部数据:如果数是流式一个个到来的,排序无从下手,而堆法只需要随来随处理。
求第 k 大为什么用小根堆而不是大根堆
这是全题最反直觉的一步。我们要维护的集合是「目前最大的 k 个数」,而这个集合的门槛,是它内部最小的那个元素——新数只有超过门槛,才有资格挤进来。小根堆恰好把最小值放在堆顶,门槛随手可查、可换。若改用大根堆,堆顶是 k 个里最大的,门槛沉在堆底,判断「新数该不该进」反而没法一步完成。求第 k 大配小根堆、求第 k 小配大根堆,方向记反是本题最高频的错误。
换顶规则为什么能保证答案正确
算法全程维持一条不变量:堆里装的永远是已扫过元素中最大的 k 个。新数到来分两种情况:它不大于堆顶,说明连当前门槛都过不了,绝无可能进入前 k,直接丢弃,堆不动;它比堆顶大,说明原堆顶已经保不住前 k 的席位,弹掉堆顶、放入新数、下沉调整。两种分支处理后不变量都完好,于是扫完全部 n 个数时,堆里正是全局最大的 k 个,堆顶即答案。
复杂度、快速选择与常见坑
每个数至多触发一次 O(log k) 的堆操作,总时间 O(n log k),堆本身只占 O(k) 空间。另一条路线是快速选择(quickselect):借快排的 partition 平均 O(n) 定位第 k 大,但最坏退化到 O(n²),且需要全量数据在手;堆法稳定、好写、天然适配数据流场景,LC703 数据流中的第 K 大就是同一套思路的流式版。两个常见坑:把整个数组不设上限地塞进堆,复杂度退化成 O(n log n);新数不超过堆顶时也去入堆替换,纯属无效操作还会挤错元素。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住「小根堆只留最大的 k 个,堆顶即第 k 大」,下面每来一个数都在套它。
- 4k = 3开局:一个空的小根堆,容量上限 3。我们要让它始终只保留「目前最大的 3 个数」,堆顶是这几个里最小的。
- 5num = 5第 1 个数 5。堆还没满 3 个,先看看。
- 6push 5堆没满,直接放进堆并上浮到正确位置。现在堆里是 5,堆顶(最小)是 5。
- 7num = 2第 2 个数 2。堆还没满 3 个,先看看。
- 8push 2堆没满,直接放进堆并上浮到正确位置。现在堆里是 2 5,堆顶(最小)是 2。
- 9num = 8第 3 个数 8。堆还没满 3 个,先看看。
- 10push 8堆没满,直接放进堆并上浮到正确位置。现在堆里是 2 5 8,堆顶(最小)是 2。
- 11num = 1第 4 个数 1。堆已满,拿它和堆顶(最小的 2)比一比。
- 121 ≤ 2,跳过1 比堆顶 2 小(或相等)→ 它进不了「最大的 3 个」,直接丢弃。堆不变,堆顶仍是 2。
- 13num = 9第 5 个数 9。堆已满,拿它和堆顶(最小的 2)比一比。
- 149 > 2,替换9 比堆顶 2 大 → 说明 2 不可能是最大的 3 个之一,踢掉 2、换成 9 再下沉调整。新堆 5 9 8,堆顶 5。
- 15num = 3第 6 个数 3。堆已满,拿它和堆顶(最小的 5)比一比。
- 163 ≤ 5,跳过3 比堆顶 5 小(或相等)→ 它进不了「最大的 3 个」,直接丢弃。堆不变,堆顶仍是 5。
- 17num = 7第 7 个数 7。堆已满,拿它和堆顶(最小的 5)比一比。
- 187 > 5,替换7 比堆顶 5 大 → 说明 5 不可能是最大的 3 个之一,踢掉 5、换成 7 再下沉调整。新堆 7 9 8,堆顶 7。
- 19num = 4第 8 个数 4。堆已满,拿它和堆顶(最小的 7)比一比。
- 204 ≤ 7,跳过4 比堆顶 7 小(或相等)→ 它进不了「最大的 3 个」,直接丢弃。堆不变,堆顶仍是 7。
- 21num = 6第 9 个数 6。堆已满,拿它和堆顶(最小的 7)比一比。
- 226 ≤ 7,跳过6 比堆顶 7 小(或相等)→ 它进不了「最大的 3 个」,直接丢弃。堆不变,堆顶仍是 7。
- 23第 3 大 = 7扫完整个数组,小根堆里留下的正是最大的 3 个数 9、8、7,而堆顶(它们中最小的)7 就是第 3 大。答案 = 7。
⚠️ 容易写错的地方
✗ 错:求第 k 大却用大根堆
✓ 对:求第 k 大用小根堆
小根堆留最大的 k 个、堆顶是其中最小=第 k 大;大根堆会留最小的 k 个,方向反了
✗ 错:把整个数组都塞进堆
✓ 对:堆大小封顶 k
塞满 n 个就退化成 O(n·log n),失去 top-k 的省内存优势
✗ 错:新数 ≤ 堆顶还入堆
✓ 对:只有「比堆顶大」才换顶
更小的数不可能进入最大的 k 个,入堆纯属浪费且会挤错
完整代码(Python / Java / C++)
Python
import heapq
def findKthLargest(nums, k):
h = [] # 小根堆
for x in nums:
if len(h) < k:
heapq.heappush(h, x) # 没满直接放
elif x > h[0]:
heapq.heapreplace(h, x) # 比堆顶大→换顶
# 否则丢弃
return h[0] # 堆顶即第 k 大Java
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> h = new PriorityQueue<>(); // 小根堆
for (int x : nums) {
if (h.size() < k) h.offer(x); // 没满直接放
else if (x > h.peek()) { // 比堆顶大
h.poll(); h.offer(x); // 换顶
}
}
return h.peek(); // 堆顶即第 k 大
}C++
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> h; // 小根堆
for (int x : nums) {
if ((int)h.size() < k) h.push(x); // 没满直接放
else if (x > h.top()) { // 比堆顶大
h.pop(); h.push(x); // 换顶
}
}
return h.top(); // 堆顶即第 k 大
}复杂度
时间
O(n·log k)
n 个数各做一次 O(log k) 的堆操作
空间
O(k)
堆里最多只装 k 个元素
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 数组中第 K 个最大元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了堆,还有别的解法吗?+
有快速选择(quickselect):基于快排的 partition,平均 O(n) 就能定位第 k 大,但最坏 O(n²)。堆法稳定 O(n·log k)、实现简单、适合数据流;quickselect 更快但要会写 partition。
如果是数据流(数不断来)求第 k 大呢?+
堆法天然适合:维护大小 k 的小根堆,每来一个数按同样规则更新,堆顶随时就是当前第 k 大(LC703)。
为什么不直接排序取第 k 个?+
排序是 O(n·log n),当 k 远小于 n 时,堆法 O(n·log k) 更省;且排序需要拿到全部数据,数据流场景下做不到。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 数组中第 K 个最大元素 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。