
树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
孩⼦兄弟表⽰法: 树结构相对线性表就⽐较复杂了,要存储表⽰起来就⽐较⿇烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表⽰⽅式如:双亲表⽰法,孩⼦表⽰法、孩⼦双亲表⽰法以及孩⼦兄弟表⽰法等。我们这⾥就简单的了解其中最常⽤的孩⼦兄弟表⽰法


struct TreeNode
{
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
int data; // 结点中的数据域
};⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。
在树形结构中,我们最常用的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空。

从上图可以看出⼆叉树具备以下特点:
注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的

⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀个⼆叉树的层数为 K ,且结点总数是的k方-1,就是满二叉树。

完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为 K 的(有K层),有 n 个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从 1 ⾄ n 的结点⼀⼀对应时称之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。


顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。


现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储,需要注意的是这⾥的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。后⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。




堆具有以下性质 • 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值; • 堆总是⼀棵完全⼆叉树。
⼆叉树性质 • 对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
将新数据插⼊到数组的尾上,再进⾏向上调整算法,直到满⾜堆。 • 先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后。 • 插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可。

//交换数据
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//向上调整算法 判断条件:小堆:child < parent 大堆:child > parent
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0) //child向上调整到祖先结点停止循环
{
if (arr[child] > arr[parent]) //判断条件:小堆:child < parent 大堆:child > parent
{
swap(&arr[child], &arr[parent]);
child = parent; //向上调整,所以把子结点下标变成父结点下标
parent = (child - 1) / 2;//向上找父结点
}
else {
break;
}
}
}
//堆的插入
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newcapacity*sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
AdjustUp(php->arr,php->size);
++php->size;
}删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏向下调整算法。 向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整。

//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
// 向下调整算法 小堆:parent > child arr[child] > arr[child + 1] 大堆:parent < child arr[child] < arr[child + 1]
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1; //左孩子
while(child < n ) //n为最大结点个数,不能越界
{
if (arr[child] < arr[child + 1] && child + 1 < n) //演示大堆,拿子孩子中大的能够和父亲比较,且确保有右孩子
{
child++; //左孩子小于右孩子,将child = child + 1,(大堆)大的和父亲比较
}
if (arr[child] > arr[parent])
{
swap(&arr[child], &arr[parent]); //大于父亲,因为是大堆,进行交换
parent = child; //向下调整,所以将父结点变成子结点
child = parent * 2 + 1; //再向下寻找子结点
}
else
{
break;
}
}
}
//出堆
void HPPop(HP* php)
{
assert(!HPEmpty(php)); //检查堆是否有元素(非空堆)。
swap(&php->arr[0], &php->arr[php->size - 1]); //交换最后一个子节点和祖先节点的值
--php->size;
//向下调整
AdjustDown(php->arr, 0 , php->size);
}堆顶一定是最值,大堆堆顶是最大值,小堆堆顶是最小值。
void tes02()
{
HP hp;
HPInit(&hp);
HPPush(&hp, 56);
HPPush(&hp, 10);
HPPush(&hp, 15);
HPPush(&hp, 30);
HPPush(&hp, 70);
HPPush(&hp, 25);
HPPrint(&hp);
while (!HPEmpty(&hp)) //如果堆不为空
{
int top = HPTop(&hp); //循环取堆顶元素
printf("%d ", top); //打印
HPPop(&hp);//出堆顶元素
}
HPDestroy(&hp);
}
此处大堆为例,如果堆不为空,循环取堆顶元素,然后打印,然后出堆(出堆会使根结点和最后一个结点值交换,然后删除最后一个结点,因为是大堆,运用向下调整法又找剩余数据中的最大值,直到所有的数据出堆),那这就是用用堆排序的方法吗? 这并不是堆排序,因为这需要我们自己去构建一个数据结构堆,实现需要运用堆的一般操作(初始化,销毁,入堆,出堆等等)。
答案:数组建堆,⾸尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次⼤的数据(让数组变成一个堆的结构)

1. 建堆操作,向下调整
从最后一棵子树开始调整,然后调整parent--继续调整
//堆排序
void HPSort(int* arr, int n)
{
//建堆,向下调整算法
for (int i = (n - 1 - 1) / 2; i >= 0; i--) //初始值就是最后一棵子树,n-1为最后一个子结点,再减1除2就是其父结点也就最后一棵子树开始
{
AdjustDown(arr,i,n);
}
}2.将堆顶和最后一个数据交换位置,--size,向下调整堆,继续重复该过程

//堆排序
void HPSort(int* arr, int n)
{
//建堆,向下调整算法
for (int i = (n - 1 - 1) / 2; i >= 0; i--) //初始值就是最后一棵子树,n-1为最后一个子结点,再减1除2就是其父结点也就最后一棵子树开始
{
AdjustDown(arr,i,n);
}
//堆排序
int end = n - 1; //最后位置一个结点
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
--end;
}
}
//建堆,向上调整算法
for (int i = 0; i < n; i++)
{
AdjustUp(arr,i);
}总结:排升序:建大堆 排降序:建小堆
计算向上调整算法建堆时间复杂度

计算向下调整算法建堆时间复杂度

TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。 ⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了 (可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:
先创建数据
void CreateNDate()
{
// 造数据
int n = 100000;
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() + i) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}fprintf(fin, "%d\n", x);:
fprintf 是C语言中的一个函数,用于将格式化的数据写入文件。fin 是文件指针,指向要写入的文件。"%d\n" 是格式字符串,表示以十进制整数格式写入数据,后面跟一个换行符。x 是要写入的整数值。fclose(fin);:
fclose 是C语言中用于关闭文件的函数。fin 是要关闭的文件指针。(rand() + i) % 1000000:
rand() 是C语言的随机数生成函数,返回一个伪随机整数。+ i 将循环计数器加到随机数上,增加随机性(避免连续调用rand()产生的相关性)。% 1000000 是取模运算,确保结果在0到999999之间(生成六位数以内的随机数)。这个函数的作用是生成100,000个随机整数(每个在0-999,999之间),每行一个,写入到"data.txt"文件中。
void TopK()
{
int k = 0;
printf("请输入K:");
scanf_s("%d", &k);
const char* file = "data.txt";
FILE* fout = fopen(file,"r");
if (fout == NULL)
{
perror("fopen fail!");
exit(1);
}
//找最大的前K个数据,建小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail!");
exit(2);
}
for (int i = 0; i < k; i++)
{
fscanf_s(fout, "%d",&minHeap[i]);//保存到申请的数组空间
}
//minHeap目前并不是堆结构 所以向下调整建堆(时间复杂度更小)
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap,i,k);
}
//建堆后,遍历剩下的n-k个数据,与堆顶进行比较,大于堆顶元素则替换
int x = 0;//将剩下的数据保存到x里面
while (fscanf_s(fout, "%d", &x) != EOF)
{
//x与堆顶数据比较
if (x > minHeap[0])
{
minHeap[0] = x;//但是此时不是有效堆结构
AdjustDown(minHeap, 0, k);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
}
int main()
{
TopK();
return 0;
}时间复杂度:O(n) = k + (n − k)log2k