# 第 2 节 优先队列的实现
# 优先队列的几种实现方式
可以很容易地想到「优先队列」有两种实现方式:「无序数组」和「有序数组」。
# 实现一:无序数组
- 入队:放入的时候,直接放在数组的末尾,时间复杂度:
; - 出队:每次拿出元素之前,我们都执行一次排序,或者像「选择排序」那样,把最大的那个拿出去(其他操作不赘述),时间复杂度是:
,这里 是数组的长度。
# 实现二:有序数组
- 入队:每次放入元素的时候,我们都执行一次排序,类似于「插入排序」的内层循,保持数组的有序性,时间复杂度
。 - 出队:把最大的那个拿出去
(其他操作不赘述)。
下面这个结论很重要:
可以看出,讲「优先队列」设计成线性结构,出队或者入队的时候,其中有一个操作一定是
的,也就是说「出队」或者「入队」其中一个操作一定是 。
伟大的计算机科学家平衡了入队和出队这两个操作的时间复杂度,这种数据结构就是「堆」。也就是说,不管是「入队」还是「出队」,总有一个操作得把「优先队列」中的元素都看一遍。而「堆」就是这样一个数据结构,能把
# 三种数据结构对于实现优先队列的时间复杂度的比较
实现优先队列的数据结构 | 入队操作 | 出队操作 |
---|---|---|
普通数组 | ||
顺序数组 | ||
堆 |
说明:
以在
综上所述,「堆」是实现「优先队列」的高效的数据结构。根据出队的元素是 当前 整个队中最大的那个元素或者是最小的那个元素,「堆」有「最小堆」和「最大堆」之分。
# 最大堆与最小堆
「优先队列」是一种常见的数据结构,有两种「优先队列」。
- 一种「优先队列」每次可以从中拿到人为定义下、当前优先级「最高」的元素,即「最大堆」「大顶堆」「大根堆」;
- 另一种「优先队列」每次可以从中拿到人为定义下、当前优先级「最低」的元素,即「最小堆」「小顶堆」「小根堆」。
如果没有特别说明,我们下文所指的「优先队列」都指「最大堆」「大顶堆」「大根堆」,而「最小堆」「小顶堆」「小根堆」的实现我们不累赘叙述了,原理与「最大堆」一样。
可以看到,「堆」的入队和出队的时间复杂度都是
# 二叉堆 Binary Heap 的定义
形如下面形状的一个结构就是「最大堆」。
- 第 1 条:「最大堆」是一棵「完全二叉树」
完全二叉树:从形状上看,除了最后一层之外,其它层结点的数量达到最大值,并且最后一层的结点全部集中在左侧。
「完全二叉树」的特点是:可以使用一个数组保存「完全二叉树」,而不必引入离散的树形结构,因为在通常情况下,操作数组的下标,比操作结点和指针要方便一些。(这一点非常重要,请读者仔细体会)。这样既可以利用数组元素可以快速访问的特点,又让结点和结点之间形成了「父」与「子」的结构关系。
- 第 2 条:任意一个结点,如果它有孩子结点的话,孩子结点的值一定不会大于父亲结点的值
如果一个数组中的元素,有如上特点,我们称之为 堆有序。「堆有序」不是我们通常理解意义上的「升序」或者「降序」。如果把数组排成「完全二叉树」的样子,且满足第 2 条,这个数组就是「堆有序」。这里要注意的是,通常数组的 0 号下标不使用,从 1 号下标开始使用。这只是一种习惯,因为这么做父子结点的下标关系更「好看」一些,仅此而已。有些时候也会使用从
# 从下标为 1 开始的数组实现的二叉堆的性质
我们自己画一个二叉堆(如下图),把下标标注在二叉堆上。从上到下、从左到右,从 1 号下标开始标记,即下图结点的旁边黑色的数字,我们不难发现这些数字的排列形成的规律。
从 1 开始编号的下标的规律:
- 规律 1:一个结点的左结点的下标是这个结点的下标的 2 倍;
- 规律 2:一个结点的右结点的下标是这个结点的下标的 2 倍 + 1。
因此
- 要想找到父结点:
,注意这里不能整除的时候需要向下取整; - 要想找到两个子结点:
, 。
温馨提示:这个两条性质不用记,我们只要拿一张纸,画一个草图,就能够推出父结点和孩子结点下标之间的关系。
上面这张图用数组表示出来就是一个最大堆,它在我们的程序中是这样表示的:
下标号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
元素值 | null |
在这里啰嗦一句,如果我们使用树结构来保存上面那张图的数据,我们要创建
下面给出了使用数组实现「最大堆」的一个基本结构。
Java 代码:
// 我们这个版本的实现中,0 号下标是不存数据的,这一点一定要注意
public class MaxHeap {
private int[] data;
// 当前堆中存储的元素的个数
private int count;
// 堆中能够存储的元素的最大数量(为简化问题,不考虑动态扩展)
private int capacity;
// 初始化最大堆
public MaxHeap(int capacity) {
// 初始化底层数组元素( 0 号下标位置不存数据,这是为了使得通过父结点获得左右孩子有更好的表达式)
data = new int[capacity + 1];
count = 0;
}
// 返回堆中的元素个数
public int size() {
return count;
}
// 返回一个布尔值,返回堆中是否为空
public boolean isEmpty() {
return count == 0;
}
}
Python 代码:
class MaxHeap:
def __init__(self, capacity):
# 我们这个版本的实现中,0 号索引是不存数据的,这一点一定要注意
# 因为数组从索引 1 开始存放数值
# 所以开辟 capacity + 1 这么多大小的空间
self.data = [None for _ in range(capacity + 1)]
# 当前堆中存储的元素的个数
self.count = 0
# 堆中能够存储的元素的最大数量(为简化问题,不考虑动态扩展)
self.capacity = capacity
def size(self):
"""
返回最大堆中的元素的个数
:return:
"""
return self.count
def is_empty(self):
"""
返回最大堆中的元素是否为空
:return:
"""
return self.count == 0
接下来两节的内容,我们介绍在「优先队列」的「出队」和「入队」操作中,如何保持「最大堆」数组的 堆有序 的性质。
作者:liweiwei1419 链接:https://suanfa8.com/priority-queue/implement 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。