目录
一、二叉树的概念及结构
2.1 二叉树概念
2.2 特殊的二叉树:
2.3 二叉树的存储结构
二、二叉树的顺序结构
2.1 二叉树的顺序结构
2.2 堆的概念及结构
三、堆的实现
3.1 插入数据
3.1.1向上调整算法
3.1.2向下调整算法
3.2 删除数据
3.2.1向下调整算法
3.2.2删除数据
3.3 其他
四、堆的应用
4.1 堆排序
4.1.1升序建大堆,降序建小堆?
4.1.2代码
4.2 Top-K问题
4.2.1什么是Top-K问题
4.2.2如何解决Top-K问题
4.2.3建立Top-K问题
4.2.4代码
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成上述概念非常重要!请大家在下面学习时要时常记起!
二叉树的特点:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
1. 满二叉树:如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
2. 完全二叉树:满二叉树是一种特殊的完全二叉树。完全二叉树最后一行可能不满,但是从左到右一定是连续存在的结点。
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。我们这里做简单了解,现在我们使用堆来实现二叉树。
完全二叉树更适合使用顺序结构存储,它相较于不完全的二叉树不会造成空间浪费。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。
小堆:父结点 <= 两个子结点
大堆:父结点 >= 两个子结点
(听爸爸的话,爸爸小的叫小堆,爸爸大的叫大堆)
我们先来看一下堆的创建:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的删除
void HeapPop(Heap* php);
// 取堆顶的数据
HPDataType HeapTop(Heap* php);
// 堆的数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);
我们的堆还是动态开辟的数组空间,我们还是需要提前检查容量并扩容。
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("HeapPush->realloc");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
AdjustUp(php->a, php->size - 1);
//AdjustDown
}
我们需要让插入的数据满足我们所使用堆的性质,我们先来认识一下向上调整算法,即让插入元素与其父结点比较,如果不满足我们所创建的堆的性质,则需要二者交换,然后继续向上比较。
以下是我们写的代码:
紧接着我们可以来检验以下:
我们在return处打上断点(F9),接着直接按F5来进行调试,并在监视栏中观察我们的a数组:
我们可以直观地看到,我们随机写入数组的值已经变成了小堆存储。
我们在这里留下一个引子,在删除数据时会讲到向下调整算法。
我们还心心念念想着堆可以像之前学的数据结构那样直接让size--,可是堆的删除规定是删除根结点,因为这样才能有更强的功能性。
我们来思考应该如何删除根结点,可以像顺序表一样挨个覆盖吗?很显然是不行的,如果我们单看逻辑图可能不好看出来,但当我们带入一个数组以后再观察,很显然是错的:
所以我们可以让首尾交换然后再删除尾,最后让根节点向下调整。
我们的parent应该和哪个孩子比较呢?
我们来想一想,我们比较的目的肯定是交换父结点和子结点,那如果我们的parent和较大的孩子比较并交换(以此为例),我们的根节点变成了71,很明显父结点大于子节点21,那么这还符合小堆的排列规则吗?很明显是不的。
所以我们首先要先求出较小的子结点。我们假设左孩子小,如果假设错误,则改正。
因为我们如果交换,则每次都要更新父子结点,所以我们直接把我们的假设判断放在while循环内:
我们还有堆的初始化、堆的销毁、堆的取顶、堆的数据个数、堆的判空,这与之前我们学过的数据结构大同小异,我们不做赘述,大家自行查看代码:
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
void HeapDestory(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
HPDataType HeapTop(Heap* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
有关代码我已经上传到Gitee,大家可以自行查看:登录 - Gitee.com
堆排序应用的主要是利用大堆或者小堆的特性来进行。
升序:建大堆
降序:建小堆
然后我们就利用向上调整或向下调整即可进行排序。
//升序
void HeapSort(int* a, int n)
{
//建大堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
注意我们之前的代码AdjustUp和AdjustDown都是应用于小堆,如果要实现上述代码我们还要进行修改。
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
例如我们游戏中的国服前100、现实中的福克斯排行榜......在众多数据中需要找出前k个最值即为Top-K问题。
我们可能会想到刚学过的堆排序来解决Top-K问题,但是我们的所有数据往往数以万计甚至更多,我们如果要用堆排序,我们堆占用的内存未免太大了,所以我们就想到了我们可以只建一个很小的堆,这个堆正好是K个结点,如果后面的数据有满足条件的,就和堆中的数据进行替换,到最后保留的肯定就是我们的K个最值啦!
要验证我们的代码是否正确,肯定需要我们人为先建立一个Top-K问题,下面跟着我一起来看如何建立那么庞大的数据库:
然后我们再进入我们写程序的文件就可以看到已经存储很多数据的data.txt文件。
void PrintTopK(const char* file, int k)
{
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen");
exit(-1);
}
//不支持变长数组,需要malloc
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL)
{
perror("minheap malloc");
exit(-1);
}
//先读取前K个数据、建小堆
int i = 0;
for (; i < k; i++)
{
fscanf(fout, "%d", &minheap[i]);
AdjustUp2(minheap, i);
}
//从文件中拿数据开始比较
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
if (x > minheap[0])
{
minheap[0] = x;
AdjustDown2(minheap, k, 0);
}
}
//打印前K个
i = 0;
for (; i < k; i++)
{
printf("%d\n", minheap[i]);
}
free(minheap);
minheap = NULL;
fclose(fout);
}