实现顺序结构二叉树------堆
堆 是一种特殊完全二叉树 ,它不仅有完全二叉树的性质还具备其它特性,一般使用顺序结构的数组来存储数据。
1 堆的概念和结构
1.1 堆的概念
堆分为大根堆 (也称为最大堆)和小根堆(最小堆)。
- 堆中某个结点的值总是不大于其父节点(大根堆)或不小于其父节点(小根堆)。(也可理解为父节点总是>=或<=左右孩子结点的值(孩子结点存在的话))
- 堆总是一棵完全二叉树。
小根堆:
大根堆:

下面性质很重要,堆的一些函数实现会用到。堆也是二叉树,所以下面性质堆也有。
1.2 二叉树的性质(底层数组)
对于具有n个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0号开始则对于序号为i的结点有:
- 若
i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点 - 若
2i+1<n,左孩⼦序号:2i+1,2i+1>=n否则无左孩子 - 若
2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
1.2 堆的实现
1.2.1 Heap.h头文件的初始设置
c
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;
int capacity;
}HP;
typedef int HPDataType用typedef重命名数据类型int,从而在替换堆的数据类型时就只用替换这条语句,实现代码可扩展性 。Heap堆结构包含一个指向数组的指针arr,堆中元素个数的size,堆的容量的capacity。
1.2.2 堆的初始化
c
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
assert(php)判断php是否为NULL,如果为NULL则停止执行后面步骤并报错,将arr数组指针初始化为NULL,并将size和capacity都置为0。
1.2.3 堆的插入
c
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * newcapacity);
if (temp == NULL)
{
perror("realloc failed!");
exit(1);
}
php->arr = temp;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
//插入后要用向上调整算法
AdjustUp(php->arr, php->size);
php->size++;
}
堆的插入详细刨析 :
第一部分:
c
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * newcapacity);
if (temp == NULL)
{
perror("realloc failed!");
exit(1);
}
php->arr = temp;
php->capacity = newcapacity;
}
先
assert(php)判断php是否为是否为NULL,插入的时候我们要用php->size == php->capacity判断数组是否还有多余空间插入,如果没有则用三目表达式int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity确定新容量newcapacity,并用realloc动态开辟函数扩容,并更新capacity的大小。
第二部分:
c
php->arr[php->size] = x;
将
x插入数组末位即size下标位置
上面的部分只是将数据添加到数组里面了,但是并不能保证还是堆这个结构。所以我们必须用向上调整算法AdjustUp保证添加后仍是堆结构
AdjustUp函数如下:
c
void AdjustUp(HPDataType* a, int child)
{
while (child >0)//假如是根结点就不用移动
{
int parent = (child - 1) / 2;
//小根堆<
//大根堆>
if (a[parent] > a[child])
{
//要在数组内部进行交换
swap(&a[parent], &a[child]);
child = parent;
}
else
{
break;
}
}
}

函数参数说明:a是数组首元素地址,child是新插入结点的下标。
首先用二叉树性质
parent = (child - 1) / 2;找到最后一个尾节点的parent父结点,如果parent父节点>child子结点,说明不满足小根堆的结构性质,将父节点parent和子节点child交换即可。这样就可以保证在局部下是一个小根堆。

堆只要求了父节点和子节点之间的关系,并没有涉及到一个父节点的左右子结点之间的关系,所以我们只用判断该点和父节点关系就行了。
循环结束条件:
child子节点更新到了根结点即child=0则不用比较了或者此时child结点与父结点parent符合堆的关系。所以外层大循环为while(child>0)。
第三部分:
c
php->size++;
最后将
size++更新。
1.2.4 堆的删除
有元素才能进行删除,所以我们首先要写一个
HPEmpty函数判断堆是否为空
c
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
size代表的就是堆的元素数目。
堆的删除代码实现:
c
void HPPop(HP* php)
{
assert(php && !HPEmpty(php));
swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, php->size, 0);
}
堆的删除是删除堆顶元素,删除堆顶元素的方法是将堆顶元素与最后一个元素互换,再将
size--,并重新用向下调整算法AdjustDown将变换后的数组再次变成堆结构。
下面重点说明向下调整算法AdjustDown:
特别说明:向下调整算法的左右子树必须满足堆结构
c
void AdjustDown(HPDataType* a, int n, int parent)
{
//找左右子树最小值与之交换
int child = parent * 2 + 1;
while (child < n)
{
//假设左孩子为较小值
//小跟堆 a[child + 1] < a[child]
//大根堆 a[child + 1] > a[child]
if ((child + 1 < n)&&(a[child + 1] < a[child]))
{
child = child + 1;
}
//找到最小的孩子了
//小根堆 a[parent] > a[child]
//大根堆 a[parent] < a[child]
if (a[parent] > a[child])
{
swap(&a[parent], &a[child]);
}
else
{
break;
}
parent = child;
child = parent * 2 + 1;
}
}

核心思想:以该结点为父节点,找到孩子左右结点的最小值,并与之交换。
堆的向下调整算法详解
第一部分:
c
//找左右子树最小值与之交换
int child = parent * 2 + 1;
while (child < n)
先假设左孩子为左右孩子中的最小者(如果是建大堆的话则假设为最大者),循环结束条件为
child<n,孩子child结点最大为n-1不能超过n。
第二部分:
c
if ((child + 1 < n)&&(a[child + 1] < a[child]))
{
child = child + 1;
}
child + 1 < n判断右结点是否存在,如果存在右结点并且它比左结点要小则假设不成立,将最小结点变为右结点child = child + 1(即是由左结点变为右结点)。
第三部分:
c
if (a[parent] > a[child])
{
swap(&a[parent], &a[child]);
}
else
{
break;
}
这里的swap函数必须地址从而实现两个数本身的互换
c
void swap(HPDataType* a, HPDataType* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
将最小结点
child与父结点parent比较,如果符合a[parent] > a[child],两个数组元素就互换位置。否则代表堆已经排好了,直接break跳出循环就行了。
第四部分:
c
parent = child;
child = parent * 2 + 1;
改变父节点
parent和最小结点child的值
1.2.5 返回堆顶数据
c
HPDataType HPTop(HP* php)
{
assert(php && !HPEmpty(php));
return php->arr[0];
}
php指针不为NULL并且堆不为空
assert(php && !HPEmpty(php)),直接返回第一个元素就是堆顶数据php->arr[0]。
1.2.6 堆的元素数
c
int HPSize(HP* php)
{
assert(php);
return php->size;
}
直接返回
php->size
1.3 堆的应用
1.3.1 堆排序
c
void HeapSort(int* arr, int n)
{
//向下调整算法建堆
for(int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
//向上调整算法建堆
//for (int i = 0; i < n; i++)
//{
//AdjustUp(arr, i);
//}
int end = n - 1;
while (end > 0)
{
swap(&(arr[0]), &(arr[end]));
AdjustDown(arr,end, 0);
end--;
}
}
第一步部分:
c
//向下调整算法建堆
for(int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
//向上调整算法建堆
//for (int i = 0; i < n; i++)
//{
//AdjustUp(arr, i);
//}
先用向上调整算法和向下调整算法将数组构成一个堆结构
向上调整算法建堆思想:先找到最后一个结点的父结点,将这一个父节点的局部向下调整建成一个最小堆,再parent--将父节点转移,从而将一个个父节点为根结点的树建成堆结构,直到最后一个整个树的根结点作为父结点parent。
向下调整算法思想:从数组第一个元素开始,通过向下调整建成局部堆,并将数组元素逐渐加入,并最后建成一个完整的堆。
第二部分:
c
int end = n - 1;
while (end > 0)
{
swap(&(arr[0]), &(arr[end]));
AdjustDown(arr,end, 0);
end--;
}
我建的是小根堆所以数组第一个元素就是整个数组的最小值,将数组的最小值与数组最后一个元素交换
swap(&(arr[0]), &(arr[end])),这样就将最小的元素放到最后面,并再次向下调整AdjustDown(arr,end, 0)确保这是一个堆结构(此时堆顶的数据就是第二小的元素了),并将end--,更新未被排序的最后一个数组元素小标和未被排序的元素数量。这样就能将小的元素放在后面从而形成降序排列。
如果想升序排列则建大根堆,将堆顶最大的元素放在后面。
1.3.2 TOP-K问题
TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了
(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:
1)⽤数据集合中前K个元素来建堆
前k个最⼤的元素,则建⼩堆
前k个最⼩的元素,则建⼤堆
2)⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素
将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素
c
void CreateNDate()
{
//造数据
int n = 100000;
srand(time(0));
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("flie open failed!");
exit(1);
}
for (int i = 0; i < n; i++)
{
int x = rand() % 100000 + 1;
fprintf(pf, "%d\n", x);
}
fclose(pf);
}
//我建的是小堆,可以找最大的k个数
void topk()
{
int k = 0;
int num = 0;
printf("请输入k:>");
scanf("%d", &k);
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("file open failed!");
exit(1);
}
//将k个数据导入到数组中
int* minHeap = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++)
{
fscanf(pf, "%d", &minHeap[i]);
}
//建立小根堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap, k, i);
}
while (fscanf(pf, "%d", &num)!= EOF)
{
if (num > minHeap[0])
{
minHeap[0] = num;
}
AdjustDown(minHeap, k, 0);
}
fclose(pf);
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
}
核心代码:
c
while (fscanf(pf, "%d", &num)!= EOF)
{
if (num > minHeap[0])
{
minHeap[0] = num;
}
AdjustDown(minHeap, k, 0);
}
建的是小根堆,则堆顶是最小 的元素,如果
data.txt里面的元素比堆顶的数据大则替换,并再次向下调整保证堆结构,这样我们一次次的将堆结构里面最小的元素替换掉,则里面剩余的就是最大 的k个元素了。
