排序算法(四):优先队列、二叉堆以及堆排序
优先队列
我们经常会碰到下面这种情况,并不需要将所有数据排序,只需要取出数据中最大(或最小)的几个元素,如排行榜。
那么这种情况下就可以使用优先队列,优先队列是一个抽象数据类型,最重要的操作就是删除最大元素和插入元素,插入元素的时候就顺便将该元素排序(其实是堆有序,后面介绍)了。
二叉堆
二叉堆其实是优先队列的一种实现,下面主要讲的是用数组实现二叉堆。
先上一个实例:
如有一个数组A{9,7,8,3,0,6,5,1,2}
用二叉树来表示数组更直观:
从这张图我们可以总结一些规律:
- 当一个二叉树的每个结点都大于等于它的两个子节点时,称为堆有序
- 根节点是堆有序的二叉树中的最大结点
- 在数组中,位置为K的结点的父节点,位置为K/2,它的两个子节点位置分别为:2K和2K+1(下标从1开始,A[0]不使用)
上面这三点应该非常好理解
下面就引出一个问题,怎样让一个数组变成堆有序呢?
首先,需要介绍两个操作:
- 由下至上的堆有序化(上浮)
当插入一个结点,或改变一个结点的值时,上浮指的是交换它和它的父节点以达到堆有序
在上面的堆有序的图中,如果我们把0换成10,那么上浮的操作具体为:
(1)10比它的父节点7大,所以交换
(2)交换后,10比它的父节点9还要打,交换
之后得到的二叉树如下图:
代码如下(需要注意,下标是从1开始,A[0]保留不用,以下所有代码相同):
//index based on 1 public void swim(Integer[] a,Integer key) { while(key > 1 && a[key/2] < a[key]) { change(a,key/2,key); key /= 2; } }
2. 由上至下的堆有序化(下沉)
由上浮可以很容易得出下沉的概念:
当插入一个结点,或改变一个结点的值时,下沉指的是交换它和它的较大子节点以达到堆有序。
在原来的二叉树中,如果将根节点9换成4,操作如下:
(1)4与它的最大子节点8交换位置
(2)4与它的最大子节点6交换位置
交换后的二叉树如下图:
代码如下:
//index based on 1 public void sink(Integer[] a,Integer key) { Integer max = key*2; while(key*2 < a.length - 1) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } }
那么将一个数组构造成有序堆,相应的也有两种方法:使用上浮以及使用下沉:
初始数组如下:
Integer[] a = {null,2,1,5,9,0,6,8,7,3};
上浮构造有序堆:
从数组左边到右边依次使用上浮,因为根节点A[1]没有父节点,所以从A[2]开始:
public void buildBinaryHeapWithSwim(Integer[] a) { for(int k=2;k<a.length;k++) { swim(a,k); } }
结果如下:
a: [null ,9 ,7 ,8 ,5 ,0 ,2 ,6 ,1 ,3] 读者有兴趣可以自己画一下二叉树,看是否有序
下沉构造有序堆:
代码: public void buildBinaryHeapWithSink(Integer[] a) { //index based on 1 for(int k=a.length/2;k>=1;k--) { sink(a,k); } }
为什么使用下沉只需要遍历数组左半边呢?
因为对于一个数组,每一个元素都已经是一个子堆的根节点了,sink()对于这些自对也适用。如果一个结点的两个子节点都已经是有序堆了,那么在该结点上调用sink(),可以让整个数组变成有序堆,这个过程会递归的建立起有序堆的秩序。我们只需要扫描数组中一半的元素,跳过叶子节点。
a: [null ,9 ,7 ,8 ,3 ,0 ,6 ,5 ,1 ,2]
可以看到使用下沉和上浮构造出来的有序堆并不相同,那么用哪一个更好呢?
答案是使用下沉构造有序堆更好,构造一个有N个元素的有序堆,只需少于2N次比较以及少于N次交换。
证明过程就略过了。
堆排序
前面说了那么多,终于要说到堆排序了,其实前面的优先队列和二叉堆都是为了堆排序做准备。
现在我们知道如果将一个数组构造成有序堆的话,那么数组中最大的元素就是有序堆的根节点。
那么很容易想到一个排序的思路:
第一种:将数组构造成有序堆,将根节点拿出来,即将A[1]拿出(因为A[0]不用,当然也可以使用,读者可以自己编程实现),对剩下的数组再构造有序堆……
不过第一种思路只能降序排列,并且需要构造一个数组用来存放取出的最大元素,以及最大的弊端是取出最大元素后,数组剩下的其它所有元素需要左移。
那么第二种办法就可以避免以上的问题:
第二种:先看图:
先来解释下这幅图:
- 一开始先将数组构造成一个有序二叉堆,如图1
- 因为有序二叉堆的最大元素就是根节点,将根节点和最后一个元素交换。
- 从index=1到index=a.lenth-1开始调用sink方法重新构造有序二叉堆。(即第二步交换过的最大元素不参与这次的构造)
- 经过第三步后,得到数组中第二大的元素即为根节点。
-
再次交换根节点和倒数第二个元素
…….
这样循环下去,即得到按升序排序的数组
代码:
public void heapSort(Integer[] a) { for(int k=a.length/2;k>=1;k--) { sink(a,k); } Integer n = a.length - 1; while(n > 0) { change(a,1,n--); //去除最后一个元素,即前一个有序堆的最大元素 sink(a,1,n); } }
注意在while循环中,sink()方法多了一个参数,这个参数的目的是去掉上一个有序堆的最大元素。
全部代码如下:
public class HeapSort extends SortBase { /* (non-Javadoc) * @see Sort.SortBase#sort(java.lang.Integer[]) */ @Override public Integer[] sort(Integer[] a) { // TODO Auto-generated method stub print("init",a); heapSort(a); print("result",a); return null; } public void buildBinaryHeapWithSink(Integer[] a) { //index based on 1 for(int k=a.length/2;k>=1;k--) { sink(a,k); } } public void buildBinaryHeapWithSwim(Integer[] a) { for(int k=2;k<a.length;k++) { swim(a,k); } } public void heapSort(Integer[] a) { for(int k=a.length/2;k>=1;k--) { sink(a,k); } Integer n = a.length - 1; while(n > 0) { change(a,1,n--); //去除最后一个元素,即前一个有序堆的最大元素 sink(a,1,n); } } //index based on 1 public void swim(Integer[] a,Integer key) { while(key > 1 && a[key/2] < a[key]) { change(a,key/2,key); key /= 2; } } //index based on 1 public void sink(Integer[] a,Integer key) { Integer max = key*2; while(key*2 < a.length - 1) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } } public void sink(Integer[] a,Integer key,Integer n) { Integer max = key*2; while(key*2 < n) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } } public static void main(String[] args) { Integer[] a = {null,2,1,5,9,0,6,8,7,3}; //(new HeapSort()).sort(a); (new HeapSort()).buildBinaryHeapWithSink(a); print("a",a); } }
堆排序的平均时间复杂度为NlogN
作者