堆与优先队列:TopK 与调度
本节目标
- 理解堆的结构性质与索引公式
- 能手写最小/最大堆的核心操作
- 用堆解决 TopK、合并 K 个有序链表
堆的索引公式:数组表示完全二叉树,parent(i) = (i-1)>>1,left = 2i+1,right = 2i+2。最小堆保证父 ≤ 子。
// 运行环境:Node.js 14+
// 保存为 dsa-l13.js,执行:node dsa-l13.js
// MinHeap 实现见 dsa-l7.js,这里复用同款
class MinHeap {
#h = []
push(x) { this.#h.push(x); this.#siftUp(this.#h.length - 1) }
pop() {
if (!this.#h.length) return undefined
const top = this.#h[0], last = this.#h.pop()
if (this.#h.length) { this.#h[0] = last; this.#siftDown(0) }
return top
}
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 } }
}
// TopK:维护大小为 k 的最小堆,遍历完堆顶即第 k 大
function topK(arr, k) {
const heap = new MinHeap()
for (const x of arr) {
heap.push(x)
if (heap.size > k) heap.pop() // 超过 k 就扔掉当前最小
}
return heap // 堆内即 TopK
}
const data = [3, 1, 5, 8, 2, 9, 4]
const h = topK(data, 3)
console.log([...Array(h.size)].map(() => h.pop())) // [9, 8, 5](降序弹出)合并 K 个有序链表:用小顶堆每次取各链表头的最小值(leetcode 23)。
名词解释
- 堆(Heap):满足"父节点恒优于子节点"的完全二叉树,用数组存。最小堆父 ≤ 子,最大堆父 ≥ 子。
peekO(1),push/popO(log n)。 - 优先队列(Priority Queue):出队顺序按"优先级"而非入队顺序,底层常用堆实现。调度系统、定时器、Dijkstra 都靠它。
- TopK:从 n 个数里找最大/最小的 k 个。用大小为 k 的堆可 O(n log k) 解决,比全排序 O(n log n) 更省(k≪n 时)。
课后练习
- 用最小堆求"第 k 大"为什么维护 k 大小的堆、弹出最小的?
- 答案:堆里始终留着"目前见过最大的 k 个",堆顶是这 k 个里最小的,即全局第 k 大;遍历完数组,堆顶就是答案。
- 合并 K 个有序链表为什么用堆?
- 答案:每次只需从 K 个链表头里取最小,堆能 O(log K) 取到并补入下一节点,总复杂度 O(N log K),远比逐个合并 O(NK) 高效。
总结
堆是"局部有序即可"思想的极致体现——它不保证整体排序,只保证"堆顶是最值",却足以解决一大类问题。这一点和数组排序形成鲜明对比:当你只需要 TopK 而不需要全排序时,堆把复杂度从 O(n log n) 降到 O(n log k),在 n 很大、k 很小时收益巨大。我特别想点出 siftUp/siftDown 这两个操作是堆的全部灵魂:插入后上浮、删除后下沉,每个都是 O(log n)。一旦你亲手写出它们(见 dsa-l7),优先队列、调度器、Dijkstra 最短路径都只是"在合适的地方调用堆"而已。工程里遇到"每次取最值"的需求,第一时间想到堆,你的算法档次立刻不同。