堆排序
光听名字,你可能会以为这是用堆来排序的算法,实则不然。堆排序是利用堆的思想来进行排序的,那么是如何来进行排序的?
现在随便给出一组数组:
c
int arr[8]={12,20,23,2,4,14,13,21};
我们之前实现堆的时候,知道堆分两种,一种是大堆,一种是小堆。如果我们要把这个数组arr建成一个大堆,该如何来进行排序?

建堆的算法我们分为两种,一种是向下排序算法,一种是向上排序算法,我们来实现一下。
向下排序算法
思路
向下排序算法中,因为我们要保证整个二叉树结构是一个大堆结构,所以我们先从最后一棵子树开始调整。

最后一个结点的父结点肯定就是最后一棵子树的根结点,比如这棵树,最后一个结点的位置是7,(7-1)/2=3,3就是最后一棵子树的根结点。
向下调整的函数Adjustdown我们之前写过,这里就直接给出来了。
c
void Adjustdown(int* arr, int parent, int n)
{
int child = parent * 2 + 1;//左孩子
while (child < n)//防止孩子结点越界
{
if (child+1 < n && arr[child] < arr[child + 1])
child++;
if (arr[parent] < arr[child])//调整为大堆结构
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}

把最后一棵子树调整为大堆结构后,我们parent - - 就可以找到上一个子树的根结点,我们再接着调整这棵树为一个大堆结构:

调整完成后,parent再 - - 找上一棵子树,再继续调整:

调整完成后再parent - -,此时会发现parent=0,说明是最后一次调整了,调整的是整个树的结构:

调整完成后就会变成一个大堆:

代码实现
理清思路后,我们就可以来实现一下代码了。
我们要从最后一棵子树开始,遍历调整所有子树的结构,所以我们写个for循环。
假设数组大小为n,最后一个结点的位置就是n-1,根据堆的性质,我们就可以找到它父结点parent的位置了:(n-1-1)/2,每次调整子树结构完成后,parent都要 - -找上一棵子树,直到parent=0时,说明是调整整棵树,是最后一次调整,所以循环条件我们给一个parent大于等于=0,这里我们用 i 来表示父结点,用来和Adjustdown的parent区分开来:

我们来测试一下:


写成二叉树形式是这样的:

说明成功被排为了大堆结构,代码功能没问题。
向上排序算法
思路
向上排序算法的思路非常简单,可以类比为入堆操作,遍历一遍数组一个个入堆,然后每次入堆都调整保证是一个大堆结构。每次调整都调用函数Adjustup,我们之前实现过,这里就直接给出来了。
c
void Adjustup(int* arr, int child)
{
int parent = (child - 1) / 2;
while (parent>=0)
{
if (arr[parent] < arr[child])//调整为大堆
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}


代码实现

我们简单测试一下:


写成二叉树结构是这样的:

说明该代码功能没问题。
向下排序算法和向上排序算法的时间复杂度
那么这两个算法哪个更好呢?

观察一下,我们发现这两个算法都是一个循环,每次循环调用这个函数(时间复杂度为O(logn)),那这两个算法的时间复杂度都是O(nlogn)吗?下面我们用满二叉树来分析一下这两个算法。
向下排序算法的时间复杂度
这里有一个k层的满二叉树,向下排序算法是从第k-1层开始往前遍历子树的根结点的。

第k-1层有2k-2个结点,向下调整要移动1次。
第k-2层有2k-3个结点,向下调整要移动2次。
第k-3层有2k-4个结点,向下调整要移动3次。
...
第2层有21个结点,向下调整要移动k-2次。
第1层有20个结点,向下调整要移动k-1次。
可知向下排序移动的总步数为:每层结点个数*向下调整移动次数。
设T(k)为向下排序算法的总步数,则有:

两边同时乘2,可以得到:

2式减1式错位相减可以得到:

1可以写成20,用等比数列求和公式可以整理成:

根据二叉树的性质:2k-1=n,k=log2n+1,
k是层数,用n代换掉k可以得到: O(n)=n-log2n-1
所以向下调整算法的时间复杂度为O(n)。
向上排序算法的时间复杂度

每次调整都到第一层为止。
第一层有20个结点,向上调整0层。
第二层有21个结点,向上调整1层。
第三层有22个结点,向上调整2层。
...
第k-1层有2k-2个结点,向上调整k-2层。
第k层有2k-1个结点,向上调整k-1层。
易知向上排序算法移动的总步数:每层结点数*移动层数
设移动总步数为T(k),则有:

两边同时乘2可以得到:

2式减1式错位相减得:

-2和-2k写入括号里整理可得:

等比数列求和整理得:

根据二叉树的性质:2k-1=n,k=log2n+1,把k代换成n可得:

所以向上排序算法的时间复杂度为O(nlogn)
向下排序算法优于向上排序算法。
排序
建完大堆后,我们就要排序了,怎么排序呢?
我们知道,堆顶是最值,所以我们建的大堆堆顶就是最大值,我们把堆顶和堆尾元素交换一下,然后数组大小size - -,剩下的元素再调整为一个大堆,可以类比为出堆操作,一直循环调整,直到size=0为止。


我们简单测试一下:


可以看到成功被排为升序数组,说明代码逻辑没问题。
现在我们可以知道,排成升序数组要建大堆,降序数组就建小堆。
堆排序的时间复杂度为O(nlogn),算是很高效率的排序算法了,我们之前学过的冒泡排序(Bubble Sort)的时间复杂度都为O(n2)。