欢迎大家访问我的微博:http://weibo.com/u/2887401030
堆是具有下列性质的完全二叉树:每个结点的值都大于等于其走有孩子节点的值,称为大顶堆;或者每个结点的值都小于等于其走有孩子节点的值,称为小顶堆。
再看堆排序。堆排序就是利用堆(假设是大顶堆)进行排序的方法。它的基本思路是将待排序的序列造成大顶堆。此时,整个序列的最大值就是堆顶的根节点。将他移走(其实就是将其与堆数组的末尾元素交换,此时末尾就是最大值),然后将剩余的n-1序列重新构造成一个堆,这样就会得到n个元素的次小值,如此反复执行,便能得到一个有序序列。
上面这段话,基本把堆排序的精华都讲出来了。现在我们来看一个构造堆的例子。假设int a[] = {50,10,90,30,70,40,100,60,20};我们要将这个数组中的数据造成大顶堆。将这个数组看成完全二叉树,按层序遍历,如下图:
这里的长度为9,从[9/2]=4开始调整,一直到1。原因是30是最后一个有子结点的结点。
先看30的子结点,左结点是60,右节点是20,60>20,就将父节点30与60比较,30<60。将30与60交换。(注意这里如果是右结点的值大于左结点,就拿父节点的值与右结点的值比较,父节点的值大于子节点就不换,小就换)。
交换后:
注意此时交换后的30已没有子节点。
再看90的子节点,同样100>40,父节点值与右节点值比较。90<100,交换两个的值。
交换后:
再看10的子节点,70>60,父节点值与右节点值比较。10<70,交换两个的值。
交换后:
看最后一个节点50的子节点,100>70,父节点值与右节点值比较。50<100,交换两个的值。
交换后:
注意此时的交换后的50还有子节点,再次比较子节点,90>40,父节点值与右节点值比较。50<90,交换两个的值。
交换后:
ok,观察此时的图,根结点的值最大,每个根结点的值都大于子结点的值,构造大顶堆成功,再以层序遍历,装到数组,int a[]={100,70,90,60,10,40,50,30,20};
这就是构造大顶堆的方法。用代码的形式表现出来:
/*
假设只有i没有调整好
i : 要调整的下标
m : 要调整的最大的下标
*/
void HeapAdjust(int *a,int i,int m)
{
int j,temp;
if(a == NULL || m <= 0)
{
return;
}
temp = a[i];
//假设i是0,对应50
//注意j<=m
for(j = 2*i+1;j <= m;j = j*2+1 )//注意左子节点是下标*2+1
{
if(j < m && a[j] < a[j+1])
j++;//如果左结点小于右结点,移动到右结点
if(temp >= a[j])
break;//没必要调整
a[i] = a[j];
i = j;//要调整的序列变化,对应90
}
a[i] = temp;//最后i的序列对应的就是temp保留的值
}
注意的是上面的代码是按照数组的下标来的,与之前的图上左边的小数组不同,小一个1。也就是说,每个根节点的子节点的小标是根结点的小标*2+1(按照之前的图的话,应该是根节点小标*2),这点大家要注意。
另外,for(j = 2*i+1;j <= m;j = j*2+1 ) 之所以循环这么写,也是因为上面所讲的,交换后如果发现还有子节点,得继续交换,子节点再到自己的子节点;j = j*2+1,下标的变换。
在看真正的堆排序:
void HeapSort(int *a,int len)
{
int i;
if(a == NULL || len <= 0)
{
return;
}
//开始的数组建立大顶堆
for(i=len/2;i>0;i--)
{
//在这里是3 -- 0
HeapAdjust(a,i-1,len-1);
}
for(i=len-1;i>0;i--)
{
//进行排序
swap(a,0,i);//将第一个与最后一个交换,最后一个最大
//重新建立大顶堆
HeapAdjust(a,0,i-1);//不包括最后一个已经出来的最大的数
}
}
看这里:
//开始的数组建立大顶堆
for(i=len/2;i>0;i--)
{
//在这里是3 -- 0
HeapAdjust(a,i-1,len-1);
}
就像我之前讲的,从[len/2]开始,先将无序的数组变成大顶堆。到此时数组已经变成大顶堆后的数组。
for(i=len-1;i>0;i--)
{
//进行排序
swap(a,0,i);//将第一个与最后一个交换,最后一个最大
//重新建立大顶堆
HeapAdjust(a,0,i-1);//不包括最后一个已经出来的最大的数
}
再看这个 swap(a,0,i);,将数组中的第一个数与最后一个数交换,swap就是一个简单的交换数组内数据的函数(后面会给代码)。因为现在的数组的已经内构造成大顶堆,根据定义,第一个数的值最大。所以,此时就是把最大的数放最后了。
再看这个HeapAdjust(a,0,i-1); 就是之前构造堆的函数,注意这里i-1。HeapAdjust这个函数最后一个参数表示的是调整最大的下标,这也就是说现在是不包括已经换了之后的数。例如:100已经到了数组最后一个,此时调用的是HeapAdjust(a,0,7),在这之后的90又到了堆顶,此时调用的是HeapAdjust(a,0,6),…….以此类推。
到这里,我们再回头看我们对堆排序的定义:
堆排序就是利用堆(假设是大顶堆)进行排序的方法。它的基本思路是将待排序的序列造成大顶堆。此时,整个序列的最大值就是堆顶的根节点。将他移走(其实就是将其与堆数组的末尾元素交换,此时末尾就是最大值),然后将剩余的n-1序列重新构造成一个堆,这样就会得到n个元素的次小值,如此反复执行,便能得到一个有序序列。
发现没有,我们完全就是按这样的套路在走,一分不差。如果你此时对堆排序的定义非常清楚,说明你已经掌握了它。如果你还是不能完全明白,那你可能要在思考几下。
总的来说,堆排序,就是利用了完全二叉树来进行的排序。整体的思路没有那么难。大家可以试着自己写代码。向大家提个建议,试着将我的代码改一下,因为我是按照数组的标号(从0开始)来写的,你们可以试着从1开始,数组的第一位试着先空掉,真正的数据从下标1开始,就像我之前展示的图片一样。这样的话,会有真正的体会。另外,我把完整的代码放下面,给大家参考一下:
完整代码:
#include<stdio.h>
#include<stdlib.h>
void swap(int *a,int i,int j)
{
int tmp;
if(a == NULL || i < 0 || j < 0)
{
return;
}
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
void printAll(int *a,int len)
{
int i;
if(a == NULL || len < 0)
{
return;
}
for(i=0;i<len;i++)
{
printf("%d ",a[i]);
if((i+1)%10 == 0)
printf("\n");
}
}
/*
50
10 90
30 70 40 100
60 20
*/
/*
i : 要调整的下标
m : 要调整的最大的下标
*/
void HeapAdjust(int *a,int i,int m)
{
int j,temp;
if(a == NULL || m <= 0)
{
return;
}
temp = a[i];
//假设i是0,对应50
//注意j<=m
for(j = 2*i+1;j <= m;j = j*2+1 )//注意左子节点是下标*2+1
{
if(j < m && a[j] < a[j+1])
j++;//如果左结点小于右结点,移动到右结点
if(temp >= a[j])
break;//没必要调整
a[i] = a[j];
i = j;//要调整的序列变化,对应90
}
a[i] = temp;//最后i的序列对应的就是temp保留的值
}
void HeapSort(int *a,int len)
{
int i;
if(a == NULL || len <= 0)
{
return;
}
//开始的数组建立大顶堆
for(i=len/2;i>0;i--)
{
//在这里是3 -- 0
HeapAdjust(a,i-1,len-1);
}
for(i=len-1;i>0;i--)
{
//进行排序
swap(a,0,i);//将第一个与最后一个交换,最后一个最大
//重新建立大顶堆
HeapAdjust(a,0,i-1);//不包括最后一个已经出来的最大的数
}
}
int main()
{
int a[] = {50,10,90,30,70,40,100,60,20};
int len = sizeof(a)/sizeof(*a);
printf("调整前:\n");
printAll(a,len);
HeapSort(a,len);
printf("\n调整后:\n");
printAll(a,len);
return 0;
}
原文地址:http://blog.csdn.net/tomjohnson/article/details/44757681