Lecture 5 | Binomial Queue¶
约 2196 个字 12 行代码 预计阅读时间 11 分钟
Binomial Queue¶
堆的其中一个应用就是优先队列。本节要介绍的二项队列(Binomial Queue)也是优先队列的一种实现,只不过不同于之前我们用一个堆维护优先队列,二项队列同时维护了具有不同特征的若干队列。
link
Wikipedia: https://en.wikipedia.org/wiki/Binomial_heap
YouTube: https://www.youtube.com/watch?v=6JxvKfSV9Ns (虽然这个视频是讲解斐波那契堆的,但是中间先讲了二项堆。)
学习建议
在正式开始本节内容之前,请带着这样一个念头:尽力思考二项队列与二进制的关系。
概念¶
二项队列(Binomial Queue)本质上是一系列二项树(Binomial Tree)的集合。所以我们首先给定二项树(Binomial Tree)的定义:
Binomial Tree
二项树满足堆的性质,即 parent 节点的值小于(大于) child 节点的值。
一个非常关键的性质是,\(k\) 阶二项树都是同构的,且 \(k\) 阶二项树是两个 \(k-1\) 阶二项树合并得到的。而其合并方式是直接令其中一棵成为另外一棵的根的新 child,这也决定了二项树的根每一个 child 本身也都是一个二项树。
显然能够得到,二项树并非二叉树;更进一步的,\(k\) 阶二项树 \(B_k\) 的根有 \(k\) 个 child,\(2^k\) 个节点;再进一步的,\(B_k\) 的第 \(d\) 层一共有 \(C_k^d = \binom{k}{d}\) 个节点,有趣的是,所有层的节点加起来刚好符合二项式定理,此条定理可以由数学归纳法很容易地证明,不再赘述。
关于实现
可以发现,二项树是一个 \(N\) 叉树,所以通常我们使用链表 sibling 的形式来表示一个节点的 children。
再对二项树做一个简单的总结,\(k\) 阶二项树结构唯一确定,两个 \(k\) 阶二项树合并后得到一个 \(k+1\) 阶二项树,而二项树本身也具有堆的性质。
但是,虽然二项树具有堆的性质,看似能独立完成优先队列的功能,但是二项树对点的数量具有比较严格的要求,只有点的数量符合 \(2^k\) 时,才能使用二项树表示。因此,观察一个二项树能承载的点的数量特征,我们联想到二进制对数的表示——我们可以用一系列二项树来维护 \(N\) 个节点的优先队列,而具体用几阶二项树,则取决于 \(N\) 的二进制表达中,为 1
的是哪几位。
Binomial Queue
二项队列(Binomial Queue)是一系列二项树(Binomial Tree)的集合,其中每个二项树的阶数 \(k\) 都是不同的,反过来讲这句话,集合中 \(k\) 阶的二项树要么只有一个,要么没有。
而为何要这么设计二项队列的结构,具体体现在操作的设计中。
操作¶
在二项队列中,合并是一个非常基础的操作,也是精髓所在。
而为了感受这些操作的精髓,在理解二项队列过程中,可以试图从两个纬度同时理解这些操作:
- 树/堆的纬度,具体观察数据的转移与变化过程;
- 二进制的纬度,将 \(k\) 阶二项树抽象为 bit vector 第 \(k\) 位的
1
,从二进制加法的角度理解;
为了方便后面阐述,我们简单做一下说明:
特征比特向量
对于一个二项队列,定义它的特征比特向量(我自己口胡的东西,不是术语)是它的元素数量的二进制表示(反过来说,特征比特向量的真值表示队列中的元素数量)。
也就是说,特征比特向量象征了二项队列的集合中有哪几阶的二项树。
队列合并¶
合并两个二项队列,实际上就是合并两个集合,合并过程中,我们分别合并每一个 \(k\) 阶二项树,当两个二项队列都存在 \(k\) 阶二项树时,它们合并为一棵 \(k+1\) 阶二项树。可以联想,这项操作“对应”着特征比特向量的相加,而合并操作则类似于进位。
因此,它就类似于一个 1bit 的全加器。
graph LR;
A(["T1.B[k]"])
B(["T2.B[k]"])
C(["T.carry[k-1]"])
D(["T.result[k]"])
E(["T.carry[k]"])
ADD["Adder"]
A ---> ADD
B ---> ADD
C ---> ADD
ADD ---> D
ADD ---> E
于是,根据一个 carry 位、一个被合并树和一个合并树的情况,有一共 \(2^3=8\) 种可能。
单点插入¶
插入结点可以看作合并一个只有一个结点的左偏堆,所以我们可以直接复用合并过程。
查询队首¶
二项队列的队首,也就是整个队列的最小值(最大值),就是这若干个(\(O(\log N)\) 个)二项树的根中最小(最大)的那个。所以其时间复杂度为 \(O(\log N)\)。
不过有时候我们也会额外维护一个指针,指向当前最小的那个根,此时其复杂度为 \(O(1)\)。
队首出队¶
队首出队首先要找到队首,这件事我们在#查询队首已经讨论过了。
找到队首后,我们将其从二项队列中移除,我们知道,队首必然是某个二项树的根,所以删掉这个队首以后,就会产生 \(k\) 个新的子树。
而让我们回顾二项树的合并过程,可以发现,根的所有儿子都是一个完整的 \(k\) 阶二项树合并过来的,所以当我们删掉这个根,产生的所有子树都是二项树。
因此我们联想到,将队首出队问题转化为合并二项队列的问题——假设 \(T\) 的队首是 \(B_k\) 的根,则队首出队可以转化为求解 \(merge(T-B_k, B_k.root.children)\) 的问题。
🌰
假设我们有如下的二项队列,现在进行弹出队首的操作。
如上队列,其特征比特向量为 10100
,共 20 个节点。现在删掉一个,应当有 19 个节点。
更具体的,10100
= 10000
+ 100
,分别代表两个二项树,而我们的最小值是从 10000
所代表的树里删除的,所以新的特征比特向量为:(10000
- 1) + 100
= 1111
+ 100
= 10011
,即 19 个。
注意观察这里 10000
- 1 = 1111
的变化,寻找它与具体视角中的关系。
提示
支持着队首出队可以这样进行的性质就是,每一个二项树的根的 children 本身也都是二项树。
摊还分析¶
Project: Fibonacci Queue¶
Link
创建日期: 2024年1月13日 19:00:24