堆排序
算法介绍
堆是一种数据结构,可以把堆看成一棵完全二叉树,这棵完全二叉树满足:任何一个非叶结点的值都不大于(或不小于)其左右孩子结点的值。若父亲大孩子小,叫大顶堆;若父亲小孩子大,则这样的堆叫做小顶堆。
二叉树:每个结点最多只有两棵子树,即二叉树中结点的度只能为0、1、2。子树有左右之分,不能颠倒。
满二叉树:在一棵二叉树中,如果所有的分支结点都有左孩子和右孩子结点,并且叶子结点都集中在二叉树的最下一层。另一种定义:一棵深度为k且有个结点的二叉树称为满二叉树。
完全二叉树:深度为k,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。通俗来讲,一棵完全二叉树是由一棵满二叉树从右至左从下而上,挨个删除结点所得到的。
堆排序的思想:将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样有序序列元素增加·个,无序序列中元素减少1个,对新的无序序列重复这样的操作,就实现了排序。
堆排序执行过程
l 将原始序列化为一个完全二叉树。
l 从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上,对每一个结点进行调整,最终将得到一个大顶堆。
对结点的调整方法:将当前结点(假设为a)的值与其孩子结点进行比较,如果存在大于a值的孩子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子结点值都小于a的值为止。
l 将当前无序序列中第一个元素,反映在树中是根节点(假设为a)与无序序列中最后一个元素交换(假设为b)。a进入有序序列,到达最终位置。无序序列中元素减少1个,有序序列中元素增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。
l 重复3)中的过程,知道无序序列中的元素剩下1个时排序结束
算法代码(Java)
/**
* 堆排序
* @author 卡罗-晨
*
*/
public class HeapSort {
/**
* 找出左孩子
* @param i 堆下标
* @return 左孩子的下标
*/
public static int leftChild(int i) {
return 2*i+1;
}
/**
* 用于删除一个最大值和建立堆
* @param a 无序序列
* @param i 过滤下来的位置
* @param n 堆的逻辑大小
*/
private static <T extends Comparable<? super T>> void percDown(T[] a,int i,int n) {
int child;
T tmp;
for (tmp = a[i];leftChild(i)<n;i=child) {
child = leftChild(i);
if(child != n-1 && a[child].compareTo(a[child+1])<0) {
child++;
}
if(tmp.compareTo(a[child])<0) {
a[i] = a[child];
}else
break;
}
a[i] = tmp;
}
/**
* 标准堆排序
* @param a可比较的集合
*/
public static <T extends Comparable<? super T>> void heapsort(T[] a) {
//建立堆
for(int i=a.length/2-1;i>=0;i--) {
percDown(a, i, a.length);
}
for(int i=a.length-1;i>0;i--) {
T tmp = a[0];
a[0] = a[i];
a[i] = tmp;
percDown(a, 0, i);
}
}
}
性能分析
时间复杂度
对于percDown()方法来说,显然child走的是一条从当前结点到叶子结点的路径,完全二叉树的高度为log(n+1),即对每个结点调整的时间复杂度为O(logn)。对于heapsort()方法,基本操作总数应该是两个并列的for循环中基本操作的次数相加,第一个循环的基本操作次数为O(logn)*n/2,第二个循环基本操作次数为O(logn)*(n-1),因此整个算法的基本操作次数问为O(logn)*n/2+ O(logn)*(n-1),化简后得其时间复杂度为O(nlogn)。
空间复杂度
从算法代码可以看出,本算法的额外空间只有一个tmp,因此额外空间复杂度为O(1)。
注意:堆排序的最坏情况下的时间复杂度也是O(nlogn),这是它相当于快速排序的最大优点,堆排序的空间复杂度为O(1),在所有时间复杂度为O(nlogn)的排序中是最小的,这也是其一大优点,堆排序适合的场景是记录数很多的情况。