1.堆的概念
首先,堆是一棵二叉树,而且是完全二叉树。不同于其他二叉树的储存,堆是由一维数组来储存数据的。再根据根节点的与其孩子节点的大小关系可以分为大(根)堆和小(根)堆。大堆就是根节点大于等于自己的孩子节点,同理,小堆就是根节点小于等于自己的孩子节点。
2.堆的实现
2.1. 向下调整算法
数组int array[] = {27,15,19,18,28,34,65,49,25,37};为例。向下调整算法要求左右子树必须是堆才可以进行,并且要同为大堆或者小堆。
具体如何用代码实现,我们一会再讲。
2.2堆的创建
我们以数组int arr[] = {6,9,4,20,13,18}为例来建立一个大堆
我们可以看到,在从最后一个节点开始向上调整形成大根堆的过程中,子树的堆结构可能会被破坏,所以我们需要递归调用创建堆的过程来保证子树也是堆。这里用到的算法叫做向上调整算法,具体代码我们下面会讲。
2.3堆的插入
从数组尾部插入一个数据,然后使用向上调整算法将数据插入到堆内。我们还是以数组int arr[] = {6,9,4,20,13,18}为例,这次我们创建一个小堆。我们向数组尾插入一个1,得到数组int arr[] = {6,9,4,20,13,18,1}
2.4堆的删除
堆的删除就是,我们只能删除堆的堆顶元素,如果随机地在堆内删除一个元素,会破坏堆的结构,然后要花费许多操作去重新调整堆。所以我们将要删除的堆顶元素与最后一个元素进行交换后,再进行删除,然后进行从上向下的向下调整算法来把堆的结构完善。以数组int arr[] ={6,9,4,20,13,18}为例
3.堆的代码实现
3.1堆的定义和初始化
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
3.2向上调整算法和向下调整算法
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0) //当child下标大于0时继续判断进行调整
{
if (a[child] < a[parent]) //建小堆,从下至上调整
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整算法
//这里比向上调整算法多传一个int n是堆的数组大小,因为我们需要从0到n整个范围内调整堆的结构。
void AdjustDown(HPDataType* a, int n, int parent)
{
assert(a);
int child = 2 * parent + 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 = 2 * parent + 1;
}
else
{
break;
}
}
}
3.3堆的插入
void HPPush(HP* php, HPDataType x)
{
assert(php);
//if语句的作用是当空间已经满时进行扩容
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity*sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
}
php->capacity = newcapacity;
php->a = tmp;
}
php->a[php->size] = x; //在尾部插入一个元素
php->size++; //堆里的元素个数加一
AdjustUp(php->a, php->size - 1); //向上调整
}
4.堆排序
我们学习了堆的向下调整算法,可以自由地建小堆或者大堆。
如果我们需要排升序,建大堆;排降序,建小堆。
以数组int arr[] = {6,9,4,20,13,18,1}为例排升序。
4.1堆的Top-k问题
在我们生活里有很多排名,当数据样本小的时候,我们可以用简单的排序可以解决,但是遇到数据样本大的时候我们再去用那些简单的排序就显得捉襟见肘了。比如我们给全世界的百万富翁;世界五百强企业等排个名次,这些数据样本非常庞大,我们选择用堆排序是一个非常好的解决方案。
4.1.1建堆思路
找前k个最大的,建小堆
找前k个最小的,建大堆
在我们选取样本的前k个数据然后建堆之后,再和剩下的—— n-k个逐一比较,如果有比堆顶元素更大(小)的就替换堆顶元素,并向下调整,直至把所有样本数据比较完,这样我们就得到了前k个最大的数据或者最小的数据。
4.1.2代码实现
我们的数据要尽可能的多而且随机。所以我们使用文件来储存数据
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 (size_t i = 0; i < n; ++i)
{
int x = rand() % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
然后再按照上述思路来进行排序
void PrintTopK(int k)
{
/*申请一块内存来存放k个数据*/
int* HeapMin = (int*)malloc(sizeof(int) * k);
if (HeapMin == NULL)
{
perror("malloc fail");
return;
}
/*创建一个常量字符指针来保存data文件的地址*/
const char* file = "data.txt";
/*创建一个FILE*类型的指针,用fopen来打开data.txt,模式为仅读*/
FILE* read = fopen(file, "r");
if (read == NULL)
{
perror("read fail");
return;
}
for (int i = 0; i < k; i++)
{
//将文件里面的前k个数据写到数组里面
fscanf(read, "%d", &HeapMin[i]);
}
/*造一个k个数的小堆*/
for (int i = (k-1-1)/2; i >= 0; i--)
{
AdjustDown(HeapMin, k, i);
}
/*从文件里读取剩下的n-k个数据,然后进行向下调整*/
int x = 0;
while (fscanf(read, "%d",&x) > 0)
{
if (HeapMin[0] < x)
{
HeapMin[0] = x;
AdjustDown(HeapMin, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", HeapMin[i]);
}
printf("\n");
}
那么如何验证我们代码的正确性呢?从上面的代码不难看出,所生成的数据都一定不会大于1000000,我们就在文件里直接修改10个数据,使这10个数据变成最大或者最小。比如这样
代码验证结果:
本篇博客到这里就结束啦,如果有哪里不对,还请大家指正,如果大家觉得有用,别忘了三连哦,我也会回你哒,谢谢大家!