堆与优先队列实现实时排行榜 / TopK 流
本节目标
- 用堆实现"实时排行榜"(支持插入与取 TopK)
- 理解"海量数据流 TopK"的流式处理
- 体会优先队列在调度 / 限流中的价值
场景:直播弹幕实时打分,要随时给出"当前分最高的前 10 名"。用最大堆维护 TopK:新分数来了就入堆,堆超 K 就弹出当前最小(若用最小堆)或最大(若用最大堆保留 TopK 最小的)。下面用最小堆保留"前 K 大":
// 运行环境:Node.js 14+
// 保存为 dsa-l24.js,执行:node dsa-l24.js
class MinHeap {
#h = []
push(x) { this.#h.push(x); this.#siftUp(this.#h.length - 1) }
pop() { if (!this.#h.length) return undefined; const t = this.#h[0], l = this.#h.pop(); if (this.#h.length) { this.#h[0] = l; this.#siftDown(0) } return t }
peek() { return this.#h[0] }
get size() { return this.#h.length }
#siftUp(i) { while (i > 0) { const p = (i-1)>>1; if (this.#h[p] <= this.#h[i]) break; [this.#h[p], this.#h[i]] = [this.#h[i], this.#h[p]]; i = p } }
#siftDown(i) { const n = this.#h.length; while (true) { let m = i, l = 2*i+1, r = 2*i+2; if (l < n && this.#h[l] < this.#h[m]) m = l; if (r < n && this.#h[r] < this.#h[m]) m = r; if (m === i) break; [this.#h[m], this.#h[i]] = [this.#h[i], this.#h[m]]; i = m } }
}
// 实时排行榜:保留分数最高的 K 名
class Leaderboard {
constructor(k) { this.k = k; this.heap = new MinHeap() }
submit(score) {
this.heap.push(score)
if (this.heap.size > this.k) this.heap.pop() // 超出则扔掉当前最小
}
topK() {
const snapshot = [...this.heap['#h']] // 复制堆数组
return snapshot.sort((a, b) => b - a) // 降序展示
}
}
// 调用示例:模拟弹幕打分流
const lb = new Leaderboard(3)
;[88, 95, 70, 99, 82, 91].forEach(s => lb.submit(s))
console.log(lb.topK()) // [99, 95, 91](前 3 高)流式 TopK:数据量太大放不进内存时,不必存全部,只维护大小为 K 的堆,逐条处理,空间 O(K)、时间 O(N log K)。这是日志分析、实时监控的标配。
名词解释
- 实时排行榜(Leaderboard):动态维护"当前最优的前 K 个"。用堆可 O(log K) 插入、O(1) 看当前最小/最大,远快于每次全排序。
- 数据流 / 流式处理(Streaming):数据像水流一样逐条到达,不能回头重算。算法必须"边到边处理",内存只保留必要摘要(如 TopK 堆)。
- 最大堆 vs 最小堆保留 TopK:保留"前 K 大"用最小堆(堆顶是 K 个里最小的,新值比它大就替换);保留"前 K 小"用最大堆。
课后练习
- 流式 TopK 为什么只用 O(K) 内存而不是 O(N)?
- 答案:堆只保存"目前见过最优的 K 个",新数据若不如堆顶就直接丢弃,无需保留全部 N 条,故内存恒定 O(K)。
Leaderboard.topK为什么先复制堆数组再排序,而不是直接弹堆?- 答案:直接
pop会破坏堆(清空它);复制一份再排序只是为"展示",不影响后续继续submit。
- 答案:直接
总结
堆在这一节完成了从"算法题"到"生产组件"的转身。实时排行榜是几乎所有社交/直播/游戏产品的刚需,而它的内核就是一个 K 大小的堆——新分数入堆、超量就弹堆顶,全程 O(log K)。最打动我的是"流式 TopK"这个思想:当数据大到内存装不下(比如全站用户打分日志),你根本不需要存全部,只要维护一个 K 大小的堆边到边处理,内存恒定、结果正确。这背后是"只需最优摘要、不必保留全集"的工程智慧,在实时监控、异常检测、推荐候选召回里无处不在。优先队列还有更广的舞台:任务调度按优先级出队、限流器按过期时间弹出、Dijkstra 按距离取最小——凡是"每次取最值"的地方,堆都是默认答案。把堆用熟,你会发现一半的"实时/调度"问题都迎刃而解。