本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1. 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
把数据一个个放入堆中,每插入一个都潜在的维持了堆的结构(大堆或小堆)
然后取堆顶数据(最大或最小)放到数组中,再把堆顶数据删除掉,删除之后向下调整,并将目前最大(或最小)的数据挪到堆顶,重复这个步骤直到堆为空。
下面的动图是用这个思路来排序的,但它是从后往前放到数组里的,这个跟我们下面的参考代码有些不同。
参考代码
void HeapSort(int* a, int size)
{
HP hp;
HeapInit(&hp);
for (int i = 0; i < size; i++)
{
HeapPush(&hp, a[i]);
}
int j = 0;
while (!HeapEmpty(&hp))
{
a[j] = HeapTop(&hp);
HeapPop(&hp);
j++;
}
HeapDestroy(&hp);
}
int main()
{
int a[] = { 5,10,6,1,-1,2,7,0 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
1.1堆排序的优点和不足
冒泡排序和堆排序的时间复杂度比较:
冒泡排序:(N-1)+(N-2)+·····+2+1:arrow_right:时间复杂度是O(N^2)
堆排序:建堆是O(N * logN),取堆顶数据是O(N):arrow_right:时间复杂度是O(N * logN)
这俩的区别可大着呢!如果不会时间复杂度和空间复杂度请看:数据结构自学教程——算法的时间复杂度和空间复杂度
这样看来,堆排序还是很棒的,但我们一般不会这样使用堆排序,因为它需要另建一个堆来存放数据,空间复杂度是O(N)
那么有没有什么办法,能够在原本的数组上建堆,使用堆排序呢?
1.2 优化:直接在数组上建堆
利用:one:向上调整函数,插入数据的思路建堆或:two:向下调整从下往上建堆
1.2.1 向上调整建堆
//在原数组中向上调整,建堆
for (int i = 1; i < size; i++)//i没必要从0开始,第一次没必要向上调整
{
AdjustUp(a, i);
}
1.2.2 向下调整建堆
向下调整函数是有要求的,左子树右子数都为大堆(或小堆)才能用。
那样的话就从下往上调,先让子树满足条件,再逐渐向上。
并且叶子节点不用往下调(它们就是最下面的),就从倒数第一个非叶子节点(也就是最后一个节点的父亲)开始调整。
//向下调整,建堆(从倒数第一个非叶子节点开始,也就是最后一个节点的父亲)
for (int i = (size - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, size, i);
}
1.2.3 时间复杂度对比
:one:向上调整
第二层的2个节点最多要向上调整1次,第三层的4个节点最多要向上调整2次,最后累积,我们会得到下面这个公式
:two:向下调整
第一层的1个节点最多要向下调整h-1次,第二层的2个节点最多要向下调整h-2次,最后累积,我们会得到下面这个公式
1.3 堆排序整体逻辑
提问:能够建小堆实现升序排序?
不能!
建小堆的话最小的数在第一个位置了,接下来要把第二小的数放到第二个位置。
但是将第一个位置的数从堆中除去,剩下的数关系已经乱了,要通过重新建堆才能找到次小的数,时间复杂度为O(N),实在太麻烦了。
同理可得不能建大堆实现降序排序
应该将排好的部分留在数组后部,不影响前方的排序
因此:
升序:建大堆
降序:建小堆
下面的动图用了向上调整建堆,建大堆实现升序排序
void HeapSort(int* a, int size)
{
////在原数组中向上调整,建堆
//for (int i = 1; i < size; i++)
//{
// AdjustUp(a, i);
//}
//向下调整,建堆(从倒数第一个非叶子节点开始,也就是最后一个节点的父亲)
for (int i = (size - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, size, i);
}
size_t end = size - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);//将排完了的数,不看做堆里的数
end--;
}
}
int main()
{
int a[] = { 5,10,6,1,8,2,7,0 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}