【数据结构与算法分析】05:优先队列(堆)
目录
优先队列(堆)
优先队列是一种特殊的队列,是允许至少下列两种操作的数据结构:Insert(插入)以及DeleteMin(删除最小者)。DeleteMin操作要求,优先队列要有办法能够找出最小者,在优先队列的实现中,如何做到花费最优实现插入和删除最小值,是主要需要探讨的问题。
1简单的想法
有几种显而易见的方式实现优先队列:
- 链表:表头以O(1)执行插入操作,并遍历该链表以删除最小元(耗费O(N)时间);
- 有序链表:让表保持有序状态,使得插入代价为O(N)而删除花费O(1);
- 二叉查找树:对两种操作的平均时间都是O(log N),但是很“浪费”,因为它支持许多并不需要的操作。
以上想法中,3明显是时间最优的方式,那么,有没有不“浪费”,同时做到最坏情形时间O(log N)的数据结构呢?下面介绍二叉堆。
1.1二叉堆
二叉堆一般我们习惯性称之为“堆”,同二叉查找树一样,堆也有两个性质,即结构性和堆序性。正如AVL树一样,对堆的一次操作可能破坏这两个性质中的一个,因此堆的操作必须要到堆的所有性质被满足时才能终止。
1.1.1结构性质
堆是一棵完全被填满的二叉树,有可能的例外是在底层,底层上的元素从左至右填入。这样的树称为完全二叉树。如下图所示:
容易证明,一棵高为h的完全二叉树,有
完全二叉树很有规律,可以用一个数组表示,而不需要指针,上图的二叉树用数组可以表示如下:
A | B | C | D | E | F | G | H | I | ||
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
对于数组中任一位置i上的元素,其做儿子在位置2i上,右儿子在做儿子后的单元(2i+1)中,它的父亲则在位置[i/2](向下取整)上。因此,不仅指针不需要,而且遍历该树所需要的操作也非常简单。唯一的问题在于,最大的堆大小需要事先估计。
1.1.2堆序性质
使操作被快速执行的性质是堆序性。我们想要快速的找到最小元,所以最小元应该放在根上。同理,任意节点都应该小于它的后裔。
根据堆序性质,最小元总可以在根处找到,因此我们以常数时间完成附加运算FindMin。
1.2堆操作
1.2.1基本操作
Insert:
为了将一个元素X插入到堆中,我们在下一个空闲位置创建一个空穴,否则堆将不是完全树。如果X可以放在该空穴而并不破坏堆的序,那么插入完成,否则,我们把空穴的父节点上的元素移入该空穴中,这样空穴就朝着根的方向上行一步。继续改过程直到X能被放入空穴中位置。
为了插入14,首先在堆的下一个空闲位置创建一个空穴:
由于将14插入空穴破坏了堆序性质,因此将31移入该空穴:
继续这种策略,将21移入空穴:
最后找到14的正确位置:
这种一般的策略叫做上滤(percolate up);新元素在堆中上滤直到找到正确的位置。
代码如下:
public <T extends Comparable<T>> Boolean insert(T x) {
if (isFull()) {
System.out.println("ERROR:Priority queue is full");
return false;
}
Node node = new Node(x);
dataArry[size] = node;
PercolateUp(size++);
return true;
}
//上滤法
private void PercolateUp(int index) {
int parent = (index - 1)/2;//父节点索引
Node bottom = dataArry[index];//新加的最后一个节点
while(index > 0 &&
(dataArry[parent].getElement().compareTo(bottom.getElement())<0)){
dataArry[index] = dataArry[parent];
index = parent;
parent = (index-1)/2;
}
dataArry[index] = bottom;
}
DeleteMin:
DeleteMin以类似于插入的方式处理,找出最小元是容易的,删除确实困难的。删除一个最小元时,在根节点处产生了一个空穴,由于现在堆少了一个元素。因此堆中最后一个元素X必须移动到该堆的某个地方。如果X可以放入空穴中,那么DeleteMin完成。若不行,那么空穴中的两个儿子中较小者移入空穴,这样就把空穴向下推一层,重复该步骤直到X可以被放入空穴中。
删除最小元13后,我们必须将31放入堆中的正确位置:
31不能放入空穴,于是我们把较小的儿子14放入空穴,同时空穴下滑一层:
重复该过程,将24置入空穴,下一层建立一个新的空穴:
最后将31置入空穴:
这种一般的策略叫做下滤(percolate down),代码实现如下:
public <T extends Comparable<T>> Node DeleteMin() {
if (isEmpty()) {
System.out.println("ERROR:Priority queue is empty");
return null;
}
Node<T> minNode = dataArry[0];
PercolateDown();
return minNode;
}
private void PercolateDown() {
int index = 0;
int largeChildIndex;
Node lastNode = dataArry[size - 1];
while (index < size / 2) {
int leftChildIndex = 2 * index + 1;
int rightChildIndex = leftChildIndex + 1;
if (leftChildIndex < size
&& (dataArry[leftChildIndex].getElement().compareTo(dataArry[rightChildIndex].getElement()) < 0)) {
largeChildIndex = rightChildIndex;
} else {
largeChildIndex = leftChildIndex;
}
if (lastNode.getElement().compareTo(dataArry[largeChildIndex].getElement()) >= 0) {
break;
}
dataArry[index] = dataArry[largeChildIndex];
index = largeChildIndex;
}
dataArry[index] = lastNode;
}
2.d-堆
d-堆是二叉堆的推广,它就像一个二叉堆,只是所有节点都有d个儿子(因此,二叉堆是2-堆)。
下图展示的是一个3-堆。
注意,d-堆要比二叉堆浅的多,它将Insert操作改进为O(logd N)。然而,对于大的d,DeleteMin操作费时的多,因为虽然树浅了,但是d个儿子中的最小者必须要找出,如果用标准算法,会花费d-1次比较。
先就写到这,以后再补充
上一篇: 数组foreach引发的小问题