实现顺序结构二叉树------堆
堆 是一种特殊完全二叉树 ,它不仅有完全二叉树的性质还具备其它特性,一般使用顺序结构的数组来存储数据。
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
个元素了。