堆排序属于选择排序,是对简单选择排序的一种优化。堆排序算法利用了完全二叉树的特点,这里的堆指的就是完全二叉树。完全二叉树的定义:除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。左对齐的意思就是一个非叶子结点只要有右孩子就一定有左孩子。堆排序中堆分为两种,一种大顶堆(父结点大于等于孩子结点,根结点最大),一种小顶堆(父结点小于等于孩子结点,根结点最小),本文以大顶堆为例。说明:网上关于堆排序的文章有好多,大部分讲的也非常好,但由于这一块不是很好理解,所以我在参考网上文章的同时,加入了自己的理解,疏漏之处欢迎读者批评指出。言归正传:
完全二叉树的特性:左孩子的位置=当前父节位置的2倍+1,右孩子的位置=当前父节点位置的2倍+2
这里位置指的是数组的下标(从0开始的那个)。
如根结点的下标为0,则根结点的左孩子的位置=0*2+1=1,根结点的右孩子的位置=0*2+2=2。
基本的知识介绍完以后,我们开始上代码:
public class HeapSort {
/**
* 堆排序函数
* @param a
*/
public static void sort(int[] a){
int n = a.length-1;
for (int i = (n-1)/2;i>=0;i--){
//i为最后一个根节点,n为数组最后一个元素的下标
//i的值依次为3,2,1,0(假设数组的长度为9时)
heapAdjust(a,i,n);
}
System.out.println("构造完成的大顶堆:");
for (int b:a) {
System.out.print(" "+b);
}
System.out.println();
for (int i = n;i>0;i--){
//把最大的数,也就是顶放到最后
//i每次减一,因为要放的位置每次都不是固定的,i就是堆的相对最后位置
swap(a,i);
//再构造大顶堆
heapAdjust(a,0,i-1);
}
}
/**
* 构造大顶堆函数,parent为父节点,length为数组最后一个元素的下标
* @param a
* @param parent
* @param length
*/
public static void heapAdjust(int[] a,int parent,int length){
//定义临时变量存储父节点中的数据,防止被覆盖
int temp = a[parent];
//parent*2+1是其左孩子的节点
for (int i = parent*2+1;i<= length;i=i*2+1){
//如果左孩子小于右孩子,就让i指向右孩子
if (i<length && a[i] < a[i+1]){
i++;
}
//如果父节点大于较大的孩子,那就退出循环
if(temp>a[i]){
break;
}
//如果父节点小于或等于较大的孩子节点,那就把孩子节点放到父节点上,这里体现了堆排序的不稳定
a[parent] = a[i];
//把孩子结点的下标赋值给parent,让其继续循环以保证大根堆构造正确
parent = i;
}
//将刚刚的父节点中的数据赋值给新位置
a[parent] = temp;
}
/**
* 定义swap函数
* 功能:将根元素与最后位置的元素交换
* 注意这里的最后是相对最后,是在变化的
* @param a
* @param i
*/
public static void swap(int[] a,int i){
int temp = a[0];
a[0] = a[i];
a[i]= temp;
}
}
关键地方在代码中都有标注,这里再说一下比较重要的一个地方。我觉得理解左孩子的位置=父节点位置*2+1是相当重要的,这对我们理解构造大顶堆很关键。
最后再贴一下测试代码:
@Test
public void testHeap(){
//定义整型数组
int[] a = {1,5,6,8,7,2,3,4,9};
HeapSort.sort(a);
//输出排序后的数组
System.out.println("输出排序后的数组:");
for (int n:a) {
System.out.print(" "+n);
}
}
个人心得:当不能理解堆排序的过程时,就在理解原理的基础上多写几遍代码,或者在纸上画一下堆排序的过程,对我们理解堆排序算法有很大的帮助。