什么是堆:堆就是一个具有下列性质的完全二叉树:每个节点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者是每个节点的值都小于等于其左右孩子结点的值。称为小顶堆。(二叉树的性质5:对于一个有n个结点的完全二叉树(其深度为log2 n +1的下界)的结点按层层序编号,对于任意一个结点i有①如果i=1则该节点是二叉树的根,无双亲,如果i>1则其双亲是节点i/2的下界②如果2i>n则节点i无左孩子;否则其左孩子节点是节点2i③如果2i+1>n,则节点i无右孩子;否则其右孩子的结点是2i+1)
堆排序是个啥:堆排序就是利用堆这个数据结构进行排序(以大顶堆,也就是最大堆为例)的方法,他的基本思想就是,将待排的序列构造成一个大顶堆,此时,根据大顶堆的性质可知,最顶上的数据一定是最大的,然后将它和数组的最后一个数据进行交换,然后把去掉最后已经排好的的元素再构造成一个大顶堆,重复此类操作,就可以得到一个有序的序列。
堆排序要解决的两个问题:
如何构建一个大顶堆和在交换元素之后如何把剩下的元素重新构建成一个新的大顶堆。
根据思想所以会得到如下代码:
public static void heapSort(int[] table){
//拿到待排序数组的长度
int n=table.length;
//创建大顶堆
//由于完全二叉树的性质,其实就是从下到上、从右到左,将每个非终端点(非叶子结点)当做根节点,将其和其子树调整为大顶堆
//j的初始值就是最后一个非叶子结点,n-1就是最后一个结点(全是层次遍历)
for(int j=n/2-1;j>=0;j--){
heapAdjust(table,j,n-1);
}
//把这个大顶堆最顶上的元素也就是第一个元素和最后一个元素进行调换
//然后把去掉最后一个元素的数组调整成为一个新的大顶堆,然后依次类推
for(int j=n-1;j>0;j--){
int temp = table[0];
table[0] = table[j];
table[j] = temp;
heapAdjust(table,0,j-1);
}
}
其实最重要的就是如何把一组元素调整为一个大顶堆了,解决了这个问题,堆排序也就解决了。
下面来看代码:
//将这组数据调整为一个大顶堆,begin和end表示该排序算法的范围
public static void heapAdjust(int[] table,int begin,int end){
//i为子树的根,j为i结点的左孩子
int i = begin,j;
//获得第i个元素的值,就是把该子树根节点的值先放在辅助的空间中
int temp = table[i];
//从左孩子结点开始寻找最大值,沿最大值孩子结点向下筛选
for(j=2*i;j<=end;j*=2){
//数组元素比较(改>为最小堆)
if(j<end&&table[j]<table[j+1]){
//j为左右孩子的较大者,其实就是找到该节点左右孩子最大值的坐标
j++;
}
//把根节点与值较大的孩子节点进行比较,若父母结点值较大(改<为最小堆)
if(temp>=table[j]){
break;
}
//孩子结点中较大值上移
//要注意的是,该句执行完毕之后,若进行交换操作之后,此时根节点的值和孩子节点值较大的坐标的值都为该孩子节点的值
table[i] = table[j];
//i、j向下一层,i的值此时为该子树值最大的坐标值
i=j;
}
table[i] = temp;//把根节点的值插入到值较大的孩子节点的位置
}
堆排序的时间复杂度分析:我们通过上面对堆排序算法的学习,我们知道,其实堆排序的运行时间主要是花费在初始构建堆和重建
堆时的反复筛选上。①在构建堆时,从堆排序的原理易知,我们是从一颗完全二叉树的最下面最右面的非叶子结点开始的,将其结
点的值与其子节点的值进行比较,然后满足条件时进行互换,从这儿来看出,其实在构建堆排序时最多只需要进行两次比较和互换
操作,所以构建堆的时间复杂度就是O(n)②在正式排序时,第i次取堆记录重建堆时需要O(log i)的时间(因为完全二叉树的深度为log2 n
向下取整并加1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度就为O(nlogn)。
综上,我们得到堆排序的时间复杂度为O(nlogn),所以我们知道堆排序的时间主要是花在堆重建上,其对初始数据的顺序并不敏感,
无论初始数据如何,它的时间复杂度都为O(nlogn),这在性能上要远远好于冒泡、选择、插入排序的O(n^2)了。在空间复杂度上
,其也只需要一个辅助单元 ,因为堆排序的比较和交换是跳跃式的,所以,其是一种不稳定的排序算法。