一、堆的定义及结构
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树。
如依据二叉树的说法堆根据根的大小可分为大根堆和小根堆
大根堆:堆中每个根节点都大于左右孩子。
小根堆:堆中每个根节点都小于左右孩子。
大根堆与小根堆形如下图:
二、堆的应用
1.堆排序:堆排序即利用堆的思想来进行排序,总共分为两个步骤:
建堆
升序:建大堆降序:建小堆利用堆删除思想来进行排序建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。2.TopK问题就是在很多组数据中找前K个最大的或者最小的数。
三、堆的代码实现
1.堆的存储结构选择
因为堆可以看作一棵完全二叉树,而完全二叉树用数组的存储结构利用其特性能够根据其下标找到父亲节点和孩子节点。
parent = (child - 1)/2 //child 任意孩子节点的下标
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
typedef int DataType;
typedef struct Heap
{
int size; //大小
int capacity; //容量
DataType *a;
}HP;
2.堆的初始化HeapInit()
void HeapInit(HP* php)
{
php->size = php->capacity = 0;
php->a = NULL;
}
整个初始化方式类似于数组将其所定义的数据都给初始化,这里php->a初始化为空,是后面在堆的插入中再为a申请存储空间,当然也可以在初始化的时候为其分配存储空间。
3.堆的插入HeapPush()
//向上调整
void AdjustUp(DataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
// 堆的插入
void HeapPush(HP* php, DataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
DataType* temp = (DataType*)realloc(php->a,sizeof(DataType)* newcapacity);
if (temp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = temp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
代码拆解讲解:
因为我们在堆初始化的时候没有给 php->a申请存储空间所以在进行插入时要为其申请存储空间。后续当数组a的大小于容量的话就需要扩容,这里我们用的时realloc进行申请存储空间和后续数据的扩容。
关于realloc,有两个参数void*ptr,size_t size。一个是原分配的空间,一个是分配多少大小(字节),若原分配空间为空,则realloc 相当于 malloc。分配完空间后更新数组的大小和容量。进行插入。
插入需要用到堆的向上调整算法:为什么要用到向上调整算法?因为堆的创建需要不断的移动对应值使其符合堆的定义。
关于向上调整算法:
有一个定性要求——进行调整时前面的数据必须是堆,如果不是堆最后构建完后就不一定是堆。
1.向上调整
要求:向上调整的前提是前面的数据是堆。
参数:需要排序的数组,因为是要前面的数据是堆所以进行第一次向上调整时需要前面有最少一个数据,所以向上调整时第一个数据是从数组下标为1的位置开始(1)将数据向上调(若一直调将调到堆顶)所以要先初始化父亲节点
parent = (child - 1)/2。
(2)并不是调一次所以需要一个循环,条件?每次循环child换到parent位置,parent = (child - 1) /2。
但parent 并不会小于 0 尽管到最坏的调整情况,parent = -1……/2 ,parent仍然 = 0。
所以循环可控制为child > 0
(3)调完一次后若parent节点还是大于/小于child的值,继续调整,此时的child = parent ,parent = (child-1)/2。
若不需要调的话就直接break循环即可。
4.堆的删除HeapPop()
//向下调整
void AdjustDown(DataType*a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
child++;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//堆的删除
void HeapPop(HP* php)
{
assert(php); //堆删除时先将堆顶与最后的节点进行更换,这样的话整个树的左右子树就还会符合堆的规则
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
--php->size;
Adjustdown(php->a, php->size, 0);
}
堆的删除使用了一个堆顶与最后一个元素交换,大小--将堆顶的数据所覆盖,然后对其进行向下调整算法,利用循环判空可以将其堆里的数据全部删除。
向下调整算法:
2.向下调整
要求:左右子树必须是堆
参数:需要排序的数组,数组的个数,根节点也就是parent = 0;因为是向下调整,所以是从起始位置开始
(1)左右孩子比较找到较小的那个,进行交换。左右孩子比较时,可先假设左孩子是较小的,在比较如果左孩子是最小的那么就将左孩子与调整节点交换。
左孩子+1 = 右孩子。
(2)调整一次要是继续调整则更换孩子节点与父亲节点的下标。这里有个可能会导致数组越界的情况,如:进行左右孩子比较时,左孩子有值右孩子为空,这样程序就会判定右孩子为较小的孩子则进行交换时,会导致数组越界的情况
parent = child。
child = parent*2 + 1。
(3)最坏的调整情况是从根节点调到叶子节点(child的值是逐渐变大的过程)。所以循环的条件是child < n
5.判断堆是否为空HeapEmpty()
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size > 0 ? false : true;
}
C里面使用布尔值的话需要使用一个头文件 #include<stdbool.h>
直接用一个三目运算符判读堆里的大小是否大于0就行。
6.获取堆顶数据Heaptop()
//获取堆顶
DataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
返回数组第一个元素即为堆顶。
7.堆的销毁HeapDestroy()
//堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
四、TopK问题
TopK问题:从很多很多个数据里面找出K个最大/最小的数据
1.模拟实现,先造多个数据,保存到文本文件中(需要对C语言的文件操作有一定的基础,看得懂一些代码)
void CreateNDate() { // 造数据 int n = 10000; srand(time(0)); const char* file = "data.txt"; FILE* fin = fopen(file, "w"); if (fin == NULL) { perror("fopen error"); return; } for (int i = 0; i < n; ++i) { int x = rand() % 1000000; fprintf(fin, "%d\n", x); } fclose(fin); }
2.从文本文件中先读取K个数据至所创建大小为K的数组中进行建堆,然后看你所找的是最大的前K个数据还是最小的,找最大建小堆,最小建大堆。
注意:向下调整算法你要根据你所选建立的是大堆还是小堆,需要对选择的左孩子还是右孩子进行一个更新。
//向下调整 void Adjustdown(HPDataType* a, int n, int parent) { int child = parent * 2 + 1;//假设左孩子是较小的值 while (child < n) { if (child + 1 < n && a[child] > a[child + 1])//防止数组越界 { ++child; //若假设不成立则右孩子是较小的值 } if (a[parent] > a[child]) //向下调整 将大的放到叶子节点 { Swap(&a[parent], &a[child]); parent = child; child = parent * 2 + 1; } else break; } } void PrintTopk(const char* filename,int k) { FILE* fout = fopen(filename, "r"); if (fout == NULL) { perror("fopen error"); return; } int* minheap = (int *)malloc(sizeof(int) * k); for (int i = 0; i < k; i++) { fscanf(fout, "%d", &minheap[i]); } //建堆 for (int i = (k-1-1)/2; i >= 0; i--) { Adjustdown(minheap, k, i); //向下调整 } //将n-k个文本数据进堆,与堆顶进行比较,大于堆顶则进堆 int x = 0; while (fscanf(fout, "%d",&x) != EOF) { if (x > minheap[0]) { minheap[0] = x; Adjustdown(minheap, k, 0); //向下调整 } } for (int i = 0; i < k; i++) { printf("%d ", minheap[i]); } printf("\n"); }
3.然后将n-k个数据与堆顶数据比较大的进堆然后进行向下调整将其放到合适的位置。
最后得到得是一个堆(并没有排好序)。
关于其中向下调整建堆,和向下调整进行堆排列得方法及解四,我们放在堆排序那里。
五、堆排序HeapSort()
1.堆排序:堆排序即利用堆的思想来进行排序,总共分为两个步骤:
建堆
升序:建大堆降序:建小堆利用堆删除思想来进行排序什么是堆删除的思想:如大堆,先堆顶与最后一个元素交换,并数组--(这样堆顶放到这里就不会在进行其他的动作),进行向下调整将次大的放到堆顶。我们从TopK问题那里可以了解到向下调整也可以建堆,所以进行堆排序时我们就只需要向下调整这个算法就可以了。
//向下调整:从堆顶向下;左子树和右子树必须是堆
void AdjustDwon(int *a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//向下调整建堆,
void test02(int *arr,int n)
{
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDwon(arr, n, i);
}
int end = n - 1;
//排序
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDwon(arr, end, 0);
--end;
}
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
}
建堆部分:
拆解:可以结合代码看,更容易了解。
排序的话就是用堆的删除的思想。
六、总结
堆的存储结构与顺序表基本上一样,建堆,堆的删除要了解其堆的特性。向上调整算法、向下调整算法、堆删除的思想。而TopK问题和堆排序,也是要注重向上调整和向下调整两种算法,了解向下调整是如何建堆的这个很关键。
文章关于堆、TopK问题、堆排序的全部代码都放在Young/数据结构 - 码云 - 开源中国 (gitee.com)
最后,大家如果觉得文章写得不错的话,希望可以来个三连支持一下博主。
咱们一起努力一起学习!!!