文章目录
[1.1 向上调整函数](#1.1 向上调整函数)
[1.2 向下调整函数](#1.2 向下调整函数)
[2.1 向上调整建堆](#2.1 向上调整建堆)
[2.2 向下调整建堆](#2.2 向下调整建堆)
前言
上一篇文章让我们对树、二叉树以及堆的基本概念有了一个大概的认识,这篇文章会对堆的用法作一个更加全面的解析。
1.向上调整以及向下调整函数的解析
在篇文章中我们只是对这两个函数做了一个简单的概述,下面我们做一个更加详细的分析,以便更好得理解后面的堆排序以及TopK问题。
1.1 向上调整函数
我们在堆尾或者说是数组的尾部插入一个数据,首先是检查数组的空间是否充足(不足就扩容),然后插入数据。这里首先我们要知道原堆是大堆还是小堆,如果是小堆那么要求它要比父亲大,大堆要求它比父亲小。
cpp
void AdjustUp(HeapdataType* arr,int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[parent] > arr[child])
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
我们在设计函数时,将函数的参数设为一个数组,是为了后面可以再次利用,然后如果是小堆,那么节点交换的条件是arr[parent] > arr[child],同理,如果是大堆那么就是arr[parent] < arr[child]。
然后父亲节点与孩子节点的关系 parent = (child - 1) / 2;
左孩子与父亲的关系:leftchild=parent*2+1;
左孩子与父亲的关系:rightchild=parent*2+2;
这些关系是对父节点与孩子节点定位的关键。
总结一下,因为我们将一个可能改变堆的结构的数插入堆尾,所以要进行堆的重新调整。
1.2 向下调整函数
我们删除堆头的数据后,会导致堆的关系完全紊乱,我们为了保证左右堆的完整性,选择先将要删除的数据与堆尾的数据进行交换,因为删除堆尾的数据是极为便利的,这时,要回复堆的结构只需要将现在的堆头的数据进行向下调整,这样就只需要改变一个数据了,不需要遍历堆中所有的节点。
cpp
void AdjustDown(HeapdataType* arr, int size, int parent)
{
//因为我们要找出左右孩子中较小的那一个进行交换
//我们先假设左孩子是较小的那一个
int child = 2 * parent + 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 = 2 * parent + 1;
}
else
{
break;
}
}
}
这里展示的是调整成小堆的函数。
2.将数组建成堆
为了进行堆排序,我们需要将一个数组进行建堆,方便后面进行排序。
2.1 向上调整建堆
cpp
for (int i = 1;i < n;i++)
{
AdjustUp(arr, i);//这里的i是孩子节点
}
思路,将第一个元素看成堆,然后不断进行插入,所以利用向上调整函数,总而言之,将这里与插入过程进行类比,会更好理解。这个方法很好理解,我们尝试分析一下它的时间复杂度,不难发现为O(n log n),那是否有更高效的建堆方法呢?有的,那就是向下调整建堆。
2.2 向下调整建堆
对向上调整建堆来说,会发现越是底层,需要进行调整的可能就越多,底层不仅节点多,同时层数越多,导致比较低效,所以我们尝试看看能否从底层开始向下调整。

我们从最后一个节点的父亲节点开始向下调整,这里可以看成删除节点之后对堆进行调整。
cpp
int end = n - 1;//尾节点在数组中的位置
for (int i = (end - 1 - 1) / 2;i >=0;i--)
{
AdjustDown(arr, end, i);
}
时间复杂度分析为O(n);

3.堆排序的实现
堆排序的核心要素分析:升序建大堆,降序建小堆。是不是很诧异?怎么跟我想的是反的。听听我的解释,这里以降序排序为例:
小堆的堆头,即数组的第一个数是不是里面最小的数,降序要让最小的在最后对吧?那我们就把堆尾与堆头进行交换,然后不把排好的数参与比较,利用向下调整把堆进行复原,这里就是和删除堆头的思想差不多,但是这里选择了把元素进行保留。
cpp
int end = n - 1;
for (int i = (end - 1 - 1) / 2;i >=0;i--)
{
AdjustDown(arr, end, i);
}
end = n - 1;
while (end > 0)
{
Swap(arr, &arr[end]);
AdjustDown(arr,end,0);
end--;
}
相较于我们之前学过的冒泡排序,堆排序更加高效,它的时间复杂度是O(n log n)。
4.TopK的实现
堆不仅可以用来进行排序,还可以在大量的数中找出前K个大/小 的数。后面以找前K个大的数为例进行讲解
具体思想:
方法一:来建一个大小为N大堆,找到堆头,然后将保存后删除,再进行向下调整(HeepPop),如此循环K次,就可以了,时间复杂度O(K*log(n));
方法二:建立一个大小为K的小堆,然后让数组前K个数入堆,如果有比堆头大的数,就代替堆头元素入堆,然后进行向下调整,如此循环往复,效率与方法一相近,只是对内存的要求不大。
cpp
void CreateNDate()
{
// 造数据
int n = 100000;
srand(time(0));
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)//写入数据
{
int x = (rand() + i) % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void TopK()
{
int k;
printf("请输入k的值\n");
scanf("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc fail");
return;
}
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
// 读取文件中前k个数
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap[i]);
}
//建一个小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
// 读取剩下的N-K个数
int x = 0;
while (fscanf(fout, "%d", &x) > 0)
{
if (x > kminheap[0])
{
kminheap[0] = x;
AdjustDown(kminheap, k, 0);
}
}
printf("最大前%d个数:", k);
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
结语:
堆的相关内容到这里就结束了,我们知道堆只适用于完全二叉树,后面我们将要开始非完全二叉树的学习。最后如果发现文章中有任何问题,请帮助我指正,同时欢迎各位在评论区交流讨论。