您的位置:首页 > 理论基础 > 数据结构算法

《算法4》优先队列和堆排序

2017-10-12 22:14 218 查看

优先队列和堆

优先队列支持两种操作:删除最大元素和插入元素。可以用有序或者无序数组完成这个数据结构,但是用二叉堆(以下简称堆)可以更加快速地完成元素的删除和插入。

堆的插入和删除元素的最大数量级都是logN

堆的定义:堆就是一个每个节点都大于它的子节点的二叉树,也叫最大优先队列。

堆的第一个元素可以是最大的或者是最小的,前者称为最大优先队列,后者是最小优先队列,这里我们讨论最大优先队列,对于最小优先队列只需要在比较函数less()中将小于号“<”反向即可得到最小优先队列。

可以用数组来表示堆,这里从数组的a[1]开始,对于元素a[k]它的子节点是a[2k]和a[2k+1] ,它的父节点是a[⌊k2⌋]



上图显示出了一个堆的排布方式。

堆的实现

在实现堆之前先定义两种操作,称为上浮(swim)和下沉(sink)。先假设我们有一个有序的二叉堆,然后由于不知道什么原因,某个元素发生了变化造成了整个堆不是有序的了。那么,我们可以通过上浮和下沉重新让堆变成有序的。

上浮

如果一个元素突然变得比他的父节点大,那么他就需要上浮。因为假设其他部分都是有序的,那么,交换该节点和它的父节点,现在这个结点的子树是堆有序的。因为该节点大于其父节点,而父节点又大于他的另一个子节点,那么交换后的这个节点大于它的两个子节点,这两个子节点的子树一定也是堆有序的,那么可知该节点的子树是堆有序的。然后观察该节点是否还是比其父节点小,是的话再继续交换上浮,直到他比其父节点小。由上面的证明可知此时的整个堆是有序的。

private void swim(int k){
while(k>1 && less(pq[k/2], pq[k])){  //这个地方k最小不能为1,就是因为数组的0位置处是不使用的
exch(pq, k/2, k);
k = k/2;
}
}


下沉

下沉原理上和上浮类似,当一个元素比其子节点小的时候,交换该节点和这个更大的子节点。证明就略过,可以知道,当一个节点下沉到他比最大的子节点都大的时候这个堆是有序的。值得注意的是每次sink()都要进行两次比较,一次是找出更大的子节点,一次是判断是否比更大的子节点大。

private void sink(int k){
while(2*k<N){
int j=2*k;
if (j+1<=N &&  less(pq[j], pq[j+1])) j++;
if (!less(pq[k], pq[j])) break;
exch(pq, k, j);
k=j;
}
}


插入

向一个堆中插入元素,把该元素放在数组的最后,那么这个状态下满足我们证明中除了一个元素其他都是堆有序的状态,那么通过对这个元素进行上浮操作,就能够最后让堆有序。

删除

只有第一个元素是有意义的,一般我们都是删除这个元素。第一个元素删除之后,我们将最后一个元素复制到第一个的位置。那么这时也满足除了第一个元素之外,其他元素是堆有序的。然后就可以通过下沉操作让堆重新达到有序。不要忘了最后为了防止对象游离要把最后一个值设为null。这里解释一下对象游离,Java的数组除了基本数据类型,数组里面存的都是引用(这样操作起来也更快速),当一个数据被弹出或者删除,就这个数据本身来说,肯定是不会再被使用了,但是数组里面依然有它的引用,那么垃圾收集器就不能将其占用的内存回收。如果把数组里面对它的引用置为null,那么就没有任何引用指向这个被删除的数据,GC就可以把它回收了。

完整的最大优先队列代码如下:


public class MaxPQ<key extends Comparable<key>> {
private key[] pq;
private int N=0;

public MaxPQ(int maxN){
pq = (key[]) new Comparable[maxN+1];
}

private void swim(int k){ while(k>1 && less(pq[k/2], pq[k])){ //这个地方k最小不能为1,就是因为数组的0位置处是不使用的 exch(pq, k/2, k); k = k/2; } }

private void sink(int k){ while(2*k<N){ int j=2*k; if (j+1<=N && less(pq[j], pq[j+1])) j++; if (!less(pq[k], pq[j])) break; exch(pq, k, j); k=j; } }

public void insert(key v){
pq[++N] = v;
swim(N);
}

public key delMax(){
key key = pq[1];
exch(pq, 1, N--);
pq[N+1] = null;
sink(1);
return key;
}

private static boolean less(Comparable a,Comparable b){
return a.compareTo(b)<0;
}

private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}

}


优先队列总结

实际使用中还可以有很多改进比如加进动态调整数组大小的代码,加入利用一个数组作为参数的构造函数,给每个元素加个索引变成索引优先队列等等

对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大元素操作最多不超过2lgN次比较

堆排序

有了优先队列之后,使用它排序的原理非常好理解。

对于一组元素,直接建堆,然后将第一个元素和最后一个元素交换,然后堆的规模减一,再对较小的堆做下沉操作使其有序,之后重复以上步骤,直至排序完毕。

但是这里的建堆方式是从下往上直接建堆,在下方的sort()函数中我们从k=N/2开始,这是标号最大的非叶节点,对它进行sink()可以将这个最小的只有三个节点的子树堆有序化,然后k不断减一,从右到左,从下到上,从小到大不断地sink(),最后整个堆一定是堆有序的建堆过程中发生的交换次数为

∑h=0⌊lgN⌋⌈n2h+1⌉h≈12n∑h=0∞h2h=n

比较次数为交换的两倍为2n。

public class HeapSorting {
private static boolean less(Comparable a,Comparable b){
return a.compareTo(b)<0;
}

private static void exch(Comparable[] a,int i,int j){
i--;
j--;
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}

private static void sink(Comparable[] pq,int k, int N){
while(2*k<=N){
int j=2*k;
if (j<N &&  less(pq[j-1], pq[j])) j++;
//原来是less(pq[j], pq[j+1]),这里要减1,因为优先队列是从1计算的,而输入的数组是从0开始计算的
if (!less(pq[k-1], pq[j-1])) break;//原来是!less(pq[k], pq[j])
exch(pq, k, j);
k=j;
}
}

public static void sort (Comparable[] a){
int N = a.length;
for (int k=N/2;k>=1;k--){//从下至上建堆
sink(a, k, N);
}
while(N>1){
exch(a, 1, N--);
sink(a, 1, N);
}
}

}


唯一要注意的就是我们的堆是从1开始的,而数组是从0开始的,所以在less()和exch()两个函数中对引用要减一

将N个元素排序,堆排序只需要(2NlogN+2N)次比较,以及一半次数的交换
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息