使用 Java 实现优先队列
优先队列基本模型
优先队列的基本模型十分简单。可以向队列插入一个元素,也可以从队列删除一个元素。但需要注意的是,基于堆结构的优先队列,插入和删除操作极可能破坏优先队列的结构性和有序性。因此每完成一次操作,都需要重新调整队列结构以保证总是满足这两个特性。
优先队列的实现
优先队列的实现方式有很多,但最常用的还是 堆(heap) 结构。下面我们先简单讨论几种其它方式的实现,再动手实现一个基于堆结构的优先队列。
基于链表实现
优先队列的核心思想便是每次取出的元素都是当前队列优先级(优先级的逻辑意义可以自己定义)最高的,故你可以使用一个单链表,每次在表头插入一个元素,删除元素时遍历整个链表从而删除最小元素。这样的话插入操作为 O(1),删除操作为 O(N)。也可以通过排序算法让单链表始终有序,这样的话插入操作为 O(N),删除操作为 O(1)。
基于二叉查找树实现
二叉查找树结构很容易保证每次取出的都是优先级最高的元素。你只需要不断地访问左子树(假设左儿子优先级大于根节点),最终访问的节点一定是优先级最高的。查找树的插入和删除操作都能将时间复杂度保证在 O(logN),甚至你可以使用 AVL 树来避免过多删除操作使查找树严重失衡。但使用查找树来仅仅实现优先队列有些大材小用了。
基于堆实现
二叉堆(binary heap)
堆和二叉查找树一样,具有两个性质:结构性 和 堆序性。对堆的操作很可能破坏这两个性质。因此每次操作都需要重新调整堆的结构,而堆的一个巨大优势便是结构调整的效率很高,你只需要调整堆中的一条链路即可,平均时间复杂度仅为 O(logN)。
结构性:
堆的结构是一颗完全二叉树,一颗高为 h 的完全二叉树有 2h 到 2h+1 -1 个节点。意味着具有 N 个节点的完全二叉树高为 ⌊logN⌋。由于完全二叉树结构的规律性,堆常用一个数组即可实现。使用数组实现的堆,其 i 节点的父节点为 (i + 1) / 2,左儿子为 2i,右儿子为 2i + 1。
堆序性:
以小根堆为例,任意节点小于它的所有后裔。因此根节点就是堆中的最小 元素。
开始实现一个支持泛型的优先队列
属性
// 优先队列默认容量
private static final int DEFAULT_CAPACITY = 10;
// 当前元素个数
private int currentSize;
// 存储元素的数组
private E[] array;
插入一个元素
为了保证堆的结构性,我们应该在数组中最后一个元素的下一位置添加一个空节点 n 暂时作为新元素的插入位置,这样可以使得堆依然是一个完全二叉树,但问题是堆序性可能已经被打破。接着让待插入元素和节点 n 的父节点比较,如果待插入元素优先级低于 n 的父节点,则直接将元素插入在节点 n 处。否则,交换节点 n 和其父节点。接着,重复这个比较过程,使节点 n 不断向上调整,直至找到合适的位置并将新元素插入。这个过程称为 上滤(percolate up),由于节点 n 无论怎么调整,只会涉及完全二叉树中的某一条链路(因为只会和其父节点交换),故上滤操作的时间复杂度为 O(logN)。
public void insert(E x) {
if (currentSize == array.length - 1) {
enlargeArray(array.length * 2 + 1);
}
// Percolate up
int hole = ++currentSize;
array[hole] = x;
percolateUp(hole);
}
// 上滤操作
private void percolateUp(int hole) {
E temp = array[hole];
for (;temp.compareTo(array[hole / 2]) < 0; hole /= 2) {
array[hole] = array[hole / 2];
}
array[hole] = temp;
}
删除优先级最高的元素
删除优先级最高的元素一般可以分为两步:找出优先级最高的元素和删除它。在基于堆结构的优先队列中,优先级最高的元素一定是根,因此找到优先级最高的元素的代价仅为 O(1)。随后,我们不直接删除根节点,因为这会破坏堆的结构性。我们先暂时让根为空,然后将堆中最底层的最后一个元素 n 移动到根处,这样删除的是最后一个元素,堆依然保证了结构性。但由于临时的根 n 并不是优先级最高的元素,故堆序性遭到了破坏。我们让 n 和它两个儿子中优先级较高的一个交换位置,使得 n 下降一层,这样堆中优先级最高的元素成为了新的根。接着让 n 重复和它优先较高的儿子进行比较,如果 n 的优先级比其低,则继续交换位置,否则 n 达到了正确位置,堆结构调整结束。节点 n 不断下降的过程也被称为 下滤(percolate down),和上滤一样,下滤也只会涉及堆中的一条链路,故时间复杂度为 O(logN)。
public E deleteMin() {
if (isEmpty())
return null;
E minItem = findMin();
array[