堆是具有下列性质的二叉树:每个结点的值都大于或等于其zuo'左右结点的值称为大顶锥;或者每个结点的值都小于或等于其左右孩子的值,称为小顶锥。
堆排序就是利用堆进行排序的方法。他的基本思想是,将待排序的序列构造成一个大顶锥。此时整个序列的最大值就是堆顶的结点。将他移走(其实就是将与其堆数组的末尾元素交换,此时末尾元素的值就是最大值),然后将剩下的n - 1个序列的值chon重新构造成大顶锥,这样就会得到序列中的次大值。如此反复执行,便能得到一个有序序列了。
代码如下:
void swap(SqList *L,int i,int j) //交换数据
{
int tmp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = tmp;
}
void HeapAdjust(SqList *L,int s,int m) //使序列成为一个大顶锥
{
int tmp,j;
tmp = L->r[s];
for(j = 2*s;j <= m;j*=2)
{
if(j < m && L->r[j] < L->r[j + 1])
j++;
if(tmp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = tmp;
}
void HeapSort(SqList *L) //堆排序
{
int i;
for(i = L->length/2;i > 0;i--)
HeapAdjust(L,i,L->length);
for(i = L->length;i > 1;i--)
{
swap(L,1,i);
HeapAdjust(L,1,i - 1);
}
}
先看堆排序函数:
void HeapSort(SqList *L)
{
int i;
for(i = L->length/2;i > 0;i--) //把L中的r构建成一个大顶锥
HeapAdjust(L,i,L->length);
for(i = L->length;i > 1;i--)
{
swap(L,1,i); //将锥顶记录和当前未经排序子序列的最后一个记录交换
HeapAdjust(L,1,i - 1); //将剩下的重新调整为大顶锥
}
}
从代码中可以看出,整个排序过程中分为两个for循环。第一个for循环要完成的就是将现在的待排序序列构成一个大顶锥。第二个循环要完成的就是逐步将每个最大值的根结点与末尾元素交换,并且在调整成为大顶锥。
假设要排序的序列为{50,10,90,30,70,40,80,60,90},则L.length = 9,第一个for循环,i 从[ 9/2] = 4 开始,为什么从4开始呢,因为他们都是有孩子的结点。
再看堆调整函数:
void HeapAdjust(SqList *L,int s,int m)
{
int tmp,j;
tmp = L->r[s];
for(j = 2*s;j <= m;j*=2) //沿关键字较大的孩子结点向下筛选
{
if(j < m && L->r[j] < L->r[j + 1])
j++; //j为关键字中较大的记录的下标
if(tmp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = tmp; //插入
}
1.函数被第一次调用时,s = 4,m = 9,传入的SqList 参数的值为length = 9,r[10] = {0,50,10,90,30,70,40,80,60,20}.
2.第4行,将r[s] = r[4] = 30赋给tmp。
3.第5~13行,循环遍历其结点的孩子,由于二叉树的性质,j 由2*s开始,以j*=2递增(当前结点序号是s,其左孩子序号一定是2s).
4.第7~8行,此时 j = 8,j < m 说明他不是最后一个结点,如果r[j] < r[j + 1],则说明左孩子小于右孩子,j++则说明现在的j是左右孩子最大值的下标。
5.第9~10行,tmp = 30,r[j] = 60,不满足条件。
6.第11~12行,将60赋给r[4],并令s = j = 8。也就是说,当前算出,以30为根节点的子二叉树,当前最大值是60,在第8个位置。
7.再循环因为j = 2*j = 16,跳出循环。
8.第14行,将tmp = 30赋值给r[s] = r[8],完成30与60的交换工作,本次调用完成,再次调用此函数仍以这样的步骤。
接下来堆排序函数的第6~11行就是正式的排序过程,就是将堆顶与堆尾不断进行交换的过程。
复杂度:
时间复杂度O为nlogn。性能要好于冒泡,简单排序,直接插入的n*n的时间复杂度。空间复杂度上由于记录的比较和交换是跳跃式进行,因此堆排序是一种不稳定的排序。