本文内容基于《数据结构与算法分析 Java语言描述》第三版,冯舜玺等译。
参考以下场景:
- 若在打印机有空时正好有多个单页的作业及一项100页的作业等待打印,则更合理的做法也许是最后处理长的作业,尽管它不是最后提交上来的。
- 在多用户环境中,操作系统调度程序必须决定在若干进程中运行哪个进程,一般来说,短的作业要尽可能快地结束,应该拥有优先权。
这些特殊的应用需要一类特殊的队列,称之为优先队列。
1. 模型
优先队列是允许至少两种操作的数据结构:insert以及deleteMin。
2. 实现
可以使用一个简单链表在表头以O(1)执行插入操作,并遍历该链表以删除最小元,这需要O(N)时间。
或者反过来始终让链表保持排序状态,这样插入为O(N),删除为O(1)。
另一种实现方法是使用二叉查找树,两种操作的平均运行时间都是O(logN)。
3. 二叉堆
堆有两个性质:结构性和堆序性。恰似AVL树,对堆的一次操作可能破坏这两个性质中的一个,因此堆的操作必须到堆的所有性质都被满足时才能终止。
3.1 结构性质
堆是一颗被完全填满的二叉树,有可能的例外是在底层,底层上的元素从左到右填入,这样的树被称为完全二叉树。
因为完全二叉树很有规律,所以可以用一个数组表示:
对于数组中任一位置i上的元素,其左儿子在位置2i上,右儿子在左儿子后的单元(2i+1)中,它的父亲则在位置i/2上。
这种实现方法的唯一问题在于最大堆的达到需要事先估计(如有需要,可以重新调整)。
因此,一个堆结构将由一个Comparable对象的数组和一个代表当前堆的大小的整数组成。
3.2 堆序性质
最小元应该在根上,即在一个堆中,对于每一个节点X,X的父亲的关键字小于(或等于)X中的关键字,根节点除外(无父亲)。
3.3 堆操作
3.3.1 insert
为将一个元素X插入到堆中,先在下一个可用位置创建一个空穴。如果X可以放在该空穴中而并不破坏堆序,那么插入完成。否则把空穴的父节点上的元素移入到该空穴中。这个策略叫做上滤。
3.3.2 deleteMin
当删除一个最小元时,先在根节点建立一个空穴。由于现在堆少了一个元素,因此堆中最后一个元素X必须移动到该堆的某个地方。如果X可以被放到空穴中,那么deleteMin完成。否则将空穴的两个儿子中较小者移入空穴。这个策略叫做下滤。
4. 实现代码
/**
* Construct the binary heap.
*/
public BinaryHeap() {
this(DEFAULT_CAPACITY);
}
/**
* Construct the binary heap.
* @param capacity the capacity of the binary heap.
*/
public BinaryHeap(int capacity) {
currentSize = 0;
array = (AnyType[]) new Comparable[capacity + 1];
}
/**
* Construct the binary heap given an array of items.
*/
public BinaryHeap(AnyType[] items) {
currentSize = items.length;
array = (AnyType[]) new Comparable[(currentSize + 2) * 11 / 10];
int i = 1;
for (AnyType item : items) {
array[i++] = item;
}
buildHeap();
}
/**
* Insert into the priority queue, maintaining heap order.
* Duplicates are allowed.
* @param x the item to insert.
*/
public void insert(AnyType x) {
if (currentSize == array.length - 1) {
enlargeArray(array.length * 2 + 1);
}
// Percolate up
int hole = ++currentSize;
for (array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2) {
array[hole] = array[hole / 2];
}
array[hole] = x;
}
private void enlargeArray(int newSize) {
AnyType[] old = array;
array = (AnyType[]) new Comparable[newSize];
for (int i = 0; i < old.length; i++) {
array[i] = old[i];
}
}
/**
* Find the smallest item in the priority queue.
* @return the smallest item, or throw an UnderflowException if empty.
*/
public AnyType findMin() {
if (isEmpty()) {
throw new UnderflowException();
}
return array[1];
}
/**
* Remove the smallest item from the priority queue.
* @return the smallest item, or throw an UnderflowException if empty.
*/
public AnyType deleteMin() {
if (isEmpty()) {
throw new UnderflowException();
}
AnyType minItem = findMin();
array[1] = array[currentSize--];
percolateDown(1);
return minItem;
}
/**
* Establish heap order property from an arbitrary
* arrangement of items. Runs in linear time.
*/
private void buildHeap() {
for (int i = currentSize / 2; i > 0; i--) {
percolateDown(i);
}
}
/**
* Test if the priority queue is logically empty.
* @return true if empty, false otherwise.
*/
public boolean isEmpty() {
return currentSize == 0;
}
/**
* Make the priority queue logically empty.
*/
public void makeEmpty() {
currentSize = 0;
}
private static final int DEFAULT_CAPACITY = 10;
private int currentSize; // Number of elements in heap
private AnyType[] array; // The heap array
/**
* Internal method to percolate down in the heap.
* @param hole the index at which the percolate begins.
*/
private void percolateDown(int hole) {
int child;
AnyType tmp = array[hole];
for (; hole * 2 <= currentSize; hole = child) {
child = hole * 2;
if (child != currentSize && array[child + 1].compareTo(array[child]) < 0) {
child++;
}
if (array[child].compareTo(tmp) < 0) {
array[hole] = array[child];
} else {
break;
}
}
array[hole] = tmp;
}
// Test program
public static void main(String[] args) {
int numItems = 10000;
BinaryHeap<Integer> h = new BinaryHeap<>();
int i = 37;
for (i = 37; i != 0; i = (i + 37) % numItems) {
h.insert(i);
}
for (i = 1; i < numItems; i++) {
if (h.deleteMin() != i) {
System.out.println("Oops! " + i);
}
}
}
}