上面两篇文章中,我们可以了解到大堆的堆顶存放的是堆中最大的元素,小堆的堆顶存放的是堆中最小的元素。利用堆的这个属性就可以实现排序的目的。
如果排升序,那么使用大堆选取堆中最大的数据放置在堆顶,将堆顶元素与堆中最后一个元素进行交换,将最大的元素排除在外进行向下调整,循环遍历整个堆就可以得到原数据的升序排列。如果排降序,同样的道理,先选取小堆的堆顶,得到堆中最小的元素,将其与堆中最后一个元素进行交换位置,将最后一个元素排除在外,再进行向下调整,遍历整个堆,就会得到原数据的升序排列。总结一下:由于堆的特殊属性,排升序时使用大堆,排降序时使用小堆。
在排序之前我要声明的是,不是要建立一个堆这样的数据结构来进行排序。而是进行模拟堆的属性进行排序。期望是有一组存放在数组中的数据,通过堆排序函数实现排序的目的。由于升序降序的核心思想是一致的,这篇文章选取模拟建大堆的过程实现数据的升序排序。下面正文开始!
1、准备工作
先给出本次建堆的需求:将一组存放在数组当中的数据进行升序排序,期望输出原数据的升序排列。
cpp
#include <stdio.h>
int main()
{
int arr[] = {5,3,7,8,2,4,1,6,0,1,-1,-100,50 };
int sz = sizeof(arr) / sizeof(arr[0]);
HeapSort(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
2、堆排序
在堆实现这篇文章中我提到堆在插入后要进行向上调整已保证堆的属性:大堆堆顶为最大的数据,小堆堆顶为最小的数据。现在我们假设数组的第一个元素就是堆顶,需要做的是将从第二个元素到最后一个元素进行向上调整以获得堆顶元素,升序要建大堆,我们这里就是为了获得最大的元素。
2.1、模拟向上调整建堆
这里将向上调整封装成一个函数,方便后面的调用。
cpp
void Swap(int* ps1, int* ps2)
{
int tmp = *ps1;
*ps1 = *ps2;
*ps2 = tmp;
}
void AdjustUp(int* arr,int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
这里的逻辑和上文中是一致的,当子结点的数据小于父结点的数据时就进行交换,当子结点的下标小于等于0时就跳出循环。这里我们就可以将数据建成一个大堆并可以获取原数据中最大的元素。
2.2、 模拟向下调整排序
这里将向下调整封装成一个函数:
cpp
void AdjustDown(int* arr, int size, int parent)
{
assert(arr);
int child = parent * 2 + 1;//默认左边的子结点最大
while (child < size)
{
if (child + 1 < size && arr[child + 1] > arr[child])
{
child++;//右边子结点存在且满足条件进行调整
}
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
如果排升序,那么使用大堆选取堆中最大的数据放置在堆顶,将堆顶元素与堆中最后一个元素进行交换,将最大的元素排除在外进行向下调整,循环遍历整个堆就可以得到原数据的升序排列。
cpp
void HeapSort(int* arr, int sz)
{
assert(arr);
int i = 0;
for (i = 1; i < sz; i++)
{
AdjustUp(arr, i);
}
int end = sz - 1;
for (i = end; i > 0; i--)
{
Swap(&arr[0], &arr[i]);//将堆顶与末尾进行交换
AdjustDown(arr, i,0);//这里注意堆的尾结点位置为i
}
}
那么现在所有的工作已经准备就绪,运行程序可以得到结果:
3、时间复杂度
3.1、向上调整
向上调整是从堆的最后一个元素进行的,假设最后一层元素的高度为h,那么这层结点最坏的情况下需要向上交换h-1次(因为它的上面还有h-1层)。假设整个向上调整的次数为T(N),那么T(N)=每一层结点各自的调整次数之和。
这里使用错位相减法求T(N):
所以带入h的值,有:
3.2、向下调整
与向上调整不同的是,向下调整从倒数第二行开始,这就注定着它的时间复杂度要优于向上调整。那么整个向下调整的次数T(N)为:
4、堆排序优化
根据上面复杂度的计算,我们发现向下调整要优于向上调整,实际上在建堆的过程中可以完全使用向下调整。思路如下:
找到最后一个结点的父节点,进行子树的向下调整得到子树的最大值。依次按照图中的顺序遍历每个子树就可以得到最大值。
代码实现如下:
cpp
void HeapSort(int* arr, int sz)
{
assert(arr);
int i = 0;
int end = sz - 1;
for (i = (end - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, sz, i);//父结点的下标为i
}
end = sz - 1;
for (i = end; i > 0; i--)
{
Swap(&arr[0], &arr[i]);//将堆顶与末尾进行交换
AdjustDown(arr, i,0);//这里注意堆的尾结点位置为i
}
}
上述代码在建堆过程中可以适当优化代码。但整体代码的时间复杂度仍为O(N*logN)