这一篇在干嘛?

很多场景只关心”当前最小/最大”:任务调度取最高优先级、Dijkstra 取最近节点、事件模拟取最早事件——全排序是浪费。优先队列(priority queue)把”取最值 + 插入”做到 、建堆做到 ,是第 7 章堆排序和第 9 章图算法的引擎。

6.1 模型与朴素实现的代价

优先队列 ADT 至少两个操作:insert(插入)、deleteMin(取出并删除最小元,也常做 deleteMax 版)。

朴素实现的两难:有序数组——deleteMin 但 insert 要找位 无序数组——insert 但 deleteMin 要全扫 。鱼与熊掌的解法就是堆。

6.2 二叉堆:两条性质撑起一切

结构性:完全二叉树——除最后一层外全满,最后一层从左到右填充。这带来一个决定性优化:不需要指针,用数组就能存树。位置 的孩子是 ,父亲是 (下标从 1 开始)。

图:完全二叉树——只有左边的满足堆序,数组下标即树位置

堆序性:每个节点 ≤ 它的两个孩子(小根堆)。注意:只保证父≤子,兄弟和跨子树之间无序——所以”找第 2 小”不能 ,堆不是排序结构。

图:很大的完全二叉树——高只有 log N

白话:堆是”家家都不过孩子”的家族——每家家长都比自家孩子小,但两家孩子谁大谁小没有结论。完全二叉树 + 数组 = 最省内存的树表示(无指针开销、缓存友好)。

insert:上滤(percolate up)

新元素先放到最后一个位置(洞),然后与父亲比较:比父亲小就把父亲拉下来(不是自己上移),继续向上直到父亲 ≤ 自己。

void insert(const Comparable &x) {
    array.push_back(std::move(x));   // 建洞
    int hole = theSize;
    while (hole > 1 && x < array[hole / 2]) {   // 还没到根且父亲更大
        array[hole] = std::move(array[hole / 2]); // 父亲下来填洞
        hole /= 2;                                 // 洞上移
    }
    array[hole] = std::move(x);      // 洞停在合适位置,x 入座
}

白话:插入像新员工”往上漂”——只跟直接领导比,领导大就换位。最多爬 层,;而且实际往往提前结束(平均 2.6 次比较,随机数据下大多数元素本来就沉在低层)。

deleteMin:下滤(percolate down)

根就是最小元,取走后剩个洞。策略:把最后一个元素搬到洞,然后向下渗透——与两个孩子中较小的交换,直到两个儿子都 ≥ 它。

void percolateDown(int hole) {
    int child;
    Comparable tmp = std::move(array[hole]);   // 待下滤元素
    for (; hole * 2 <= theSize; hole = child) {
        child = hole * 2;
        if (child != theSize && array[child + 1] < array[child])
            ++child;                            // 选较小的儿子
        if (array[child] < tmp)
            array[hole] = std::move(array[child]); // 小儿子上来
        else
            break;
    }
    array[hole] = std::move(tmp);
}

白话:末位元素”空降”到顶后会一路下沉,每层跟”两个孩子中较小者”换位,保证换上来的永远不比换下去的大。

buildHeap:O(N) 的魔法

拿到乱序数组,从最后一个非叶节点到根逐个 percolateDown。直觉上每个节点下滤最贵 、共 个节点,像 ——实际是

白话:低层节点多但下滤近(树的高层节点少、下滤贵——两者相乘求和被 压住)。堆排序、Dijkstra 初始化都靠免费建堆。注意:逐个 insert 建 N 元堆是 ,和 buildHeap 的 不是一回事。

其他操作

decreaseKey(降值后上滤)、remove(p)(降到 再 deleteMin)、findMin increaseKey 下滤。

6.3 应用一:选择问题(找第 k 大)

  • 算法 6A:全部建堆后 deleteMin k 次——,对 (中位数)即
  • 算法 6B:维护一个只装 k 个元素的大根堆,流式处理:新元素比堆顶大就替换。,内存只装 k 个元素——海量数据 Top-K 的标准解法

6.4 应用二:事件模拟

银行排队模拟:事件(到达/离开)按时间排序处理,但新事件动态产生——优先队列天然匹配:deleteMin 取最早事件,处理产生的新事件 insert 回去。

6.5 d 堆:孩子多一点,插入快一点

完全 d 叉树:insert 上滤深度 更浅(插入快),deleteMin 要在 d 个孩子里找最小(找最值慢)。插入多、合并少的场景用大 d(如 )有实测优势。

6.6 左式堆:支持 O(log N) 合并

二叉堆的 merge 很难做(数组表示没法高效拼两棵树)。**左式堆(leftist heap)**用指针表示,专攻合并。

零路径长(null path length, npl):从节点出发到最近一个”不足两孩子的节点”的最短路径长;空节点 npl = −1。

左式性质:每个节点左孩子的 npl ≥ 右孩子的 npl(左重右轻)。

图:零路径长——只有左树满足左式性质

白话:左式堆把”重活”偏置到左边,右边天然很浅(可证右路径至多 )。合并只在右路上做,复杂度就有了保证。

合并算法(递归版)

LeftistNode *merge(LeftistNode *h1, LeftistNode *h2) {
    if (h1 == nullptr) return h2;
    if (h2 == nullptr) return h1;
    if (h1->element > h2->element)
        std::swap(h1, h2);                    // 保证 h1 根小,当新根
    if (h1->left == nullptr)                  // 左空直接挂
        h1->left = h2;
    else {
        h1->right = merge(h1->right, h2);     // 右子树递归合并
        if (h1->left->npl < h1->right->npl)   // 左轻则交换,恢复左式性质
            std::swap(h1->left, h1->right);
        h1->npl = h1->right->npl + 1;
    }
    return h1;
}

白话:小根当新根;把大根堆塞进小根的右子树里继续递归合并;回溯时检查左右”轻重”(npl),轻了就左右交换。整个过程沿右路径进行,。insert = 与单节点堆 merge;deleteMin = 取根后 merge 左右子堆。

图:两个左式堆

图:合并——H₂ 先与 H₁ 的右子堆合并,再交换

图:合并结果

6.7 二项队列:三操作全对数的均衡派

二项树 单节点; 由两棵 合并而成,恰有 个节点、高 k。二项队列 = 二项树的森林,且每种高度至多一棵——于是”森林的二进制表示”:

图:6 个元素的二项队列 H₁(= 0110₂ = B₂+B₁)

  • insert ≈ 二进制 +1,二项树”进位合并”(两棵同高 合并成一棵 ),最坏 、摊还
  • merge ≈ 二进制加法(带进位),
  • deleteMin:找到最小的根( 扫描森林),删掉它后其孩子恰好构成一个二项队列,与剩余部分 merge。

图:二项队列 H₃ 及其森林画法

图:二项队列的左孩子右兄弟表示

图:deleteMin 删除最小根后,其孩子构成新森林 H′

白话:二项队列把”堆结构”和”二进制计数”对上了——加法就是合并,减一位就是删最小元。三操作全部 ,比二叉堆(merge 不行)和左式堆(insert 最坏 O(log N))更均衡;代价是实现复杂。

选型速查:只要 insert/deleteMin → 二叉堆(数组,最简单最快);要 merge → 左式堆(或斜堆,第 11 章摊还分析);三操作都要对数保证 → 二项队列(或第 12 章的配对堆——实践更快但理论略弱)。

常见坑

deleteMin 下滤选错孩子:必须与较小的孩子交换,选错了堆序就破了。② 以为堆能 O(1) 找第 k 小:堆只有父子有序,兄弟无序,找第 k 小要借辅助结构。③ 把逐个 insert(O(N log N))当成 buildHeap(O(N)):数组初始化场景一定要用 buildHeap。

通关标准

学完本篇你应该能做到:① 手推 insert 上滤与 deleteMin 下滤的每一步交换;② 证明 buildHeap 是 O(N);③ 用堆解 Top-K 问题并说清为什么装 k 个元素的堆是 O(N log k);④ 手写左式堆 merge 并解释 npl 与左式性质如何保证右路径 O(log N);⑤ 用二进制类比说清二项队列的 insert/merge/deleteMin。