1.堆
堆是具有以下性质的完全二叉树:每个节点都大于或等于其左右孩子节点的值,称之为大顶堆;反之每个节点都小于或等于其左右孩子的值,称之为小顶堆。
对于堆的实现,我们可以用数组来表示元素的位置关系,如果一个具有12个节点的堆(小顶堆)如下图所示:
那我们使用数组arr[0..11]的形式表示如下:
数中常见的函数形式如下所示:
root = 0
leftChild(i) = 2*i + 1
rightChild(i) = 2*i +2
parent(i) = (i-1)/2
(如何证明不在本文做过多的描述,有兴趣的可以自己推算。提示:某节点的root层 到 该节点parent 层 的元素个数是等比数列)
2. 两个关键函数
在讲解 堆排序前,先看看两个关键的函数。堆排序实现的原理,就是这两个函数实现的原理,我们先将该两个函数名定义为siftup 和 siftdown
siftup
当x[0...n-1] 是堆时即heap(0,n-1),在x[n]位置放置一个元素,可能不满足x[0...n]具有堆的性质,即heap(0,n)不成立。那么siftup的作用就是通过调整使heap(0,n)成立。 该函数名就表明了实现的策列:将添加的新元素尽可能地向上筛选,向上筛选是通过和父节点交换位置来实现。 下图(从左到右)演示了添加 元素13 在堆中 向上交换的过程(在添加新元素13之前,该结构满足堆的性质)
注意点:在上图中,除了带圈节点(新增节点或者新增节点被交换后的位置节点,后面用节点i表示) 和其父节点 可能不满足堆性质外,但其他地方都满足 堆的性质。那添加一个新节点siftup调整堆的伪代码就是 : 先for循环{
判断节点i是否是root节点,如果是,则所有节点满足堆性质,退出循环;
如果不是root 节点,找到节点i的父节点p,比较x[i]和x[p],如果x[i] >= x[p],则说明满足了堆的性质,退出循环;
如果x[i] < x[p],则交换x[i] 和x[p],交换后,i的位置索引 就是 p对应的位置索引,继续下一次for判断
}
(更详细的推导过程,包括为什么 仅仅 i节点和其父节点可能不满足堆的性质,可以见后面的参考文献)
siftdown
如果x[1...n]满足堆的性质即heap(1,n),然后放一个新元素到x[0]中,这种情况就可以通过siftdown来调整,使其满足heap(0,n).该函数的实现策列:通过向下调整,直到它没有子节点或者小于等于子节点。下图演示了 节点18 添加到x[0]后 通过siftdown调整使之满足heap(0,n)
注意点: 除了带圈 节点(新添加的节点或者新增节点被交换后的位置节点) 和其子节点不满足heap的性质外,其他的节点都是符合heap的性质的。那新增一个元素到根节点通过siftdown 调整堆的伪代码为:
for(int i = 0;i<arr.length;i++){
int c = 2*i+1 ;//左子节点
if(c >= arr.length) //说明没有子节点
break;
if(c+1 < arr.length){ //如果右子节点也存在
if(arr[c+1] < arr[c]{ //如果右子节点比左子节点小,则说明左右子节点中较小的节点是右节点(后面称较小节点为S节点 )
c++;
}
}
if(arr[i] > arr[c]){ //节点 比 S节点 还要小,则交换 节点 和 S节点
int temp= arr[c]
arr[c] = arr[i]
arr[i] = temp
i = c; //i节点为 被交换后的位置节点
}else{
break;
}
}
3. 堆排序实现
理解了上面的两个重要函数后,再看看堆排序(用数组来表示堆,降序排序)的实现过程:
- 将数组第一个元素可以看成是一个排好序的堆,从第二个元素开始可以认为是向尾部添加新元素,使用siftup方法调整,使之heap(0,1)成立;以此类推直到heap(0,n-1)成立,这样小堆顶就建好了。
- heap(0,n-1) 建立好后,元素并不是完全排序的,但根据小顶堆的性质 我们知道根节点(0 节点)是最小的,通过交换0节点 和 n-1 节点,这样最小元素就放到了n-1的索引位置上,同时heap(1,n-2)满足堆,在根节点替被替换新元素后,通过siftdown,使heap(0,n-2)成立;然后再交换0节点和n-2节点... 循环该过程,最后仅仅剩下2个元素(根据堆的性质,第二个元素和第一个元素是排序好的,不用再交换位置,siftdown了)
java代码参考:
package arithmetic;
/**
* Created by ldxPC on 2018/10/23.
*/
public class HeapSort {
private int getLeftChildIndex(int p){
return 2*p+1;
}
private int getRightChildIndex(int p){
return 2*p+2;
}
private int getParentIndex(int c){
return (c-1)/2;
}
public void siftUp(int[] date){
for(int i = 1 ;i<date.length;i++){
int siftIndex = i;
while(siftIndex>0 && date[getParentIndex(siftIndex)] > date[siftIndex]){
//swap
int tempValue = date[siftIndex];
date[siftIndex] = date[getParentIndex(siftIndex)];
date[getParentIndex(siftIndex)]= tempValue;
siftIndex = getParentIndex(siftIndex);
}
}
}
/**
*
* @param date
* @param length 表示date的前多少个元素需要进行heap调整
*/
public void siftDown(int[] date,int length){
for(int i = 0;i<length;i++){
int siftIndex = i;
while(getLeftChildIndex(siftIndex) < length){
int lessChildIndex = getLeftChildIndex(siftIndex);
if(getRightChildIndex(siftIndex) < length){
if(date[getRightChildIndex(siftIndex)] < date[getLeftChildIndex(siftIndex)]){ //更小的child 是右节点,将lessChild+1
lessChildIndex++ ;
}
}
if(date[lessChildIndex] < date[siftIndex]){ //child节点比changeIndex节点小
int tempValue = date[siftIndex];
date[siftIndex] = date[lessChildIndex];
date[lessChildIndex] = tempValue;
siftIndex = lessChildIndex;
}else{
break;
}
}
}
}
public static void main(String[] args){
int[] date = {4,3,6,1,79,4,7,32,1,33,14,64,68,35};
System.out.println("数组调整前:"+printDate(date));
HeapSort heapSort = new HeapSort();
heapSort.siftUp(date); //构建堆
for(int n = date.length-1;n>1;n--){
int lessValue = date[0];
date[0] = date[n];
date[n] = lessValue;
heapSort.siftDown(date,n-1); //对前n-1个数进行调整
}
System.out.println("数组调整后:"+printDate(date));
}
public static String printDate(int[] date){
StringBuilder builder = new StringBuilder();
for(int i = 0;i<date.length;i++){
if(i >0){
builder.append(",").append(date[i]);
}else{
builder.append(date[i]);
}
}
return builder.toString();
}
}
打印结果:
4.总结
之前学习堆排序的过程中,在网上看了一些blog,不过 一上来直接讲解了算法的流程(没有讲解本文中的两个重要的函数),看的是一知半解。 后来看到编程珠玑 第二版 第11章 对 堆排序的表述后,恍然大悟。 所以在我们学习算法的时候,有的时候需要对算法的本质或者说对算法的过程抽象 需要理解。 在理解本质后的基础上再学习该算法包括对算法的运用,就会事半功倍! 如果需要了解更详细的算法思路,可以自行参考 编程珠玑 第二版 第11章
5.参考文献
编程珠玑 第二版 第11章节