优先队列
动机
某些情况下,我们需要频繁地动态地取出一个集合S中的最小元素/最大元素。
这里动态指我们需要在集合S上执行插入/删除/更改键值操作。
问题在于如何组织数据结构使这些操作更高效。
操作定义
以最小优先队列为例:
| 方法名 | 描述 |
|---|---|
| MakeHeap() | 建立一个堆H |
| Insert(H, x) | 在堆H中插入元素x |
| ExtractMin(H) | 提取堆H中的最小元素 |
| DecreaseKey(H, x, k) | 减小堆中元素x的键值为k |
| Union(H1 , H2 ) | 返回一个新堆包含H1和H2的所有元素,销毁输入堆 |
应用举例
- Dijkstra单源最短路算法:需要
ExtractMin - Prim最小生成树算法:需要
ExtractMin与DecreaseKey - Why we need Union?
数组/链表实现
我们可以用数组或链表快速实现一个简单版本的优先队列。
由于本篇偏向理论分析,因此更在意数据结构的逻辑结构而非物理结构。对以下两种实现方式而言,可以认为数组的逻辑结构和链表相同(实际上二者复杂度分析也相同),因此都用链表代替。
无序链表实现

每次都在首部插入新元素;提取最小值时,遍历链表找到最小值并更改链表指针将其从链表中删除。
(数组版本)每次在尾部插入新元素;提取最小值时,遍历数组找到最小值并将其与尾部元素交换位置,减小长度计数。
复杂度:
- 插入操作:
- 提取最小值:
有序链表实现

插入新元素时,遍历链表至第一个大于其的元素位置处,在此之前插入;提取最小值直接返回首部,并将其删除(更改头指针)。
(数组版本)倒序维护数组大小,插入新元素时,遍历链表至第一个小于其的元素位置处,在此之前插入,后续元素后移;提取最小值时直接返回尾部,并将其删除(减小长度计数)。
复杂度:
- 插入操作:
- 提取最小值:
问题
是否存在使得插入和提取最小值时间复杂度更均衡/更优的数据结构?
二叉堆
观察有序链表实现,插入操作实际上每次都在进行插入排序以维持整个链表的有序,然而这属于工作过头了,我们只需要维护一个最小值。
- 有序链表的每个指针指向的元素都大于等于自身,数据结构限制过于严格,因此需要放松限制。
- 但限制不能过分放松,否则就会像无序链表那样,元素间没有任何限制,导致提取操作复杂度上升。
二叉堆:
- 逻辑结构:一颗二叉树,且父结点小于子结点。所有结点除最后一层外按照完全二叉树排列。

物理结构:为了便于随机读取以及减少指针开销。结点以完全二叉树编号为下标放入数组中。

因此对结点 ,左子结点下标为,右子结点下标为,父结点下标为(向下取整)。下标越界时,表示结点不存在。
操作:
插入:将新元素置于二叉堆尾部,依次向上调整,使得最小堆性质(父结点小于子结点)保持。由于二叉堆高为(为元素个数),因此时间复杂度为。
提取最小值:将堆顶元素与堆尾元素交换,减小堆数组长度计数,从根结点开始依次向下调整,使得最小堆性质保持。时间复杂度为。
DecreaseKey(x, k):将下标x处元素值降低为k。从该元素开始依次向上调整。时间复杂度为。建堆:
通过建空堆依次入堆。时间复杂度。
通过"遍历堆化"实现。将所有元素原封不动地入堆,随后调整堆结构。
方法:按照完全二叉树编号从尾结点开始向根结点依次调用“向下调整”。
时间复杂度:将堆补充为完全二叉树,不影响复杂度计算。设层高为。从上到下依次为第0…d层。对第i层,操作总次数为。因此总操作数
故
合并:将两个堆的数据组成一个堆,再遍历堆化。。
至此,我们得到了插入,提取,改键均为,且合并为的堆。实现简单且复杂度不错。
问题
如果想进一步降低合并的复杂度,该怎样调整数据结构?
二项式堆
观察二叉堆的逻辑结构,我们发现只允许一棵树存在这一限制。因此在两个堆合并时,需要拆散两颗树合并为一颗。如果允许多棵树存在,逻辑结构不是一棵树而是森林。合并时无需破坏原有堆的结构。
- 放松限制:一棵树 -> 允许森林
- 不要过分放松:同类型的树只存在一颗。
综上,既然允许多棵树存在,插入操作就变成了合并操作,即合并一颗只含根结点的树,直接将其插入森林中即可;由于同类型的树只能有一颗,我们需要合并同类型的树,合并后的树也应该属于某个类型。
二项树
定义:为二项式树,定义为的度数。
- 只含有一个结点
- 两个可以合并为
这里合并操作指将作为另一的根结点的儿子插入。
这样的定义使二项树有一些有趣的性质:
- 树层高为
- 树的每个子结点分别为 (树的第个子结点的度为)
- 树的每层结点个数正好是的二项式系数()(命名来源)
二项堆
定义:二项堆为二项树组成的森林
- 其中每个度数的二项树只有一颗
- 对于二项堆中的每颗二项树,满足最小堆性质
由此,当二项堆里有相同类型的二项树需要合并时,取根结点最小的树的根结点作为父结点,将另一棵树作为其儿子插入。
二叉堆二项树根结点用双向链表连接,指针指向最小的根结点。
二项堆也有一些有趣的性质:
- 对于规模为的二项堆,其二项树的组合是确定的。二项堆含有当且仅当,其中。
- 二项堆最多含有(向下取整)颗二项树
- 二项堆的层高不超过(向下取整)
操作:
- 插入:合并一颗树。最好情况下,最坏情况下,均摊复杂度为。这是什么?别急
- 提取最小值:将最小根结点取出,将该二项树拆为其所有子树,合并相同类型的二项树。。
- 合并:复杂度,均摊复杂度。
- 减小键值:在某颗二项树上向上调整。。
均摊分析
为什么要均摊分析?在分析插入/合并/提取最小值时,我们总考虑工作量的上限,即最坏情况,推出复杂度均为。但是这三个操作复杂度在时间上具有继承性,如果做了一次工作量较大的操作,随后许多次操作都只需要很小的工作量。这种情况下可以引入势能分析。
插入
伪代码:
| |
一次插入操作需要消耗时间, 是while语句执行的数量,表示number of。
引入势函数,即二叉堆中树的数量。当进行一次插入操作后,势函数可以分为上升的部分和下降的部分。
- 上升:
- 下降:,即树减少的数量
因此一次插入操作需要。而由于,故。这为我们统计一系列操作提供了方便。
可以看到,当分析合并时,伪代码与插入只有第二行的区别,一次合并操作需要的时间也是。
提取最小值
伪代码:
| |
一次操作需要消耗,,为去除根结点的树的度(也是其子树数量)。
我们用同样的势函数。进行一次操作,势函数既有上升的部分也有下降的部分。
- 上升:
- 下降:
因此一次操作需要。其中。
分析
若对一个二叉堆做了次插入操作和次提取最小值操作,总消耗的时间
由于与无关,因此插入操作的均摊复杂度为,提取最小值的操作的均摊复杂度为。合并操作均摊复杂度与插入操作相同,为。
问题
对比二叉堆,我们成功地将合并操作的复杂度降低为(均摊)。如果进一步考虑,如何降低减小键值的复杂度呢?
斐波那契堆
对于减小键值操作,无论是二叉堆还是二项堆,都避免不了对堆进行调整的操作。为了降低复杂度,能否进行操作后,不进行或推迟调整操作?
斐波那契堆在二项堆的基础上:
放松限制,允许同类型的二项树共存;同时在减小键值操作后,并不调整堆而是直接将该结点及其子树摘下放入根结点列表中,同时给该结点的父结点打上标记,已表明该父节点“失去”过一个结点。(1)
注意到该限制不是完全放松的,对每颗二项树而言,非根结点最多失去一个结点。(2)
“摘下”操作相当于取消了原来的堆调整过程,将其推迟到堆做合并时。减小键值对应着摘下结点。
当已被标记的结点的子结点被摘下后,该结点应取消标记并也被摘下,递归地向父结点进行直至没有标记。
根结点不允许失去结点 -> 便于统计树的度数 。
注意到由于条件(1)的存在,插入和合并都只需向根结点列表中添加新的树即可。但堆总要做出调整,何时?
答案是不得不调整时,即提取最小值后,此时无论如何都要进行合并操作了,否则根结点列表数量将膨胀。
合并操作是合并相同度数的树。树的度数等于根结点的子结点数目。
操作:
- 插入:合并一颗树。复杂度。
- 提取最小值:将最小根结点取出,将拆为其所有子树并插入根结点列表。最坏&均摊。为什么?
- 合并:复杂度。
- 减小键值:均摊复杂度。
均摊分析
提取最小值
伪代码:
| |
与二叉堆的操作相同,但这里我们使用新的势函数,为被标记的非根结点数量。 why?
一次提取最小值操作带来的势函数变化为:
- 上升(静态):(移除结点的度)
- 下降(动态):
一次操作需要,为根结点的度。
注意到由于树会被摘下结点,因此有可能大于。
想象单颗二项树的情景,有,由于减小,故。扩展到堆也可能成立。
因此均摊操作复杂度取决于的上界,后续会证明的上界。思路:找到二项树对应的结点数最小的树,即斐波那契树的结点数目。
减小键值
再回想一下减小键值的操作:
减小键值对应着摘下结点(若堆序被破坏)。
当已被标记的结点的子结点被摘下后,该结点应取消标记并也被摘下,递归地向父结点进行直至没有标记。
根结点不能失去结点:对父结点为根结点的操作特殊处理。
伪代码:
| |
为了复杂度计算方便,暂时先不考虑x的父结点为根结点的情况。由伪代码可以得到,一次操作消耗的时间为。
考察势函数,一次减小键值操作带来的势函数变化为:
- 上升(静态):
- 下降(动态):
故一次操作消耗的时间为。
当x的父结点为根结点时也符合上式。
插入
上升,下降。
一次操作消耗的时间为。
分析
考虑一段操作序列:次插入,次提取最小值,次减小键值。
总的运行时间最大为:。
注意到:。
因此总的运行时间最大为:。
因此插入的均摊复杂度为,减小键值的均摊复杂度为,提取最小值的均摊复杂度为。
限界分析
为的上界,限界即确定与堆结点数目的关系。
考虑一颗树,则其度,若堆中有多颗树,相当于增加,则其中一颗树的度。因此为了确定的上界,堆中树越少越好,即只有一颗。
考虑一颗树,其度,若从其上摘下结点(减小键值操作),对单颗树来说,相当于减小,则,当最小时,此时取得最大的。也可以考虑为不变时,这种构成堆的方式(即单颗树且丢失最多的结点)可使最大,即取到。
于是我们开始寻找各个度对应的最少结点树,不妨记其为,为度数:

回想二项树由两颗树构成:

树由和构成:

尽管,但不会小太多。。
对于斐波那契数列: 注意到,因为。
由斐波那契通项性质。
考虑一个斐波那契堆含有个结点,为其中一颗树,其根结点度为。
则。
因此。故。
因此提取最小值的均摊复杂度为。





















