1、堆排序的堆,其实是一个 完全二叉树。既是一个结点要么是叶子结点,要么必定有左右两个子节点的树。
2、堆有序:每个结点的值,都必须大于两个子节点。但是两个子结点的大小不作要求。
3、一棵大小为N的完全二叉树,高度为lgN(层)。
用数组实现堆,假设数组下标从0开始,下标为k的元素,它的左子树是2k+1,右子树是左子树+1,即2k+2
一:由上至下的有序化(下沉)
如果堆的有序状态,因为某个结点比它的两个子结点或者其中之一小而打破了,那么可以通过与两个子结点中的较大者来交换。
交换后可能会在子结点处打破原有序状态,那么只需要继续下沉,直至叶子结点。
因为是在子树中做交换,对树的其他分支并没有影响。
//下沉维护堆 public static void sink(int[]a,int k,int N){ while(2*k+1<=N){ int j = 2*k+1;//左孩子的下标 //j<N,说明存在右孩子 //a[j]<a[j+1],左孩子小于右孩子 if(j<N&&a[j]<a[j+1]) j++; //如果根结点并没有小于他的孩子,不用下沉了 if(!(a[k]<a[j])) break; //否则 Example.exch(a, k, j); //交换后,更改当前,继续下沉 k=j; }//end while }//end sink
二:下沉实现的堆排序(原地排序)
1、构建堆
对于一个普通的数组,我们可以将他看成一个无序的堆。
要使用堆排序,第一步就要将该数组构造成一个有序堆。
一个简单的思想是,创建一个新数组,通过堆的插入元素方法,一个一个插入原数组的元素,进而完成一个新堆。
其实并不需要,我们只要将前N/2的元素,下沉到合适的位置即可,此时堆即有序,后N/2全是叶子结点,无需下沉。
2、下沉排序
根据定义,根结点,既是数组的0位元素,永远是有序堆的最大值。
所以我们只需要将它和数组最后一个元素交换位置,再将大小为N-1的数组(除去最后一个元素,因为他是最大值,放在最后),从新构建成一个新堆即可。怎么构建呢?就是把新的0号元素下沉到适合的位置。
如此往复,即可得到一个有序的数组。
//下沉排序 public static void sort(int []a){ //构建堆 //一开始可以将数组看作无序的堆 //将从下标为N/2开始一直到0的元素下沉到合适的位置即可。N/2后面的元素,其实都是叶子结点,无需下沉。 int N = a.length-1; for(int k = N/2;k>=0;k--){ sink(a,k,N); } //下沉排序 //堆的根结点永远是最大值,我们只需将最大值和最后一位的元素互换位置。然后再维护一个除原最大结点以外的N-1的堆,再将新堆的根节点放在倒数第二的位置。如此反复 while(N>0){ Example.exch(a, 0, N--); sink(a,0,N); } }//end sort
时间复杂度:
主要是下沉,其实下沉的时间复杂度非常直观,每次下沉,结点的高度-1,一棵树的高度是lgN,所以从0号位下沉到最后一位(最坏情况),也只是lgN。
后面我们在下沉排序里面对每个元素其实都进行了下沉,所以总的来说是NlgN。
空间复杂度:
我们在该算法中,并没有用到临时变量或者新建的数组,所以不需要额外的空间。复杂度是常数。
稳定性:
不稳定