排序算法(四):优先队列、二叉堆以及堆排序
2015-09-07 12:35
525 查看
优先队列
我们经常会碰到下面这种情况,并不需要将所有数据排序,只需要取出数据中最大(或最小)的几个元素,如排行榜。那么这种情况下就可以使用优先队列,优先队列是一个抽象数据类型,最重要的操作就是删除最大元素和插入元素,插入元素的时候就顺便将该元素排序(其实是堆有序,后面介绍)了。
二叉堆
二叉堆其实是优先队列的一种实现,下面主要讲的是用数组实现二叉堆。先上一个实例:
如有一个数组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
相关文章推荐
- 【转】Objective-C 与 Runtime:为什么是这样?
- Apache设置局域网内ip地址访问
- matlab保存figure
- 防止SQL注入
- Session生命周期讨论
- centos 7 systemd docker http proxy
- h.264中的DTS和PTS
- sap和dinic模板
- SVN:show log with no date或者日志不显示的解决方法
- 下拉列表
- cisco交换机通过acl控制vlan间互访
- Mysqldump --single-transaction 选项解析
- c# System.Threading.Timer是多线程,并能同时执行
- 64位longlong格式化字符串
- ios 修改navigationBar返回键的颜色和文字
- spinner的使用
- JS实现弹性菜单效果代码
- 前端性能优化的14个规则
- LeetCode 82 - Remove Duplicates from Sorted List II
- 网页和客户端传递参数的方法