堆排序
堆排序算法是 Floyd 和 Williams 在1964 年共同发明的,同时,他们发明了“堆”这样的数据结构。
引子:叠罗汉
定义:堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子节点的值,称为大项堆;或者每个结点的值都小于或等于其左右孩子节点的值,成为小项堆。
这里需要注意的是:根节点一定是堆中所有节点最大(小)者。
堆排序思路:将线性表排列成大顶(或者小顶)堆,然后将堆顶元素(当前最大值)与最后一个元素交换,然后将去除最后元素的其它元素重新调整为大顶堆,重复之前的操作,直到最后一个元素。
代码如下:
/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义 */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
/* 通俗化理解:将一串数字进行从中间往前的最小树根排序过程,下例中,将 下标为 length/2 至 1 的元素进行最小树倒着排序 */
/*
50 50
/ \ / \
10 90 10 90
/ \ / \ -> / \ / \ ->
30 70 40 80 60 70 40 80
/ \ / \
60 20 30 20 (第一次排序,L = length/2 ,在本例中,就是 第四个节点,即 60,调整排序)
50 50
/ \ / \
10 90 70 90
-> / \ / \ -> / \ / \
60 70 40 80 60 10 40 80
/ \ / \
30 20(第二次排序,对 90、40、80排序) 30 20(第三次排序,i--,对以70为根的二叉树调整顺序)
90
/ \
70 80
-> / \ / \
60 10 40 50
/ \
30 20(第四次排序,对以50为根的二叉树调整顺序,50与90交换,50再与80交换,最后完成)
*/
void HeapAdjust( SqList *L, int s, int m)
{
int temp, j;
temp = L->r[s];
for( j = 2*s; j <= m; j*=2 ) //j=j*2的递增,是为了判断做过替换的j是否有孩子,如果有再作调整,没有则完成。
{
/* 沿关键字较大的孩子结点向下筛选 */
if( j < m && L->r[j] < L->r[j+1] )
++j; /* j为关键字中较大的记录的下标 */
if( temp >= L->r[j] )
break; /* 没有破坏原堆的性质,终止for循环 */
L->r[s] = L->r[j];
s = j;
}
L->r[s] = temp;
}
/* 我们所谓的将待排序的序列构建成一个大顶堆,其实就是从下往上、从右到左,将每个非终端节点(非叶节点)*/
/* 当作根节点,将其和其子树调整为大顶堆。 */
/* 对顺序表L进行堆排序 */
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);
/* 将L->r[1...i-1]重新调整为大顶堆 */
HeapAdjust(L, 1, i-1);
}
}
复杂度分析
- 它的运行时间主要是消耗在初始构建堆和在重建堆的反复筛选上。
- 在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和如有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
- 在正式排序时,第 i 次去堆顶记录重建堆需要用O(long i )的时间(完全二叉树的某个结点到根节点的距离为[log2 i ]+1),并且需要取 n-1 次堆顶记录, 因此, 重建堆的时间复杂度为O(n log n)。
- 所以,总体来说,堆排序的时间复杂度为O(n log n)。
- 由于堆排序对原始记录的排序状态并不敏感,因此无论是最好、最坏和平均时间复杂度均一样。这在性能上显然要远远好于冒泡、简单选择、直接插入的o(n^2)的时间复杂度了。
- 空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的交换与比较是跳跃式进行,因此堆排序也是一种不稳定的排序方法。