Theory · Data Structure · Heap & Priority Queue

堆与优先队列

完全二叉树 + 堆序:建堆 O(n) · TopK · 双堆中位数 · container/heap —— "只要最值"场景的最优解

结构

完全二叉树数组化:零指针开销,父子全靠下标公式

操作

上浮/下沉 O(log n),建堆却是 O(n)——本 deck 与复杂度 deck 的招牌联动

模式

TopK 用反直觉的小顶堆、流式中位数用双堆——面试最高频的两套组合拳

定位:堆是"只关心最值"场景的专用结构。三条主线:结构(数组化的完全树)、操作(上浮下沉 + 建堆 O(n))、模式(TopK/双堆)。

Why It Matters

先看一个需求:任务不停地来,每次要处理"最紧急"的那个

场景:急诊分诊,不能按到达顺序

病人陆续到(随时插入),医生每次叫号要叫病情最重的那个(随时取最值)。普通队列只会按到达顺序发号——它管不了"优先级"。任务调度器、Dijkstra 找最近节点、限流的定时器,全是同一个需求。

笨办法一:放数组里,每次扫一遍找最大

插入 O(1) 很爽,但每次叫号要扫全部。100 万个待处理任务,就是 100 万次比较,而且每叫一次都要重来。

笨办法二:一直保持有序

取最大变成 O(1)(拿第一个),但每插入一个都要挪数据 O(n)。把成本从"取"搬到了"插",总账没变。

洞察:我们其实不需要全部有序

排序是把 n 个元素两两之间的关系全部确定下来——可我们每次只想知道一个:谁最大。用更弱的约束换更低的维护成本:只要求"每个父亲都不小于它的孩子",那么根一定是全局最大,而兄弟之间乱着也没关系。这个"半有序"的结构就是

做法插入一个取走最大问题
无序数组O(1)O(n) 扫全表取太贵,100 万次比较
始终保持有序O(n) 挪数据O(1)插太贵
平衡树O(log n)O(log n)能做,但维护了用不到的全序,还有指针开销
O(log n)O(log n)零指针(住在数组里),但只能取最值
数量级对照:100 万个任务,取一次最紧急的——扫数组约 100 万次比较,堆只要约 20 步(log₂10⁶ ≈ 20)。而且堆整个住在一个数组里,没有一个指针,缓存友好到可以当排序算法用(堆排序)。
本 deck 路线:① 半有序到底是什么约束(堆序 + 数组化下标公式);② 两个原子动作(上浮 / 下沉)如何撑起全部 API;③ 一个反直觉结论:建堆是 O(n),不是 O(n log n);④ 两套面试组合拳:TopK 用小顶堆、流式中位数用双堆;⑤ Go 的 container/heap 怎么用不踩坑。
动机页:从"急诊分诊"这个具体场景出发,用两个笨办法把矛盾摆出来,落到核心洞察——"排序是过度完成任务,我们只要最值",从而自然引出"半有序"。全页不给堆的形式定义,定义留给下一页。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(细节后面展开)
优先队列 priority queue一种需求:随时插入、每次取出优先级最高的;它不指定怎么实现
堆 heap实现上述需求最常用的结构;二者常被混着叫,但一个是需求一个是做法
完全二叉树除最后一层外每层填满、最后一层靠左连续——正因如此才能塞进数组
堆序 heap order唯一的约束:每个父亲都不小于(或不大于)它的孩子;兄弟之间不作要求
大顶堆 / 小顶堆根是最大值 / 根是最小值;两者只差一个比较符号方向
堆顶 peek数组第 0 个元素,就是当前最值;看一眼是 O(1)
上浮 sift-up元素比父亲"更该在上面",就和父亲交换,一路往上直到不再违规
下沉 sift-down元素比孩子"更该在下面",就和更该上位的那个孩子交换,一路往下
建堆 heapify把一个乱序数组一次性整理成堆的过程(第 5 页会证明它是 O(n))
惰性删除不真删,先留在堆里,等它浮到堆顶时再发现"已过期"并丢弃

前置知识:不确定就先看这三篇

二叉树 → 完全二叉树、树高、以及数组下标公式的来源
数组与链表 → 堆为什么住在数组里就赢了
复杂度分析 → "建堆 O(n)"那一步求和的读法

最小心智模型(记住这一句)

堆只保证一件事:根是全局最值。父子之间有序,兄弟之间完全无序
所以它做不了"查找某个值""按范围取""排第几名"——那些要找平衡树

所有操作都是同一个动作

把一个站错位置的元素沿着父子链一路交换到它该去的地方:往上换叫上浮,往下换叫下沉。父子链最长就是树高,所以每次操作最多 log n 步。
插入 = 放到数组末尾然后上浮;取走堆顶 = 把末尾元素搬到堆顶然后下沉。就这两招。

阅读提示:下标公式(父 ↔ 子)不用背,知道"完全二叉树按层从左到右编号"就能自己推出来。
前置页:先把"优先队列(需求)vs 堆(实现)"这对最常被混淆的概念分开,再给十个术语的白话定义。最小心智模型把全部 API 归约为"上浮/下沉"一个动作,读者带着它读后面的代码就不会迷路。

Complete Tree + Heap Order

堆 = 完全二叉树 + 堆序不变式

把开场那个"半有序"的想法写成定义:形状上是完全二叉树(所以能住进数组),秩序上只要求父子有序。

小顶堆的树形结构与数组存储对照 一个小顶堆:每个父节点都不大于其孩子,树形与数组两种视图一一对应,数组下标 i 的父节点是 i 减一除二,左右孩子是 2i 加一和 2i 加二。 132 6457 a[parent] ≤ a[child],对每个节点成立 兄弟之间无序 —— 这是堆和 BST 的分水岭 SAME HEAP AS ARRAY · 层序落盘 1326 457 0123 456 parent(i) = (i−1)/2 left(i) = 2i+1 right(i) = 2i+2 上浮:与 parent 比较交换,直到不违反堆序 下沉:与更小的孩子交换(小顶),直到两个孩子都 ≥ 自己
一句话定义:完全二叉树(数组无空洞)+ 堆序(父≤子 或 父≥子)。最关键的区分点:堆只在父子链上有序,兄弟无序、跨子树无序——所以堆里找任意元素是 O(n),这是它和 BST 的本质差异。

Sift Up / Sift Down

两个原子操作撑起全部 API

// 小顶堆:上浮(新元素从尾部升起)
func siftUp(a []int, i int) {
    for i > 0 {
        p := (i - 1) / 2
        if a[p] <= a[i] { break }
        a[p], a[i] = a[i], a[p]
        i = p
    }
}
// 下沉(堆顶被换走后归位)
func siftDown(a []int, i, n int) {
    for {
        l, r, m := 2*i+1, 2*i+2, i
        if l < n && a[l] < a[m] { m = l }
        if r < n && a[r] < a[m] { m = r }
        if m == i { return }
        a[i], a[m] = a[m], a[i]
        i = m
    }
}

Push = 追加 + 上浮

新元素放尾部(保持完全性),一路与父比较交换——最多走树高 O(log n)

Pop = 换尾 + 下沉

堆顶(a[0])与尾元素交换、缩容量,再让新堆顶下沉归位——同样 O(log n)。用"换尾"而不是"挖洞"保持完全性,是数组化堆的精髓。

Peek = a[0]

O(1)——这是堆存在的意义:最值永远在顶部等着。

复杂度总表

push O(log n) · pop O(log n) · peek O(1) · 建堆 O(n)(下一页)· 任意查找 O(n)(兄弟无序)。

面试金句:"堆的所有操作都在一条根叶路径上做交换——路径长度就是树高 O(log n),这就是完全树形状买来的复杂度保证。"
两段代码是堆的"最小完备集",必须能默写。强调 Pop 的"换尾"手法:交换 a[0] 与 a[n-1] 后 n--,既保持完全性又 O(1) 定位替换者。面试常追问"为什么不用挖洞下沉"——答案:换尾顺便处理了容量收缩。

Build Heap · The Classic Trap

建堆 O(n) 而不是 O(n log n):两种建法对比

笨办法:逐个 push —— O(n log n)

从空堆开始 n 次上浮,第 i 次最坏 O(log i),总 O(n log n)。能对,但不是最优。

正解:自底向下沉 —— O(n)

从最后一个非叶节点(⌊n/2⌋−1)倒着 siftDown。高度 h 的节点至多 n/2^(h+1) 个,每个代价 O(h):

Σ (n/2^(h+1))·O(h) = O(n)·Σ h/2^h = O(n)·2 = O(n)

一半节点是叶子(代价 0),代价集中在稀疏的顶层——与 复杂度 deck 的主定理陷阱题完全同源。

工程含义

手里已有一批数据要变成堆(堆排序第一步、初始化优先队列),永远用一次性建堆而不是循环 push——常数差一倍以上。

// 堆排序第一步:一次性建堆 O(n)
func buildHeap(a []int) {
    // 最后一个非叶节点开始,倒着下沉
    for i := len(a)/2 - 1; i >= 0; i-- {
        siftDown(a, i, len(a))
    }
}

// 堆排序完整版:O(n log n),O(1) 空间
func heapSort(a []int) {
    buildHeap(a)                    // O(n)
    for end := len(a) - 1; end > 0; end-- {
        a[0], a[end] = a[end], a[0] // 最值换到末尾
        siftDown(a, 0, end)         // O(log n)
    }
}
错误分析复述(面试原题):"每层 O(n) × log n 层"错在哪?——把节点的代价当成的代价;底层节点海量却几乎不用下沉。
这是与复杂度 deck 的联动页:同一道题从数据结构视角(高度加权求和)和算法视角(递归式/主定理)各讲一遍。heapSort 代码顺带给出——n 次下沉 O(n log n) + O(1) 空间,为排序 deck 留接口。

TopK Pattern · LC 215/347

TopK:求最大的 k 个,为什么用小顶堆

用小顶堆求第 k 大元素的流程 维护一个只有 k 个元素的小顶堆,堆顶是当前 k 个中的最小值即门槛;新元素小于等于门槛直接跳过,大于门槛则弹出堆顶并放入新元素再下沉;全部处理完后堆里就是最大的 k 个,堆顶即第 k 大。 STEP 1 · 堆未满 → 直接入堆 5 3 size < k:无门槛,来者不拒 STEP 2 · 堆已满 → 门槛判定 5 8 9 堆顶 = 门槛(k 个里最小的) x ≤ 门槛 → 扔掉(O(1) 拒绝) x > 门槛 → 弹顶 + 入 x(O(log k)) STEP 3 · 扫描结束 k' 堆里 = 全体最大的 k 个 堆顶 = 第 k 大(LC 215) 时间 O(n log k) · 空间 O(k) —— 流式数据也能跑,不用全量装内存
反直觉点讲透:"求最大为什么用最小堆"——堆顶是门槛,比门槛大的才有资格进来;门槛取 k 个中最小者,被顶掉的永远是最没资格的。求前 k 小则反过来用大顶堆。

TopK · Three Solutions

第 K 大的三种解法:取舍比答案重要

方案时间空间 / 适用
全排序O(n log n)O(1)~O(n);数据全在内存、一次问
小顶堆O(n log k)O(k);流式/海量数据,k 远小于 n 时最优
快速选择平均 O(n),最坏 O(n²)O(log n) 栈;只问一次且允许重排数组
面试标准答案(LC 215):"数据是或 k≪n → 堆 O(n log k);一次性、允许修改数组 → 快速选择平均 O(n);面试先写堆(稳),再主动提快速选择(展示算法广度)。"

347 前 K 个高频元素:堆 + 哈希计数

先 map 统计频次 O(n),再对(频次, 元素)对维护 k 大小顶堆(按频次比大小)O(n log k)。桶排序变体:频次作下标,O(n) 极限。

23 合并 K 个升序链表

k 路归并:k 个链表头进小顶堆,每步弹最小接到结果、把它的 next 入堆——O(N log k)。这是"堆维护多个来源当前最优"的通用范式(后续 Dijkstra 同款)。

703 数据流第 K 大

构造时建 k 小顶堆,之后每个 add 流式处理——TopK 的流式版,考的就是堆方案的天然在线性。

TopK 的完整方法论:先给三方案对比表(复杂度+适用面),再落两个变形题(347 计数+堆、23 k 路归并)。快速选择的"平均 O(n)"结论在排序 deck 里展开,这里先挂链接。

Two Heaps · LC 295

数据流中位数:大顶堆 + 小顶堆的对峙

双堆结构求数据流中位数 把数据按大小分成两半:左半放大顶堆,堆顶是左半最大;右半放小顶堆,堆顶是右半最小;两堆长度相差不超过一,中位数由两个堆顶直接算出。 LEFT · 大顶堆(较小的一半) 7 堆顶 = 左半的最大值 RIGHT · 小顶堆(较大的一半) 9 堆顶 = 右半的最小值 ≤ 分界 ≥ 不变式:左堆全部 ≤ 右堆全部 · |len(左) − len(右)| ≤ 1 中位数:两堆等长 → (左顶+右顶)/2 · 不等长 → 长堆的堆顶 新元素路径:先加到左堆 → 把左堆顶搬给右堆 → 若右堆过长再把右堆顶搬回左堆(两步保平衡)
双堆是"对峙结构":左堆管小的一半(要最大)、右堆管大的一半(要最小),中位数就在两个堆顶之间。实现诀窍是"先入左堆再调整"的两步搬运——不用分类讨论新元素该去哪边,代码短且不易错。

Go Stdlib · container/heap

Go 的优先队列:接口约定,不是现成容器

// 实现五个方法即可获得堆能力
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool {
    return h[i] < h[j] // 大顶堆改 > 
}
func (h IntHeap) Swap(i, j int) {
    h[i], h[j] = h[j], h[i]
}
func (h *IntHeap) Push(x any) {
    *h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
    old := *h; n := len(old)
    x := old[n-1]
    *h = old[:n-1]
    return x
}
// heap.Init(&h) / heap.Push(&h, v)
// heap.Pop(&h) / heap.Fix(&h, i)

设计哲学:算法与数据分离

标准库只提供堆算法(上浮下沉逻辑),元素怎么比大小由你的 Less 决定——非侵入式接口,任何类型都能变堆。

三个易错点

① Push/Pop 必须定义在指针接收者上(要改长度);② Pop 的实现自己负责弹出并返回(库只负责调整顺序,约定堆顶在末尾);③ 元素被 heap.Remove/Fix 后下标失效,外部索引要自己维护。

heap.Fix:定点更新

元素优先级变化时(如任务改期),Fix(h, i) 在原地下沉/上浮 O(log n)——比"删了重插"干净,这就是索引堆的用法(Dijkstra 里 decrease-key 的 Go 实践)。

runtime 里的真堆

time.Timer 底层是四叉堆(4-ary):比二叉堆浅一层、缓存更友好——runtime 热路径的真实取舍。

Go 岗专属页:container/heap 五方法模板要能默写(Less 反向即大顶堆)。三个易错点(指针接收者、Pop 自管弹出、下标失效)都是实战踩过的坑。runtime timer 用四叉堆是漂亮的冷知识。

Where Heaps Live

堆的应用:凡是"每次取最优先",都是堆

算法层

Dijkstra:每次取当前距离最小的节点 O(log V)(图 deck);② 合并 k 路有序:23 题范式;③ Huffman 编码:每次取两个最小频率合并;④ 堆排序。

系统层

① 定时器/超时管理:到期时间建小顶堆,peek 最近到期事件(Go runtime 四叉堆、Kafka 延迟队列的时间轮+堆);② 任务调度:优先级队列;③ 网络拥塞控制的事件队列。

业务层

热搜榜 TopK 实时更新、推荐候选截断(海量打分取前 k)、限流中的最贵请求识别——"流式 + 只要前 k"的统一答案。

场景用什么堆关键操作
Dijkstra小顶(距离)Pop 最小 + 惰性/定点更新
定时器小顶(到期时间)Peek + Fix(改期)
TopK 流式小顶(门槛)Push + 淘汰堆顶
中位数大顶+小顶对峙两堆搬运平衡
k 路归并小顶(k 个头)Pop + Push(next)
面试金句:"队列管到达顺序公平,堆管优先级——需求里出现'每次都要当前最 X',就是优先队列。"
三个层次(算法/系统/业务)各举一例,表格汇总五个高频场景。与栈队列 deck 的"队列=公平"对照:"堆=优先",一句对仗收尾。

LeetCode Shortlist

必刷题单:堆与优先队列

分组题目(编号 · 难度)要点
TopK 家族215 数组第K大 M · 347 前K个高频 M · 703 数据流第K大 E · 973 最接近原点的K个点 M215 先堆后快选;347 堆+计数
k 路归并23 合并K个升序链表 H · 378 有序矩阵第K小 M · 632 最小区间 H23 是范式:堆维护 k 个"当前候选"
双堆295 数据流的中位数 H · 480 滑动窗口中位数 H · 502 IPO H295 是母题;480 加滑动窗口过期
模拟/贪心1046 最后一块石头 E · 621 任务调度器 M · 1642 可达最多建筑 H1046 练手;621 堆+数学
刷法建议:1046 热身(裸堆操作)→ 215(TopK 标准题)→ 23(k 路归并范式)→ 295(双堆收尾)。手写 container/heap 五方法或用泛型封装,二选一练熟。
题单四组:TopK、k 路归并、双堆、模拟贪心。215 和 23 出现率最高;295 是系统设计"中位数流"的前置知识。

Cheat Sheet

一页带走:复杂度、选型、两套组合拳、坑

① 复杂度(n 个元素)

看最值 peekO(1)——就是数组第 0 个
插入 pushO(log n):追加到末尾 + 上浮
弹出最值 popO(log n):末尾元素搬到堆顶 + 下沉
建堆 heapifyO(n)(自底向上逐个下沉);逐个 push 是 O(n log n)
查找任意元素O(n)——堆没有查找能力,兄弟之间无序
删除任意元素 / 改优先级O(log n),但前提是已知它的下标(要额外维护 值→下标 的映射)
堆排序O(n log n) 时间、O(1) 空间、不稳定

② 选型:该不该用堆

只要最值,反复取用堆(调度、Dijkstra、合并 k 路、定时器)
要查找 / 范围 / 排名平衡树跳表
要完整有序结果直接排序,别用堆逐个 pop
k 接近 n不如整体排序(O(n log n) 常数更小)
数据放不下内存分片各求局部 TopK,再归并 k 路(多路堆)

③ 两套必背组合拳

TopK 最大的 k 个维护容量 k 的小顶堆:新元素 > 堆顶就换掉堆顶。时间 O(n log k)、空间 O(k)。
口诀:求最大用小顶堆——堆顶是"守门员",专门淘汰不够格的
流式中位数大顶堆存较小一半 + 小顶堆存较大一半;每次插入先进一个堆再把堆顶让给另一个,维持两堆size 差 ≤ 1。中位数 = 堆顶(奇)或两堆顶均值(偶)

④ 四个坑

遍历顺序数组顺序不是有序,只保证根最值;打印堆数组别当排序结果
不稳定相同优先级不保证先来先出;要稳定就把入队序号加进比较键
优先级变了不能直接改字段,必须 heap.Fix(h, i)(否则堆序被破坏而无人知晓);或走惰性删除:插新的、弹出时丢弃过期项
Go 接口反直觉自己写的 Push/Pop 只管切片尾部追加/摘除,调用时必须用包函数 heap.Push/heap.Pop,它们才做上浮下沉
一句话背下来:堆用"只保证父子有序"这个最弱约束,换来插入和取最值都是 O(log n)、且零指针住在数组里;代价是它只会取最值,别的什么都不会。
速查页四块:复杂度(含"查找 O(n)""建堆 O(n)"两个高频考点)、选型边界、TopK/双堆两套组合拳、四个工程坑。末尾一句回收开场"用更弱约束换更低成本"的主线。

Interview QA · Part 1

高频追问:原理与复杂度

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的翻回上一页速查表。

1 · 堆和 BST 的区别?什么时候用堆?

堆序 vs 全序

堆只保证父子偏序(兄弟无序),BST 是全序(中序有序)。只要最值 → 堆 O(log n) 且数组化更省;要查找任意元素/范围/前驱后继 → BST。

2 · 为什么建堆是 O(n)?

高度加权

自底向下沉:高度 h 的节点 ≤ n/2^(h+1) 个,Σ n/2^(h+1)·O(h) = O(n)·Σh/2^h = O(2n)。一半节点是叶子零代价;"每层 O(n)×log n"是把节点代价当层代价的经典错误。

3 · push/pop 为什么是 O(log n)?

单路径

操作只动一条根叶路径:上浮/下沉最多走树高次交换。完全树保证高度 = ⌊log₂n⌋,形状即保证。

4 · TopK 为什么"求最大用小顶堆"?

门槛思维

堆顶是准入门槛(k 个里最小的):x 比门槛大才进来,顶掉的是最没资格的。堆只需 k 个元素 → O(n log k)、内存 O(k)、支持流式。求前 k 小对称地用大顶堆。

5 · 双堆中位数怎么维持平衡?

LC 295

不变式:左堆(大顶)全 ≤ 右堆(小顶)全,长度差 ≤1。实现取巧:新元素总是先入左堆,再把左堆顶搬去右堆;若右堆过长再搬一个回来——两步通用,免分类讨论。

6 · 堆稳定吗?想要稳定怎么办?

不稳定

不稳定:相等元素次序依赖交换路径。需要稳定序时给元素配单调递增序号作第二关键字(Less 里先比优先级再比序号)。

7 · 堆排序为什么不如快排快?

缓存不友好

同为 O(n log n) 但堆排跳跃访问(i 与 2i+1 距离越来越远,cache miss 多)且不稳定;快排顺序访问、分支可预测。堆排的优势只有 O(1) 空间 + 最坏 O(n log n) 有界。

8 · 为什么 Go runtime 的定时器用四叉堆?

d−ary 权衡

d 叉堆树更矮(log_d n)但每层比较更多——cache line 装得下 4 个子节点时,减少的 miss 赢多出的比较。runtime 热路径上"内存访问次数"比"比较次数"更贵。

前八题:堆 vs BST、建堆 O(n)、路径复杂度、门槛思维、双堆平衡、稳定性、堆排缓存、d 叉堆权衡。第 8 题是 Go 岗差异化亮点,能与 runtime timer 话题自然衔接。

Interview QA · Part 2

高频追问:工程与实战

同样先自答再对照。这一页的题要"结论 + 一句代价/边界"才完整——只答结论会被继续追问。

9 · 堆里的元素优先级变了怎么办?

索引堆 / heap.Fix

两条路:索引堆(hash 记元素→下标,定点 Fix 上浮下沉 O(log n));或惰性删除——插入新版本、弹出时跳过过期项(Dijkstra 常用,代价是堆可能变大)。

10 · container/heap 的 Push/Pop 为什么"反直觉"?

约定优于内置

库只实现算法(siftUp/siftDown 的索引操作),数据进出由你的方法负责:Push 靠你 append、Pop 靠你弹出末尾。Less 反向就是大顶堆。这是 Go 非侵入式接口的典型应用。

11 · 合并 k 个有序链表为什么用堆?

O(N log k)

每一步要"k 个链表当前头里的最小者"——正是堆的语义:Pop 最小 O(log k) + Push 该节点的 next。对比两两归并 O(N log k) 同级但常数大、串行;堆版天然适配流式。

12 · Dijkstra 为什么必须配优先队列?

贪心取最小

贪心正确性依赖"每次取当前距离最小的已确定节点":数组扫 O(V²),堆 O((V+E)log V)。稀疏图差距巨大——堆是图算法的标配外设(图 deck)。

13 · 堆能做"延迟删除"吗?

惰性删除

可以:删除时只打标不搬堆,Pop 到标记项跳过。适合删除频繁但可容忍堆膨胀的场景(滑动窗口中位数 480 的过期窗口元素)。定期重建可回收空间。

14 · 海量数据 TopK,内存放不下怎么办?

O(k) 内存

堆方案天然适配:只维护 k 个元素的堆,流式扫全量数据 O(n log k)、内存 O(k);多机场景每台出 TopK 再合并(k 路归并)。这就是"堆 = 流式算法"的意义。

15 · 什么时候不用堆?

边界意识

① 需要任意元素查找/有序遍历 → BST/跳表(堆找任意元素 O(n));② 只取一次最值且可重排 → 快速选择更省(平均 O(n));③ 元素很少(k<10)直接线性扫——别为 O(1) 引入 O(log n) 的复杂性。

后七题偏工程:索引堆/惰性删除、container/heap 设计哲学、k 路归并、Dijkstra、海量 TopK、以及"什么时候不用堆"的边界意识。第 15 题是防止"拿着锤子看什么都是钉子"的清醒剂。

Related & References

相关知识点与参考

数据结构系列(本分类)

二叉树 →(完全树数组化的地基)
平衡树 →(需要全序/查找时的替代)
栈与队列 →(队列管顺序,堆管优先)

算法系列

图算法 →(Dijkstra 的标配外设)
排序算法 →(堆排序 + 快速选择 TopK)
复杂度分析 →(建堆 O(n) 的证明主场)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch6 · Heapsort / Priority Queues堆性质、BUILD-MAX-HEAP 的 O(n) 分析、堆操作
src/container/heap/heap.goInterface 五方法约定、Init/Push/Pop/Fix/Remove 实现
src/runtime/time.go(本机 Go 版本)定时器四叉堆(timers 4-ary heap)实现
LeetCode 215/347/23/295 官方题解TopK 三方案、k 路归并、双堆模式
oi-wiki.org/ds/binary-heap堆的中文系统讲解与 d 叉堆讨论
收尾:六条链接把堆挂进知识网络。CLRS ch6 是建堆 O(n) 的标准出处;runtime timer 四叉堆以本机源码为准。总页数 13。