优先队列(数组实现)
在有点时候我们不一定要使数组中的多有元素都有徐,我们每次只需要处理数组中的最大值,然后再收集更多的元素。例如,你可能有一台能同时运行多个应用的电脑或手机。这是通过为每个应用程序的时间分配一个优先级,并总是处理下一个优先级高的事件来实现的。
在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这种数据类型叫作优先队列。优先队列的使用和队列(删除最老的元素)以及栈(删除最新的元素)类似。
堆的定义
数据结构二叉堆能够很好的实现优先队列的基本操作,再二叉堆的数组中,每个元素都要保证大于等于另外两个特定位置的元素。相应的,这些位置的元素又要大于等于数组中的另外两个元素,依次类推。如果我们将所有元素画成一颗二叉树,将每个较大元素和两个较小元素用边连接就可以很容易的看出这种结构。当一棵二叉树的每个节点都大于等于它的两个子节点时,它被称为堆有序这样的二叉树我们很容易的可以看出其根节点就是此二叉树最大的节点。
二叉树表示法
如果我们用指针来表示堆有序的二叉树,那么每个元素都需要三个指针来找到他的上下节点(父节点和两个子节点个需一个),如下图我们可以用完全二叉树来表示,而完全二叉树又能用数组表示,而不需要指针。具体方法就是将二叉树的节点按照层级顺序放入数组中,根节点在位置1,它的子节点在位置2和3,而子节点的子节点则在4,5,6和7,以此类推。
我们很容易发现,用数组表示的堆,位置为k的节点的父节点位置为k/2,它的两个子节点的位置分别为2k和2k+1.
堆的表示
我们使用长度为N+1的数组pq[]来表示一个大小为N的堆,我们不会使用pq[0],堆元素放在pq[1]至pq[N+1]中。
堆的有序化
在堆的有序化中我们会遇到两种情况,当某个节点的优先级上升(或者是在堆的底部加入一个新的元素)时,我们需要由下至上恢复堆的顺序。当某个节点的优先级下降(例如,将根节点替换为一个较小的元素)时,我们需要由上到下恢复堆的顺序。下面我们分别来看一下这两个辅助操作。
由下至上的堆有序(上浮)
如果堆的有序状态因为某个节点变得比他的父节点更大而被打破,那么我们就需要通过交换它和它的父节点类修复堆。交换后,这个节点比它的两个子节点都打(一个是曾经的父节点,里那个一个比它更小,因为它是曾经父节点的子节点),但这个节点任然可能比它现在的父节点更大。我们就可以重复上述操作,将这个节点不断向上移动直到我们遇到一个更大的父节点。我们直到位置为k的节点的父节点是k/2,这个过程实现起来很简单,我们可以用swim()方法来表示,swim()方法中的循环只有位置k的节点小于它的父节点(k/2)时才会被打破。方法实现(方法追加到原先的工具类中,详情请点击这里)如下:
public static void swim(Comparable[] a,int k){
while(k > 1 && less(a[k/2], a[k])){
exch(a, k/2, k);
k = k/2;
}
}
由上至下的堆有序(下沉)
如果堆的有序状态因为某个节点变得比它的子节点或者其中之一更小了而被打破了,那么我们就可以通过将它和它的两个子节点中较大者交换来恢复堆。交换可能在子节点处继续打破堆的有序状态,因此我们需要不断的用相同的方式将其修复,将节点向下移动到它的子节点都比它更小或者到达了堆的底部。我们知道位置为k的节点的子节点的位置是2k和2k+1,我们用sink()方法来表示,实现如下:
public static void sink(Comparable[] a,int k){
int N = a.length;
while(2*k<N){
int j = 2 * k;
if (j < N && less(a[j], a[j+1])) {
j++;
}
if (!less(a[k], a[j])) {
break;
}
exch(a, k, j);
k = j;
}
}
**插入元素:**我们将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置。
**删除最大元素:**我们从数组顶端删去最大的元素,并将数组中的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。