在谈堆排序之前我们需要先了解一些东西。
1.什么是堆?
定义:堆是满足下列性质的数列{r(1),r(2),r(3),r(4)....r(n)}
小顶堆:r(i)<r(2i)且r(i)<r(2i+1)
大顶堆:r(i)>r(2i)且r(i)>r(2i+1)
堆的定义是不是有点眼熟?
所谓的 r(i)其实就是二叉树中某一双亲节点i为该节点在数列中的索引值(索引从1开始),r(2i)和r(2i+1)就是该双亲结点对应的左右孩子的值(这里虽然说的是数列,你就当成数组记忆就可以)
作者观点:在我看来,所谓的堆排序就是将一个无序数组想象成一个二叉树,通过对各个节点位置的不断调整,将这个二叉树调整为大顶堆或者小顶堆,的过程。(当然这只是核心过程,详细过程下面会详细讲解)。
2.堆排序是如何实现的呢?
首先我们要清晰的认识到,所谓堆排序的存储方式不是树形的存储,而是线性的存储(我们这里对数组中的无需元素进行堆排序)。
在这里,我们得到了一个待排序的数组
//一般将数组中的零号元素作为监视哨
private int[] arr={-1,4,2,3,1,5,9,8,7,0,6};
可以看出这个数组有是一个元素,零号位置我们放的是监视哨,也就是说我们的实际存储是从一号位置开始的。
然后我们要怎么做呢?将它想象成一棵二叉树
上面是我想象出来的二叉树,但是我们发现它既不是顶堆也不是小顶堆,所以我们需要对它进行调整。
那么如何调整呢?当然是从后往前从小到大的调整了。
我们现在要将它调整为一个小顶堆(也就是从小到大排序)
第一次调整
第二次调整
第三次该调整
发现以满足小顶堆定义!
第四次该调整
我们首先找到2的左右子树中的最小值min,然后比较min与2的大小,这是返现2<0所以将0填到2原本的位置上,然后还需要判断2能否填在0原本的位置上,我们找到了0原本位置的左右子树,比较得出左小指为1返现1<2所以将1放在原本零的位置上,接下来我们仍然需要判断2能否坐在1原本的位置上,我们发现1原本的位置没有孩子节点,所以就将2放在1原本的位置上。
第五次调整
我们发现经过前四次的调整我们的树的左右子树已经变成了小顶堆了!!!
那么第五次调整就是对根节点的调整,要使整棵树成为小顶堆,这也使我们后面惊醒堆排序的核心思想,请大家一定注意!
我们首先找到4所在位置的左右孩子,并得到值较小的孩子节点的位置,如果该值小于4则替换,我们发现0<4所以替换,然后判断4能否坐在0原本的位置上,我们依然看0所在位置的左右节点的最小值与4的关系,我们发现1<4所以交换,然后再判断4能否坐在1原本的位置上,返现2<4替换,然后再判断4能否坐在2原本的位置上发现2没有左右孩子,则将4放置再2原本的位置,至此,我们的树已经是一个小顶堆了(大顶和这个类似)
3.虽然我们的数组被调整成了一个满足小顶堆定义的数组,那么如何实现排序呢?
第一步,弹出堆顶元素
第二步,将数组尾部元素放在堆顶,并清除该堆尾元素
第三部,将堆再次调整成小顶堆
重复上述步骤。。。
我来给大家演示一次弹出,后面的大家就都明白了!
所谓的调整其实就是上面的“第五次调整”,和那个完全相同,调整后就可以得到下面这张图:
重复上面步骤即可!
下面我们来用代码实现
调整堆为小顶堆:
/**
* 堆的调整函数,什么是堆的调整函数呢?就是说我们的目标堆除了某一点的左右子树已经是大顶堆或者小顶堆了
* ,但是该点的位置不对,这是后就需要通过该函数调整这个堆,使其成为大顶堆或者小顶堆
*
* 什么是大顶堆:ri>r(2*i)且ri>r(2*i+1)
*
* 其实在我看来就是二叉树的双亲结点大于或者小于左右孩子,当然,我现在好像认识到,堆排序是使用树的思想堆线性表进行排序
*
* 什么是小顶堆:ri<r(2*i)且ri>r(2*i+1)
* @param
*/
public void heap_adjust(int s,int m) {//s为出入的双亲节点再数组中的索引,m为数组中剩余元素个数
//s是需要堆调整的那个点
int t=arr[s];
for(int i=s*2;i<m;i=i*2) {
if(i<m-1 && arr[i]>arr[i+1]) {
i++;//这里就是用来保存数组中较小的点的下标(也就是选择用左孩子还是有孩子和双亲节点比大小)
}
if(t<arr[i]) {//如果两个孩子节点中的最小值都大于双亲结点的值,那证明以该双亲结点为顶点的堆已经使小顶堆了
break;
}
arr[s]=arr[i];//这就是说左右孩子中有比双亲节点小的值,那就需要用该值放在双亲节点的位置上
s=i;//表明下一个需要判断的点使上一个选择的点
}
arr[s]=t;//可以放下了
}
初始化堆为小顶堆:
//创建堆,也就是把待排序的树组处理成为一个大顶堆或者小顶堆
public void createHeap() {
for(int i=arr.length/2;i>=1;i--) {
heap_adjust(i,arr.length);
}
}
进行堆排序:
public void Hsort() {
int flag=0;
createHeap();//先创建二叉树使其为小顶堆
//排序开始,每次弹出堆顶元素,将堆尾元素填到堆顶位置,然后删除堆尾元素
for(int i=0;i<arr.length-1;i++) {
System.out.println(arr[1]);//输出堆顶
arr[1]=arr[arr.length-1-flag];//将堆尾元素填到堆顶位置
arr[arr.length-1-flag]=65535;//我是通过设置一个很大的值来表示删除堆尾元素的,当然我也在控制数组长度,也可以达到删除的目的
heap_adjust(1,arr.length-flag);//调整
flag++;
}
}
测试结果:
0
1
2
3
4
5
6
7
8
9
今天就说到这里了,谢谢大家的观赏!