前言
在上一篇内容:大小堆的实现(C语言),我们实现了关于创建大小堆的各函数与实现。但是如果突然要使用一个堆排序但是此时并没有一个现成的堆,这就需要花费时间去新建实现堆的插入删除这些操作从而实现一个堆,并且在插入的过程中存在内存空间的消耗(malloc空间),那是否有一些其它办法可以避免以上问题呢?
数组建堆
我们能不能不消耗空间就完成一次建堆?直接将所给的数组建堆可以吗?
这些数字在物理逻辑上是一个数组,而在虚拟逻辑上是一个完全二叉树,那么将这个完全二叉树进行一些调整后是不是就能得到一个堆呢?我们尝试利用之前的向上调整算法,完成建堆的操作:
上述操作的本质就是模拟堆插入的过程建堆
(第一个视为堆,第二个插入然后向上调堆、第一个和第二个视为堆,第三个插入然后向上调堆)
可以发现我们在没有申请内存空间的前提下,仅利用一个向上调整算法就完成了将数组建堆的操作,并最终到了一个小堆,但是如果我们在选出这个小堆中的最小值后,再想要选出这个堆中的次小值就会出现问题:
可以发现当要选出次小值时,缺少了”1“的小堆的剩余元素并不能算作是一个小堆它们都是无序的,所以为了选出次小值,我们还要建堆(将数组元素向上调整建堆)以此类推这就相当于选一次值就要建一次堆,那还不如直接选用时间复杂度为O(n)的暴力遍历数组选取最小值,次小值,而我们建堆的目的就是为了方便我们选出我们想要的最大/小、次大/小的数,但是很明显如果将利用向上调整算法建立一个小堆是无法满足我们的需求的,所以我们应该利用向上调整算法去建立一个大堆。
结论:若利用向上调整算法建小堆,如果取出最小值,那么剩下的数有可能不是堆,故应建大堆
只需要该算法做一些调整,将(a[child] < a[parent])变为(a[child] > a[parent]),就可以建大堆:
调整后的结果是我们得到了一个大堆,此时即使将堆顶的“9”删除,剩余的两个子树也仍然是大堆的形式,这就意味着我们在选出最小数或次小数后只需要进行向下调整即可不需要考虑重新建堆,当我们想要最小值,只需要交换首尾元素的位置,取出此时的堆顶元素即可,想要获取次小值,只需要将尾部的数不再视为堆的一部分再次重复以上操作即可:
堆排序
其实上述的内容,就是我们实际中堆排序的一种实现方式,即利用向上调整算法建大堆然后再利用向下调整算法将挑选后剩余的元素重新调整为虚拟逻辑上的大堆以便下一次的选取
利用向上/下调整算法将数组中的数字在虚拟逻辑上重新变为大/小堆与重新建堆的区别是?
在堆排序算法中,我们需要构建一个最大/小堆来进行排序。这可以通过两种方式实现:
1. 重新建堆:这是一种直接的方法,从数组的首元素开始逐个插入到空堆中,步骤如下:
- 创建一个空的堆
- 将数组中的元素逐个插入到空堆中
- 最终得到了一个满足最大(或最小)堆性质的完整二叉树
2. 向上/下调整:这是一种当已有部分构成的近似完全二叉树进行调整来达到目标状态的方法
- 向上调整:从某节点开始,将其与父节点比较并交换位置,直至满足最大/小堆性质。该过程会将当前节点及其祖先节点推向正确位置,并保持子树仍然满足对应性质。
- 向下调整:从某节点开始,将其与左右子节点比较并交换位置,直至满足最大/小堆性质。该过程会使当前节点及其后代子树变为有序树,并保持其他部分仍然满足对应性质
3、区别:
- 重新建堆是一种从零开始构建堆的方法,它将整个数组视为初始状态,并按顺序插入元素来构建最大(或最小)堆。这种方法需要较多的时间和空间复杂度。
- 向上调整和向下调整算法是在已有部分近似完全二叉树的基础上进行调整,通过比较节点与其父节点或子节点来达到维护最大(或最小)堆性质的目标。这两种算法可以在不重建完全二叉树的情况下对特定位置进行优化操作,因此具有更高效率。
4、总结:
- 如果已拥有一个无序数组并希望将其转换为一个满足最大/小堆性质的完全二叉树,则可以使用重新建堆方法
- 如果你已经拥有一个近似完全二叉树,并且只需对其中某些位置进行修正以满足最大(或最小)堆性质,则应使用向上调整或向下调整算法
利用向上调整算法实现堆排序
使用向上调整算法实现堆排序的步骤如下:
-
构建最大堆:从数组的第一个非叶子节点开始,依次对每个节点进行向上调整操作,使其满足最大堆性质。可以通过遍历非叶子节点并依次调用向上调整函数来完成这一步骤。
-
排序:将根节点(数组首元素)与末尾元素交换位置,并将末尾元素从堆中移除。然后对新的根节点进行一次向下调整操作,以维持最大堆性质。重复这个过程直到所有元素都被移除并排好序。
具体实现代码如下所示(假设数组是从索引 0 开始):
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
//向上调整,此时传递过来的是最后一个孩子的元素下标我们用child表示
void AdjustUP(HPDataType* a,int child)
{
//由于我们要调整父亲与孩子的位置所以此时也需要父亲元素的下标,而0父亲元素的下标值 = (任意一个孩子的下标值-1)/ 2
int parent = (child - 1) / 2;
//当孩子等于0的时位于树顶(数组首元素的位置),树顶元素没有父亲,循环结束
while(child > 0)
{
//如果孩子还未到顶且它的下标对应的元素值小于它的父亲的下标对应的元素值,就将父子位置交换,交换玩后还要将下标对应的值“向上移动”
//if (a[child] < a[parent])
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
//由于这是一个小堆,所以当孩子大于等于父亲时不需要交换,直接退出循环即可
else
{
break;
}
}
}
//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent)
{
//根据之前的推论,左孩子的下标值为父亲下标值的两倍+1,左孩子的下标值为父亲下标值的两倍+2
int child = parent * 2 + 1;
//循环结束的条件是走到叶子结点
while (child < size)
{
//假设左孩子小,若假设失败则更新child,转换为右孩子小,同时保证child的下标不会越界
//if (child + 1 < size && a[child + 1] < a[child])
if (child + 1< size && a[child + 1] > a[child])
{
++child;
}
if (a[child] < a[parent])
{
//如果此时满足孩子小于父亲则交换父子位置,同时令父亲的下标变为此时的儿子所在下标,儿子所在下标变为自己的儿子所在的下标(向下递归)
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
//如果父亲小于等于左孩子就证明删除后形成的新堆是一个小堆,不再需要向下调整算法,循环结束
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
//构建大堆
for (int i = 1; i < n; i++)
{
AdjustUP(a, i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
int main()
{
int a[] = { 4,6,2,1,5,8,2,9 };
HeapSort(a, sizeof(a) / sizeof(int));
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
//HeapSort(a, i);
return 0;
}
利用向下调整算法实现堆排序
1、从最后一个结点的父亲开始向下调整
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent)
{
//根据之前的推论,左孩子的下标值为父亲下标值的两倍+1,左孩子的下标值为父亲下标值的两倍+2
int child = parent * 2 + 1;
//循环结束的条件是走到叶子结点
while (child < size)
{
//假设左孩子小,若假设失败则更新child,转换为右孩子小,同时保证child的下标不会越界
if (child + 1< size && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
//如果此时满足孩子小于父亲则交换父子位置,同时令父亲的下标变为此时的儿子所在下标,儿子所在下标变为自己的儿子所在的下标(向下递归)
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
//如果父亲小于等于左孩子就证明删除后形成的新堆是一个小堆,不再需要向下调整算法,循环结束
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
//构建最小堆
for(int i = (n-1-1)/2;i>=0;--i)
{
AdjustDown(a,n,i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
int main()
{
int a[] = { 4,6,2,1,5,8,2,9 };
HeapSort(a, sizeof(a) / sizeof(int));
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
//HeapSort(a, i);
return 0;
}
~over~