手撕数据结构之二叉树_闭包树
1.树
树的基本概念与结构
树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
• 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
• 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm ,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。
下面的A就是根节点,没有前驱节点的
非树形结构:
• ⼦树是不相交的(如果存在相交就是图了,图以后得课程会有讲解)
• 除了根节点外,每个节点有且仅有一个父节点
• 一棵N个节点的数有N-1条边
树相关术语
2.二叉树
•⽗结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点; 如上图:A是B的⽗结点
•⼦结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图:B是A的孩⼦结点•结点的度:⼀个结点有⼏个孩⼦,他的度就是多少;⽐如A的度为6,F的度为2,K的度为0
•树的度:⼀棵树中,最⼤的结点的度称为树的度; 如上图:树的度为 6
•叶⼦结点/终端结点:度为 0 的结点称为叶结点; 如上图: B、C、H、I… 等结点为叶结点
•分⽀结点/⾮终端结点:度不为 0 的结点; 如上图: D、E、F、G… 等结点为分⽀结点
•兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟); 如上图: B、C 是兄弟结点
•结点的层次:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
•树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为 4
•结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先
•路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q
•⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙
•森林:由 m(m>0) 棵互不相交的树的集合称为森林;
树的表示
孩子兄弟表示法:
树结构相对线性表就⽐较复杂了,要存储表⽰起来就⽐较⿇烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表⽰⽅式如:双亲表⽰法,孩⼦表⽰法、孩⼦双亲表⽰法以及孩⼦兄弟表⽰法等。我们这⾥就简单的了解其中最常⽤的孩⼦兄弟表⽰法
struct TreeNode{struct Node* child; // 左边开始的第⼀个孩⼦结点struct Node* brother; // 指向其右边的下⼀个兄弟结点int data; // 结点中的数据域}
我们不用提前知道有多少个节点,通过这种结构体我们就能找到所有的节点
树形结构实际运用场景
⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。
2.二叉树
二叉树是树形结构的一种
在树形结构中,我们最常⽤的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空
从上图可以看出⼆叉树具备以下特点:
-
⼆叉树不存在度⼤于 2 的结点
-
⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树
注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的
满二叉树
⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀个⼆叉树的层数为 K ,且结点总数是 2k - 1 ,则它就是满⼆叉树。
完全二叉树
完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为 K 的,有 n 个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从 1 ⾄ n 的结点⼀⼀对应时称之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。
假设二叉树层次为K
除了第K层外,每层节点的个数达到了最大节点数,第K层节点个数不一定达到最大节点数
对于下面的图,因为完全二叉树节点的顺序是从左到右的,
那么下面的就不是一个完全二叉树
完全二叉树就是从左到右节点依次增加
满二叉树是完全二叉树的一种,但是完全二叉树不一定是满二叉树
当完全二叉树的节点最大时,那么这个完全二叉树就是满二叉树了
满二叉树一定是完全二叉树
但是完全二叉树不一定是满二叉树
二叉树的性质
根据满⼆叉树的特点可知:
1)若规定根结点的层数为 1 ,则⼀棵⾮空⼆叉树的第i层上最多有 2^(i-1) 个结点
2)若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2^h - 1
3)若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 ( log以2为底, n+1 为对数)
对于这个性质三来说的话,2^h-1=n 那么我们就能得到性质三
二叉树存储结构
⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。
顺序结构
顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。
现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储,需要注意的是这⾥的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段
链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。后⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。
3.实现顺序结构二叉树
⼀般堆使⽤顺序结构的数组来存储数据,堆是⼀种特殊的⼆叉树,具有⼆叉树的特性的同时,还具备其他的特性
堆的概念与结构
堆分为小堆和大堆
小堆我们也称之为小根堆
堆具有以下性质:
• 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;
• 堆总是⼀棵完全⼆叉树。
父节点比孩子节点大就是大根堆
子节点比父节点大就是小根堆
小根堆的堆顶是最小值
大根堆的堆顶是最大值
将堆中的数据存储到数组中,数组不一定是有序的,因为数的排序是从左到右的
对于下面的二叉树
对于15,就是这个数组下标为1的数据,根据性质一我们得到(1-1)/2=0
那么这个节点的双亲节点的下标是0,就是上面的10
对于56,就是这个数组下标为2的数据,根据性质一我们得到(2-1)/2=0
那么这个节点的父节点是下标为0的10
对于25,就是这个数下标为3的数据,根据性质一我们得到(3-1)/2=1
那么这个节点的父节点是下标为1的15
所以我们通过子节点下标就能求出父节点
对于父节点:
左节点:2i+1
右节点: 2i+2
如果右节点的2i+2超过了n,那么就越界了
堆的向上调整算法
我们向堆中插入一个数据16,但是插入之后就不是小堆了
所以我们要进行调整了
我们通过(i-1)/2能够找到刚刚插入的16的父节点56
找到之后我们进行两个数据的比较,谁小谁就进行向上调整
那么我们就将16和56交换位置
然后16就从i=6的位置换到i=2的位置,
我们用16找到父节点10
进行两个数据的比较,因为10<16,那么我们就不用做出调整了,最终的结果就是下图
那么这个就是堆的向上调整算法
如果插入的是6的话,那么最后的结果就是
向下调整算法
出堆:删除数据
出的是堆顶的数据,就是下标为0的数据,但是不好删除
但是假如我们删除的是最后一个数据的话就很好解决
直接size--
将70和10的位置进行换一下,然后size--得到了:
那么现在我们只需要将这个堆变成小堆就行了
现在的70是父节点,我们通过2i+1得到左边的子节点,比较左右的两个子节点,挑选出小的,
然后将小的子节点和父节点进行交换,那么最后我们就得到了:
对于25和30来说的话,现在的70是父节点,因为我们的小堆需要父节点小于子节点
那么我们需要挑选出小的子节点和父节点进行交换
那么最后我们就得到了小堆
出堆的过程:先将下标为0的数据和最后一个数据进行交换,然后我们进行size--删除这个要删除的数据
然后我们进行一系列的操作将这个堆变成小堆
那么我们这里用到了向下调整算法
堆排序
排升序--大堆
排降序--小堆
//冒泡排序//时间复杂度为O(N^2)void BubbleSort(int* arr, int n)//数组以及数组中的有效数据个数{ for (int i = 0; i < n; i++) { int exchange = 0; for (int j = 0; j arr[j + 1])//大的在后面,那么我们就进行交换 { exchange = 1; Swap(&arr[j], &arr[j + 1]); } } if (exchange == 0) { //那么就说明我们经历了一趟内循环,我们的exchange并没有改变,就说明这个数组可能之前就是有序的 break; } }}/*向上调整算法的时间复杂度和层次有关的2^k-1=n n是总节点个数那么k=log(n+1)那么向上调整算法的最差的情况是log(N)因为我们向上调整算法在这里的外面还有层循环,那么时间复杂度是Nlog(N)向上调整算法的时间复杂度也是log(N)那么这里的堆排序的时间复杂度是O(n*log n)*///堆排序//期望空间复杂度是O(1) ,不额外的开辟空间//时间复杂度是O()void HeapSort(int* arr, int n){ //根据数组来建堆,那么我们就需要便利数组 //建小堆---降序 for (int i = 0; i 0)//end会一直--,但是end必须大于0 { Swap(&arr[0], &arr[end]);//将第一个数据和最后一个数据进行交换 //那么我们就要对现在的剩下的堆进行调整 //我们采用向下调整的方法 AdhustDown(arr, 0, end);//我们从根进行调整,那么第二个参数就是0 //n-1=end,就是我们只需要对剩下的n-1个数据进行向下调整,那么第三个参数就是我们调整的有效数据n-1个 end--; }}/*我们先用向上调整法将给的数组进行建小堆的操作然后建完小堆之后我们利用循环将堆顶的数据和最后一个数据进行交换循环停止的条件是end>0在循环中,我们先利用交换函数将堆顶数据和堆尾数据进行交换然后利用我们的向下排序的方法进行正确的小堆排序然后进行end--的操作每次我们通过上面的操作就能将堆顶的最小的数据放到后面,然后剩下的就是较大的数字最后我们就实现了降序的操作*/
向上调整法
//堆的向上调整算法//下面的代码是建小堆的//如果我们要建大堆的话,我们需要将循环里面的条件语句的条件进行改变//if (arr[child] > arr[parent])//将大的数据往上放void AdjustUp(HPDataType* arr, int child)//调整的是一个数组,第二个参数是要调整的数据的下标,将孩子往父亲的位置进行调整{ int parent = (child - 1) / 2;//根据孩子求父亲的下标 //不需要等于,child只要走到根节点的位置,根节点没有父节点不需要交换 while (child > 0)//当child arr[parent])//如果孩子比父亲小的话,那么就进行换位置 { //使用交换函数进行数据的交换 Swap(&arr[parent], &arr[child]); child = parent;//进行完上面的交换操作之后,我们之前的child已经变成了parent了 //那么我们接着进行判断,判断现在的位置和父节点的大小, //我们利用现有的child进行父节点的寻找 //现在的child就是之前我们的parent的位置,是下标,child到了parent下标的位置 //我们利用新的child找新的父节点 //child走到parent的位置,parent走到(child - 1) / 2这个位置 parent = (child - 1) / 2; } else//child位置的值比parent位置的值大 { break; //我们不不用调整了,直接跳出 } }}/*进行交换函数之后,我们原先的child变成了parent了,就是位置变了然后我们这个位置的数据相较于上面的数据还是子节点我们要利用这个子节点去寻找上面的父节点child = parent;parent = (child - 1) / 2;这两句代码假如我们插入的数据是6那么我们进行完交换函数之后这个6就是新的parent但是对于现在6的父节点来说,6还是子节点,只不过子节点有了新的位置我们一开始的parent是下标,那么现在的child就是之前我们的parent的位置*/
向下调整法
//向下调整法//如果我们建大堆的话,我们需要将循环内条件语句的条件进行改变//如果右孩子比左孩子要大,我们就进行child++操作//if (child + 1 < n && arr[child] arr[parent])//将大的孩子放上面去void AdhustDown(HPDataType*arr, int parent, int n)//第一个数据是我们要调整的数组,第二个参数是父节点,就是我们开始进行调整的位置的下标对应的元素,第三个是数组中有效的元素个数{ //我们已知Parent,那么我们能够通过2i+1或者2i+2找到这个父节点的左右子节点 int child = parent * 2 + 1;//左孩子 while (child<n)//我们不能让child因为++操作导致越界 { //找左右孩子中最小的那个,我们与父节点进行交换操作 if (child+1<n && arr[child] < arr[child + 1]) { //假设我们的子节点只有一个左节点的话,那么我们就可以通过child+1 arr[parent])//如果子节点小于父节点的话我们就进行交换 { Swap(&arr[child], &arr[parent]); //交换完成之后我们让parent走到child的位置 parent =child ; //child走到当前位置的左孩子的位置 child = parent * 2 + 1; } else//如果Parent比child小的话,那么我们就没必要向下进行调整了 { break;//我们直接跳出 } }}
我们不仅能用向上调整算法建堆,还能用向下调整算法建堆,那么我们该怎么做呢
我们先从i这个位置开始,就是最后一个节点的父节点开始进行向下调整
下面的堆是我们最开始的数组的样子,就是保持无序的状态
进行完操作之后我们让i--,那么i就跑到20的位置了
然后我们让20向下调整,因为子节点都比20小,那么我们就不进行操作了
因为20比17大,那么我们将20往上放,17往下放
然后因为19比17大,那么将17往下放
那么经过这么一系列的操作,我们就得到了一个大堆
那么究竟是向上建堆好还是向下建堆好呢?
关于计算向下和向下两种调整算法建堆时间复杂度
向下调整算法建堆的时间复杂度
那么最后我们得到的
向下调整算法建堆时间复杂度是O(N)
向上调整算法建堆的时间复杂度
总结
向上调整:
节点数量多的层*调整次数多
节点数量少的层*调整的次数少
向下调整:
节点数量多的层*调整次数少
节点数量少的层*调整次数多
建议是使用向下调整算法进行建堆
建堆的时间复杂度是O(N)
下面的调整得物时间复杂度是O(N*logN)
那么堆排序的时间复杂度是O(N*logN)
TOP-K问题
TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。 ⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:
找最大的前k个数据:
取到的数据和堆顶比较,比较堆顶数据,谁大谁就跟堆顶交换(堆向下进行调整)
那么小堆中的k个数据就是N个数据中的最大的前k个数据
我们先拿k个数据进行建堆,那么在建堆操作之后我们要将剩下的N-K个数据拿过来进行向下调整的操作
那么这种排序的时间复杂度就是O(N)
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);}void TOPK(){ int k = 0; printf(\"请输入k:\"); scanf_s(\"%d\", &k); //从文件中读取前k个数据,建堆 const char* file = \"data.txt\"; FILE* fout = fopen(file, \"r\"); if (fout == NULL)//打开失败 { perror(\"fopen fail!\"); exit(1); } //创建一个大小为k的数组,建小堆,找前k个最大的数据 int* minHeap = (int*)malloc(k*sizeof(int)); //往堆内放数据 if (minHeap == NULL) { perror(\"malloc fail!\"); exit(2); } //循环读取文件内的数据 //从文件中读取前K个数据, for (int i = 0; i = 0; i--) { AdhustDown(minHeap, i, k); //我们从最后一个节点的父节点进行向上调整操作 //一开始我们的i就被定义成了这个父节点的下标的大小 } //那么我们建好堆之后我们就接着读取文件中剩下的N-K个数据 //我们进行循环读取 int x = 0;//我们创建一个x变量,将每次读取的数据存在x中与堆顶的数据进行循环比较 while (fscanf(fout,\"%d\",&x)!=EOF)//直到读到文件末尾 { //读取到的数据与堆顶的数据进行比较 //如果比对顶值大,交换入堆 if (x > minHeap[0]) { minHeap[0] = x;//直接将x放到堆顶 //经过上面的操作之后,那么这个堆就不是一个小堆了 //我们利用向下调整算法重新将这个堆变成小堆 AdhustDown(minHeap, 0, k); } } //走到这里我们已经读完了,那么这个堆剩下的数据就是这个文件前K个最大的数据了 for (int i = 0; i < k; i++) { printf(\"%d \", minHeap[i]); } fclose(fout);}
找最小的前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);}void TOPK(){ int k = 0; printf(\"请输入k:\"); scanf_s(\"%d\", &k); //从文件中读取前k个数据,建堆 const char* file = \"data.txt\"; FILE* fout = fopen(file, \"r\"); if (fout == NULL)//打开失败 { perror(\"fopen fail!\"); exit(1); } //创建一个大小为k的数组,建小堆,找前k个最大的数据 int* minHeap = (int*)malloc(k*sizeof(int)); //往堆内放数据 if (minHeap == NULL) { perror(\"malloc fail!\"); exit(2); } //循环读取文件内的数据 //从文件中读取前K个数据, for (int i = 0; i = 0; i--) { AdhustDown(minHeap, i, k); //我们从最后一个节点的父节点进行向上调整操作 //一开始我们的i就被定义成了这个父节点的下标的大小 } //那么我们建好堆之后我们就接着读取文件中剩下的N-K个数据 //我们进行循环读取 int x = 0;//我们创建一个x变量,将每次读取的数据存在x中与堆顶的数据进行循环比较 while (fscanf(fout,\"%d\",&x)!=EOF)//直到读到文件末尾 { //读取到的数据与堆顶的数据进行比较 //如果比对顶值大,交换入堆 if (x < minHeap[0]) { minHeap[0] = x;//直接将x放到堆顶 //经过上面的操作之后,那么这个堆就不是一个小堆了 //我们利用向下调整算法重新将这个堆变成小堆 AdhustDown(minHeap, 0, k); } } //走到这里我们已经读完了,那么这个堆剩下的数据就是这个文件前K个最大的数据了 for (int i = 0; i < k; i++) { printf(\"%d \", minHeap[i]); } fclose(fout);}
两个代码的最大的区别就是如果我们要找前k个最大的数据
那么我们就让这个数和堆顶的数进行比较,如果这个数大于堆顶的数的话,我们就将堆顶数据赋值为这个数
如果我们找前k个最大的数据的话,那么就是相反
谁小就去堆顶
改变这个条件就行了
顺序结构的二叉树的具体实现
Heap.h
#pragma once#include #include #include #include #include //定义堆的结构---底层结构是数组typedef int HPDataType;typedef struct Heap{ int* arr; int size;//有效的数据个数 int capacity;//空间大小}HP;//堆的初始化void HPInit(HP*php);//堆的销毁void HPDestroy(HP* php);//插入数据void HPPush(HP* php, HPDataType x);//删除数据void HPPop(HP* php);//取堆顶数据HPDataType HPTop(HP* php);//将堆顶数据拿出来,但是我们不出堆//判断堆是否为空bool HPEmpty(HP* php);//交换函数void Swap(int* x, int* y);//一定要传地址才能进行交换了//堆的向上调整算法void AdjustUp(HPDataType* arr, int child);//调整的是一个数组,第二个参数是要调整的数据的下标,将孩子往父亲的位置进行调整//向下调整法void AdhustDown(HPDataType* arr, int parent, int n);
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1#include \"Heap.h\"//堆的初始化void HPInit(HP* php){ assert(php); php->arr = NULL; php->size = php->capacity = 0;}//堆的销毁void HPDestroy(HP* php){ assert(php); if (php->arr)//不为空的话我们就进行销毁操作 { free(php->arr); } php->arr = NULL; php->size = php->capacity = 0;}//交换函数void Swap(int* x, int* y)//一定要传地址才能进行交换了{ int tmp = *x; *x = *y; *y = tmp;}//堆的向上调整算法//下面的代码是建小堆的//如果我们要建大堆的话,我们需要将循环里面的条件语句的条件进行改变//if (arr[child] > arr[parent])//将大的数据往上放void AdjustUp(HPDataType* arr, int child)//调整的是一个数组,第二个参数是要调整的数据的下标,将孩子往父亲的位置进行调整{ int parent = (child - 1) / 2;//根据孩子求父亲的下标 //不需要等于,child只要走到根节点的位置,根节点没有父节点不需要交换 while (child > 0)//当child<0我们就停止向上调整了,因为上面就没有父节点了 { if (arr[child] size == php->capacity)//说明空间是不够的 { //空间不够我们就进行扩容操作 int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity; HPDataType* tmp = (HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType)); if (tmp == NULL) { perror(\"ralloc,fail!\"); exit(1); } //申请成功 php->arr = tmp;//将申请到的空间给数组 php->capacity = newcapacity;//将capacity进行更新 } //插入数据 php->arr[php->size] = x; //一开始的size是0,那么我们就往数组中下标为0的位置进行插入数据 //我们要进行堆的向上调整,保证这个堆是小堆 //我们这里是一个个数据进行插入的,所以一开始的size是0 AdjustUp(php->arr, php->size); php->size++; //插入完数据之后我们需要进行size++操作,因为我们多了一个有效数据}//向下调整法//如果我们建大堆的话,我们需要将循环内条件语句的条件进行改变//如果右孩子比左孩子要大,我们就进行child++操作//if (child + 1 < n && arr[child] arr[parent])//将大的孩子放上面去void AdhustDown(HPDataType*arr, int parent, int n)//第一个数据是我们要调整的数组,第二个参数是父节点,就是我们开始进行调整的位置的下标对应的元素,第三个是数组中有效的元素个数{ //我们已知Parent,那么我们能够通过2i+1或者2i+2找到这个父节点的左右子节点 int child = parent * 2 + 1;//左孩子 while (child<n)//我们不能让child因为++操作导致越界 { //找左右孩子中最小的那个,我们与父节点进行交换操作 if (child + 1 arr[child + 1]) { //假设我们的子节点只有一个左节点的话,那么我们就可以通过child+1<n //这个条件避免child++,避免了越界访问,我们直接让左节点和父节点进行交换 //child+1必须小于n我们才能往下进行调整操作,避免越界访问 //假设左边的是56,右边的是15 child++;//我们进行child++操作,然后child就跑到了15的位置,然后我们将15和父节点进行交换 } if (arr[child] size);//传的数据不能为0并且数组内有效的数据要大于0我们才能进行删除操作 //现在第一步我们将堆顶的数据和最后一个数据进行交换 Swap(&php->arr[0], &php->arr[php->size - 1]); //那么我们现在就已将将原先的堆顶数据删除了 //下面我们就需要解决将交换数据后的堆变成一个有效的小堆 php->size--; //我们现在进行堆顶的向下调整操作 AdhustDown(php->arr, 0,php->size);//我们调整的是这个数组,我们从堆顶开始进行调整,堆顶对应的下标数据为0,我们还要将堆里面的有效数据个数传过去}//判断堆是否为空bool HPEmpty(HP* php){ assert(php); return php->size == 0;//如果size是0的话,那么这个堆就是空的,返回值就是true}//取堆顶数据HPDataType HPTop(HP* php){ assert(php&&php->size);//传过来的数据不能为空并且堆里面要有数据存在 return php->arr[0];//直接返回堆顶元素}
test.c
#define _CRT_SECURE_NO_WARNINGS 1#include \"Heap.h\"//冒泡排序//时间复杂度为O(N^2)void BubbleSort(int* arr, int n)//数组以及数组中的有效数据个数{ for (int i = 0; i < n; i++) { int exchange = 0; for (int j = 0; j arr[j + 1])//大的在后面,那么我们就进行交换 { exchange = 1; Swap(&arr[j], &arr[j + 1]); } } if (exchange == 0) { //那么就说明我们经历了一趟内循环,我们的exchange并没有改变,就说明这个数组可能之前就是有序的 break; } }}/*向上调整算法的时间复杂度和层次有关的2^k-1=n n是总节点个数那么k=log(n+1)那么向上调整算法的最差的情况是log(N)因为我们向上调整算法在这里的外面还有层循环,那么时间复杂度是Nlog(N)向上调整算法的时间复杂度也是log(N)那么这里的堆排序的时间复杂度是O(n*log n)*///堆排序//期望空间复杂度是O(1) ,不额外的开辟空间//时间复杂度是O()void HeapSort(int* arr, int n){ //根据数组来建堆,那么我们就需要便利数组 //建小堆---降序 // 向上调整算法建堆 //for (int i = 0; i = 0; i--) { AdhustDown(arr, i, n);//我们从i这个位置开始向下调整 //i一开始的位置就是最后一个数据的父节点,我们从这个父节点开始向下操作 } //循环将堆顶数据跟最后位置的数据(会变化,每次减小一个数据)进行交换 int end = n - 1;//n是数组的有效个数,那么最后一个数据的下标是n-1 while (end>0)//end会一直--,但是end必须大于0 { Swap(&arr[0], &arr[end]);//将第一个数据和最后一个数据进行交换 //那么我们就要对现在的剩下的堆进行调整 //我们采用向下调整的方法 AdhustDown(arr, 0, end);//我们从根进行调整,那么第二个参数就是0 //n-1=end,就是我们只需要对剩下的n-1个数据进行向下调整,那么第三个参数就是我们调整的有效数据n-1个 end--; }}/*我们先用向上调整法将给的数组进行建小堆的操作然后建完小堆之后我们利用循环将堆顶的数据和最后一个数据进行交换循环停止的条件是end>0在循环中,我们先利用交换函数将堆顶数据和堆尾数据进行交换然后利用我们的向下排序的方法进行正确的小堆排序然后进行end--的操作每次我们通过上面的操作就能将堆顶的最小的数据放到后面,然后剩下的就是较大的数字最后我们就实现了降序的操作*/void test01(){ HP hp;//创建变量 //初始化 HPInit(&hp); int arr[] = { 17,20,10,13,19,15 }; for (int i = 0; i < 6; i++) { //将这个数组内的数据循环入堆 HPPush(&hp, arr[i]); } //删除数据(出堆) //HPPop(&hp); while (!HPEmpty(&hp))//堆不为空的话,那么我们就打印堆顶数据 { printf(\"%d \", HPTop(&hp)); HPPop(&hp);//删除堆顶数据,换下一个数据 } //销毁 HPDestroy(&hp);}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);}void TOPK(){ int k = 0; printf(\"请输入k:\"); scanf_s(\"%d\", &k); //从文件中读取前k个数据,建堆 const char* file = \"data.txt\"; FILE* fout = fopen(file, \"r\"); if (fout == NULL)//打开失败 { perror(\"fopen fail!\"); exit(1); } //创建一个大小为k的数组,建小堆,找前k个最大的数据 int* minHeap = (int*)malloc(k*sizeof(int)); //往堆内放数据 if (minHeap == NULL) { perror(\"malloc fail!\"); exit(2); } //循环读取文件内的数据 //从文件中读取前K个数据, for (int i = 0; i = 0; i--) { AdhustDown(minHeap, i, k); //我们从最后一个节点的父节点进行向上调整操作 //一开始我们的i就被定义成了这个父节点的下标的大小 } //那么我们建好堆之后我们就接着读取文件中剩下的N-K个数据 //我们进行循环读取 int x = 0;//我们创建一个x变量,将每次读取的数据存在x中与堆顶的数据进行循环比较 while (fscanf(fout,\"%d\",&x)!=EOF)//直到读到文件末尾 { //读取到的数据与堆顶的数据进行比较 //如果比对顶值大,交换入堆 if (x < minHeap[0]) { minHeap[0] = x;//直接将x放到堆顶 //经过上面的操作之后,那么这个堆就不是一个小堆了 //我们利用向下调整算法重新将这个堆变成小堆 AdhustDown(minHeap, 0, k); } } //走到这里我们已经读完了,那么这个堆剩下的数据就是这个文件前K个最大的数据了 for (int i = 0; i < k; i++) { printf(\"%d \", minHeap[i]); } fclose(fout);}int main(){ //test01(); //给定一个数组,对数组中的数据进行排序 //int arr[] = { 17,20,10,13,19,15 }; ////BubbleSort(arr, 6);//冒泡排序 //HeapSort(arr, 6);//堆排序 //for (int i = 0; i < 6; i++) //{ // // printf(\"%d \",arr[i]); //} //printf(\"\\n\"); //CreateNDate();//创建一个有10w个数据的文件 TOPK(); return 0;}
4.实现链式结构二叉树
遍历规则
按照规则,⼆叉树的遍历有:前序/中序/后序的递归结构遍历:
1)前序遍历(Preorder Traversal 亦称先序遍历):访问根结点的操作发⽣在遍历其左右⼦树之前
访问顺序为:根结点、左⼦树、右⼦树
2)中序遍历(Inorder Traversal):访问根结点的操作发⽣在遍历其左右⼦树之中(间)
访问顺序为:左⼦树、根结点、右⼦树
3)后序遍历(Postorder Traversal):访问根结点的操作发⽣在遍历其左右⼦树之后
访问顺序为:左⼦树、右⼦树、根结点
遍历规则解释:
前序遍历:
一开始遍历的是1,那么我们遍历完1之后,进行1的打印,然后箭头移动,箭头指向了2,打印了2
因为2是个头节点,那么我们继续往下遍历,箭头指向了4,那么打印了4,最后箭头指向了3,打印了3,规则就是先根再左右
最后打印的就是1 2 4 3
中序遍历:
我们遍历的顺序是左子树、根节点、右子树
那么我们先遍历左子树,那么箭头指向了2,那么对于2来说,4是左子树,那么箭头来到了4
那么我们将4打印,那么对于4的话,已经没有子节点了,那么箭头就回到了2
我们打印根节点2,打印完这个根节点,我们的箭头就应该指向以2为顶点的树的右子树
但是2没有右子树
所以我们的箭头回到了1,对于1这个根节点来说,左子树已经打印完了,我们就该开始打印根节点1了,打印完根节点之后我们就开始打印右节点了,那么箭头就指向了3了
那么最后打印的就是4 2 1 3
后序遍历:
后续遍历的顺序是左子树、右子树、根节点
一开始的箭头指向1,2是1的左子树,因为4是2的左子树,那么箭头就来到了4的位置,将4进行打印
因为2没有右子树,那么我们的箭头就直接跳到了2这个根节点的位置了,将2进行打印
对于1这颗二叉树来说的话,我们的左子树已经全部打印完成了,那么就开始右子树的打印了,箭头指向了3的位置,将3打印完成之后,我们的箭头回到根节点1的位置,最后打印根节点
那么最后打印的就是4 2 3 1
对于这个二叉树来说的话
前序遍历:1 2 4 6 5 3
中序遍历:4 6 2 5 1 3
后续遍历:6 4 5 2 3 1
解释前序遍历的代码中的递归
我们用这张图进行解释
一开始我们的的root节点是1,判断节点不为空,那么我们就进行条件语句下面的代码
我们将root节点内的数据打印,就是1
然后我们进入到了root的左节点的递归里面了,root→left是2
当2是root节点时
我们先判断传过来的root不是空节点
那么我们将2进行打印,然后右进入到2的子节点的递归中,子节点就是4
当4是root节点时
我们先判断传过来的root是不是空的,然后我们进行打印4
我们进入到了root→left
发现4没有左节点,那么传的就是空值,我们直接退出
4同样没有右节点,我们依然退出
那么我们就原路返回了
对于root=2时,我们的左节点递归已经结束了,那么我们进行2的右节点递归,但是2的右节点是空的,所以我们直接退出
那么2这个树我们已经递归完成了
我们就回到了1是root了
对于1来说,左节点已经递归完了,那么就进行右节点的递归了
就是3
当3是root的时候,我们将3进行打印,打印完进入左递归,是空值,返回
然后进入右递归,是空值,返回
那么最后我们打印的就是1 2 4 3
整个递归的流程图如下:
第四种遍历方法:层序遍历\\
对于这么一个二叉树来说,我们通过层序遍历最后得到的是1 2 3 4
这里我们是不能通过递归来实现的
但是我们可以借助队列来实现这么一个结构
队列的结构是先进先出
恰好我们队列的底层结构就是链表来实现的,和这里的链式二叉树一样的
我们将之前写的Queue.c文件和Queue.h文件复制到我们链式二叉树的文件夹里面
typedef struct BinaryTreeNode* QDataType;
struct BinaryTreeNode*这个就是我们要保存在队列中的数据类型
之前的在队列中保存的数据类型是int类型
队列中存储的是堆节点的地址
那么存储的就是节点的指针,那么我们将这个类型重新定义
//层序遍历---借助数据结构(队列)void Levelorder(BTNode* root){ //创建一个队列结构 Queue q; QueueInit(&q);//进行初始化 //第一步:让根节点直接入队列 QueuePush(&q,root);//往队列中push根节点 while (!Queuempty(&q))//只要队列不为空,我们就一直取队头数据 { //取队头数据 BTNode*front=QueueFront(&q);//将队头取出,返回值是节点的地址 //打印对头 printf(\"%d \", front->data); //让队头出队列 QueuePop(&q);//那么此时的队列是空的 //将队头节点的左右孩子入队列 if (front->left)//如果这个节点的左孩子不是空的,那么我们将这个节点往队列中入 { QueuePush(&q, front->left); } if (front->right)//如果这个节点的右孩子不是空的,那么我们将这个节点往队列中入 { QueuePush(&q, front->right); } } QueueDestroy(&q);//销毁}
判断是否为完全二叉树
除了最后一层,其它层的节点数达到最大,那么这个数就是完全二叉树
这里涉及到每层,那么这里还是要借助队列这么一个结构的
我们先取根节点放进队列,队列不为空,我们将队列中的节点取出来
然后将这个节点的左右孩子一起放进队列中去
我们再将队头取出,将队头的左右孩子放进去,如果孩子为空放个NULL进去
如果我们队列剩下的都是NULL的话,我们将队列中第一个NULL取出
那么我们取到的数据为空,取到为空的情况我们就跳出循环
那么最后就是这么个情况
如果我们的二叉树现在是非完全二叉树是个什么情况呢?
那么会有很明显的不同的
如果是完全二叉树,跳出第一个循环之后,队列中全是NULL节点
如果不是完全二叉树,跳出一个循环之后,那么队列还有一个非空节点
如果是上面这张图的话,4是2的右节点,那么这个数就不是一个完全二叉树,因为没有遵循从左到右的原则
但是如果4是2的左节点的话,那么这个数就是一个完全二叉树
二叉树链式结构的实现
这里我们用到了队列里面的相关知识
test.c
#define _CRT_SECURE_NO_WARNINGS 1#include\"tree.h\"BTNode* buyNode(BTDataType x)//创建一个节点{ BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror(\"malloc fail!\"); exit(1); } newnode->data = x; newnode->left = newnode->right = NULL; return newnode;}void test01(){ BTNode* node1 = buyNode(1); BTNode* node2 = buyNode(2); BTNode* node3 = buyNode(3); BTNode* node4 = buyNode(4); /*BTNode* node5 = buyNode(5); BTNode* node6 = buyNode(6);*/ node1->left = node2; node1->right = node3; node2->left = node4; /*node2->right = node5; node3->left = node6;*/ ////前序遍历 //PreOrder(node1);// 1 2 4 3 //printf(\"\\n\"); ////中序遍历 //InOrder(node1);// 4 2 1 3 ////二叉树节点的个数 //printf(\"size:%d\\n\", BinaryTreeSize(node1));//4 ////printf(\"size:%d\\n\", BinaryTreeSize(node2));//2 ////二叉树叶子结点的个数 //printf(\"leaf size:%d\\n\", BinaryTreeLeafSize(node1)); ////第k层节点的个数 //printf(\"k size:%d\\n\", BinaryTreeLevelKSize(node1, 3)); ////二叉树的高度 //printf(\"depth/height :%d\\n\", BinaryTreeDepth(node1)); ////查找值为x的节点 //BTNode*find=BinaryTreeFind(node1, 5); //printf(\"%s\\n\", find == NULL ? \"没找到\":\"找到了\"); //层序遍历 /*Levelorder(node1);*/ //判断是否为完全二叉树 bool ret=BinaryTreeComplete(node1); printf(\"%s\\n\", ret == false ?\"不是完全二叉树\" : \"是完全二叉树\"); //二叉树的销毁 BinaryTreeDestory(&node1);}int main( ){ test01(); return 0;}
tree.c
#define _CRT_SECURE_NO_WARNINGS 1#include\"tree.h\"#include\"Queue.h\"//前序遍历 根左右void PreOrder(BTNode* root)//我们传个根节点就行了,我们是从根节点开始遍历的{ if (root == NULL) { return; } printf(\"%d \", root->data); PreOrder(root->left);//遍历左子树 PreOrder(root->right);//遍历右子树}//中序遍历 左根右void InOrder(BTNode* root){ if (root == NULL) { return; } InOrder(root->left); printf(\"%d \", root->data); InOrder(root->right);}//后序遍历 左右根void PostOrder(BTNode* root){ if (root == NULL) { return; } PostOrder(root->left); PostOrder(root->right); printf(\"%d \", root->data);}// ?叉树结点个数//第一种方法(不可行)//int size = 0;//void BinaryTreeSize(BTNode* root)//{// if (root == NULL)// {// return 0;// }// //说明当前节点不为空// size++;// //遍历整个二叉树的 左右节点// BinaryTreeSize(root->left);// BinaryTreeSize(root->right);// return size;// //}//////第二种方法(不可行,有弊端,每次调用函数要提前将size置为0)//void BinaryTreeSize(BTNode* root,int *psize)//{// if (root == NULL)// {// return 0;// }// //说明当前节点不为空// (*psize)++;// //遍历整个二叉树的 左右节点// BinaryTreeSize(root->left,psize);// BinaryTreeSize(root->right,psize);// // //不是空节点点我们就进行计数//// /*// * 我们的想法(错的)// 如果我们在函数内设置size的话,我们在进行完1 2 4 3 这个堆的时候// 我们递归完左子树,size++已将isze变为3了//// 上面这种想法就是错的// 但是当我们开始递归1的右子树的时候,我们传的size仍然是1,// // 解释为什么:// 因为我们传的wsize是值,不是地址// 递归函数内的size++并不能将size的大小直接改变// 所以我们在右子树的递归的时候我们的size就是1//// 因为我们在每个递归中的size都是传的值,所以我们不能将size进行改变// */// /*// ,假设我们每次传的是值的话,我们将这块地址取出来,这块地址里面存的就是size,我们进行++// 不管是在左子树还是右子树的递归中,我们每次调用的时候,都要先取这个地址里面的数据// 这个数据是可以进行改变的// // 此次递归的psize地址指向的size的大小就是上次递归的size加加后的大小// */// //但是这种方法有个弊端,每次我们在调用求节点个数的这个函数的时候// //我们都要将size重新置为0//}// 二叉树结点个数//第三种方法:int BinaryTreeSize(BTNode* root){ if (root == NULL) { return 0; } return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);}/*堆:1 2 4 34的左子树和右子树都是0,那么我们就return 1 + 0+ 0回到2,因为2的左节点4返回的指是1,但是没有右节点,所以右节点返回的是0那么对于2的话,return 1+ 1 +0对于1的话,左节点2的返回值是2,1的右节点3的返回值因为没有左右子节点所以3的返回值是1那么1的返回值就是1+2+1=4那么这个4就是我们这个堆的节点数*/// ?叉树叶?结点个数//所谓的叶子节点就是没有子节点的节点int BinaryTreeLeafSize(BTNode* root){ if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL)//说明这个节点就是叶子节点,没有子节点 { return 1; } //进行递归 return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right); //直接将左子树的叶子结点的个数和右子树叶子节点的个数进行返回}// ?叉树第k层结点个数//左子树第k层节点的个数+右子树第k层节点个数int BinaryTreeLevelKSize(BTNode* root, int k){ //递归一定要有结束条件 if (root == NULL) { return 0; } //判断是不是第k层 if (k == 1)//就是第k层 { return 1; } return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);}//这里我们采用的是传值操作,左右子树的操作不会因为k受到影响//?叉树的深度/?度int BinaryTreeDepth(BTNode* root){ if (root == NULL) { return 0; } //当前节点不为空,那么我们对左子树和右子树进行遍历 int leftDep=BinaryTreeDepth(root->left); int rightDep = BinaryTreeDepth(root->right); return leftDep > rightDep ? leftDep + 1 : rightDep + 1; //因为是个递归代码,我们从左后一个节点进行思考 /* 1 2 4 3 对于左子树的话,4是最后一个节点 那么我们从4开始 因为左右节点都是空的,那么左节点和右节点的大小都是0 那么在返回的时候对于2的话,4这个节点返回了1 所以2的左节点返回值是1,因为右节点是空,返回0 那么对于1的话,2这个节点的返回值是1+1 对于1的话,左节点返回值是2,右节点是3 因为3这个节点的左右节点都是空,那么就返回1 那么因为左节点返回的是2 右节点返回的是1 2>1 所以对于节点1的话,返回的值是1+2=3 那么最终的话我们的层数是3层 */} // ?叉树查找值为x的结点//找到对应节点就返回这个节点,没有找到就返回空BTNode* BinaryTreeFind(BTNode* root, BTDataType x){ //要么出现在左子树,要么出现在右子树 if (root == NULL) { return 0; } if (root->data == x) { return root; } //走到这这里说明,root不为空,值不为x,那么我们继续进行递归 BTNode* leftFind=BinaryTreeFind(root->left, x); if (leftFind)//在左子树中找到了这个节点,我们就直接返回,就不用在右子树中进行寻找了 { return leftFind; } BTNode* rightFind = BinaryTreeFind(root->right, x); if (rightFind)//在左子树中找到了这个节点 { return rightFind; } //左子树和右子树都没找到,那么我们直接返回NULL return NULL;}// ?叉树销毁void BinaryTreeDestory(BTNode** root)//传来的是root的地址,我们用二级指针进行接收{ //销毁一个堆的话,我们要先将左子树和右子树先销毁,最后再将根进行销毁 if (*root == NULL)//已经是空的,就没必要进行销毁了 { return; } BinaryTreeDestory(&((*root)->left));//对*root进行解引用我们才能找到left,这里的root是二级指针 //*root指向第一个节点的指针,再取成员left,然后对整体取地址 //上面的是左子树 BinaryTreeDestory(&((*root)->right));///右子树 //销毁根节点 free(*root); *root = NULL; /* 堆:1 2 4 3 当节点在4的时候,我们对4的左节点和右节点都进行递归了 因为都是空的,我们返回到了4这个节点,然后将4这个节点销毁了 销毁完就回到了2这个节点,因为2的左节点已经销毁了 那么就进行2的右节点的销毁了,右节点是空的,所以返回的是空 那么我们就将2销毁了 2销毁完就回到1这个节点了 对于1这个节点来说的话,左节点2已经销毁完了 那么我们的头结点就跑到了1的右节点3那里了 对于3这个头结点来说,左右节点都是空的,那么就直接返回,那么就进行3这个节点的销毁了 那么对于1的话,左右节点都销毁了 那么就将销毁1了 */}//层序遍历---借助数据结构(队列)void Levelorder(BTNode* root){ //创建一个队列结构 Queue q; QueueInit(&q);//进行初始化 //第一步:让根节点直接入队列 QueuePush(&q,root);//往队列中push根节点 while (!Queuempty(&q))//只要队列不为空,我们就一直取队头数据 { //取队头数据 BTNode*front=QueueFront(&q);//将队头取出,返回值是节点的地址 //打印对头 printf(\"%d \", front->data); //让队头出队列 QueuePop(&q);//那么此时的队列是空的 //将队头节点的左右孩子入队列 if (front->left)//如果这个节点的左孩子不是空的,那么我们将这个节点往队列中入 QueuePush(&q, front->left); { } if (front->right)//如果这个节点的右孩子不是空的,那么我们将这个节点往队列中入 { QueuePush(&q, front->right); } } QueueDestroy(&q);//销毁}//判断二叉树是否为完全二叉树//借助队列bool BinaryTreeComplete(BTNode* root){ Queue q; QueueInit(&q);//进行初始化 //将二叉树根节点入队列 QueuePush(&q, root);//往队列中push根节点 while (!Queuempty(&q))//队列不为空,我们就循环取队头 { BTNode* front = QueueFront(&q);//将队头取出 QueuePop(&q);//让队头出队,保证下次取到的是最新的队头数据 //如果我们取到的front是空的话 if (front == NULL)//如果此时的队头是空的,那么我们就取不到左右孩子 { break; } //将队头的左右孩子取出放到队列中 QueuePush(&q, front->left); QueuePush(&q, front->right); } //此时队列不一定为空 while (!Queuempty(&q))//只要队列不为空 { BTNode* front = QueueFront(&q);//取队头 QueuePop(&q);//出队列 //如果我们在队列中剩下的节点中遇到了节点的话,那么这个树就不是一个完全二叉树了 if (front != NULL) { QueueDestroy(&q);//销毁 return false; } } QueueDestroy(&q);//销毁 //到这里就说明没有找到完全二叉树 return true;}/*如果是完全二叉树,跳出第一个循环之后,队列中全是NULL节点如果不是完全二叉树,跳出一个循环之后,那么队列还有一个非空节点*/
tree.h
#pragma once#include #include #include #include //定义二叉树的链式结构//二叉树节点的结构typedef int BTDataType;typedef struct BinaryTreeNode{ BTDataType data; struct BinaryTreeNode* left;//指向左孩子节点的指针 struct BinaryTreeNode* right;//指向右孩子节点的指针}BTNode;//前序遍历void PreOrder(BTNode*root);//我们传个根节点就行了,我们是从根节点开始遍历的//中序遍历void InOrder(BTNode* root);//后序遍历void PostOrder(BTNode* root);// ⼆叉树结点个数int BinaryTreeSize(BTNode* root);// ⼆叉树结点个数//void BinaryTreeSize(BTNode* root, int* psize);// ⼆叉树叶⼦结点个数int BinaryTreeLeafSize(BTNode* root);// ⼆叉树第k层结点个数int BinaryTreeLevelKSize(BTNode* root, int k);//⼆叉树的深度/⾼度int BinaryTreeDepth(BTNode* root);// ⼆叉树查找值为x的结点BTNode* BinaryTreeFind(BTNode* root, BTDataType x);// ⼆叉树销毁void BinaryTreeDestory(BTNode** root);//层序遍历void Levelorder(BTNode* root);//判断二叉树是否为完全二叉树bool BinaryTreeComplete(BTNode* root);
Queue.c
#define _CRT_SECURE_NO_WARNINGS 1#include\"Queue.h\"//初始化void QueueInit(Queue* pq){ assert(pq);//传过来的不能是空指针 pq->phead = pq->ptail = NULL;//空的队列 pq->size = 0;}//判断队列是否为空bool Queuempty(Queue* pq){ assert(pq); return pq->phead == NULL && pq->ptail == NULL; //如果后面的表达式成立,那么就是真,返回的是true //就是说如果这里的是空队列的话,那么就返回的是true}//入队列,队尾 插入数据void QueuePush(Queue* pq, QDataType x){ assert(pq); //申请新节点 QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));//申请一个节点大小的空间 if (newnode == NULL) { perror(\"malloc dail!\"); exit(1); } //对newnode进行初始化操作 newnode->data = x; newnode->next = NULL; if (pq->phead == NULL)//说明队列为空 { pq->phead = pq->ptail = newnode;//那么此时的newnode不仅是头结点也是尾节点 } else//队列不为空 { pq->ptail->next = newnode; //那么此时的newnode 就是新的ptail pq->ptail = newnode; } pq->size++;}//出队列,队头 删除数据 从头结点开始删除数据void QueuePop(Queue* pq){ assert(pq); //队列为空(不可删除数据,因为没有数据) //队列不为空(可删除数据) assert(!Queuempty(pq));//队列为空白的话就报错 //处理只有一个节点的情况,避免ptail变成野指针 //判断只有一个节点的情况 if (pq->ptail == pq->phead)//头尾指针相同,说明只有一个节点 { free(pq->phead);//随便释放 pq->phead = pq->ptail = NULL; } else//处理多个节点的情况 { //删除队头元素 //那么我们现将下个节点的位置进行保存 QueueNode* next = pq->phead->next; //存储好之后我们直接将头结点进行释放 free(pq->phead); pq->phead = next;//那么之前存的next就是新的头结点了 } pq->size--;}//取队头数据QDataType QueueFront(Queue* pq)//返回队头数据{ assert(pq); assert(!Queuempty(pq));//队列不为空 return pq->phead->data;//将队头里面的数据直接返回就行了}//取队尾数据QDataType QueueBack(Queue* pq){ assert(pq); assert(!Queuempty(pq));//队列不为空 return pq->ptail->data;}//队列有效元素个数int QueueSize(Queue* pq){ assert(pq); //下面这种遍历的话效率太低了 //int size = 0; ////定义一个指针进行遍历 //QueueNode* pcur = pq->phead;//指向队列的头结点 //while (pcur)//pcur不为空就往后走 //{ // size++; // pcur = pcur->next; //} //return size; return pq->size;}//队列的销毁void QueueDestroy(Queue* pq){ assert(pq); //assert(!Queuempty(pq));//队列不为空 //遍历 QueueNode* pcur = pq->phead; while (pcur) { //销毁之前先把下个节点进行保存 QueueNode* next = pcur -> next; free(pcur); //将Pcur销毁之后,那么之前保存的next就是新的头结点 pcur = next; } pq->phead = pq->ptail = NULL; pq->size = 0;}
Queue.h
#pragma once#include #include#include#include//定义队列结构//typedef int QDataType;typedef struct BinaryTreeNode* QDataType;//struct BinaryTreeNode*这个就是我们要保存在队列中的数据类型//struct BinaryTreeNode*这个是个数据类型,和之前的//之前的在队列中保存的数据类型是int类型//因为我们在tree.h中将结构体类型重命名了//那么我们可以这么写//typedef struct BTNode* QDataType;typedef struct QueueNode{ QDataType data; struct QueueNode* next;}QueueNode;typedef struct Queue{ QueueNode*phead;//指向头节点的指针---队头--删除数据 QueueNode*ptail;//指向尾节点的指针---队尾--插入数据 int size;//保存队列有效个数}Queue ;//初始化void QueueInit(Queue* pq);//入队列,队尾 插入数据void QueuePush(Queue* pq, QDataType x);//出队列,队头 删除数据void QueuePop(Queue* pq);//判断队列是否为空bool Queuempty(Queue* pq);//取队头数据QDataType QueueFront(Queue* pq);//取队尾数据QDataType QueueBack(Queue* pq);//队列有效元素个数int QueueSize(Queue* pq);//队列的销毁void QueueDestroy(Queue* pq);
5.二叉树算法题
1.单值二叉树
/** * Definition for a binary tree node.定义好的二叉树节 * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ /*思路: 递归 我们用根节点先与左节点进行比较,如果相同的话 我们进行根节点和有几点的比较 如果不行同的话,就return false 如果相同的话,我们就递归下去 */ //递归是要存在结束条件的 typedef struct TreeNode TreeNode;bool isUnivalTree(struct TreeNode* root){ //递归是要存在结束条件的 if(root==NULL) { return true; } //说明此时root不为空,那么我们进行左孩子和右孩子的比较 if(root->left&&root->left->val!=root->val) { //如果左孩子存在并且左孩子里面的值与根节点的值不等 return false; } //走到这里说明根节点的值和左孩子的值是相同的 if(root->right&&root->right->val!=root->val) { //如果右孩子存在并且左孩子里面的值与根节点的值不等 return false; } //走到这里说明根节点的值和左右孩子节点的值相同 //那么我们接着递归这整个二叉树的左子树和右子树 //左子树和右子树都满足条件的话那么这个树就是单值二叉树了 return isUnivalTree(root->left)&&isUnivalTree(root->right);}
2.相同的树
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ //不仅要判断节点值相不相等,而且还要判断两个二叉树的结构是不是一样的 //这里我们不需要借助数组,也不需要借助数列 //我们采用递归的方式 //两棵树都为空树的话,那么我们返回true//如果其中一个树为空树的话,那么就不相同//想要判断两个二叉树是不是相同的树,那么我们就要进行同步递归typedef struct TreeNode TreeNode;bool isSameTree(struct TreeNode* p, struct TreeNode* q) //参数是指向两个二叉树根节点的指针{ //判断是否为空树 if(p==NULL&&q==NULL) { return true;//两个树都是空的,那么我们直接返回true } //或者其中一个为空 if(p==NULL||q==NULL) { return false; } //走到这里就说明都不为空,就说明整体结构是相同的,那么我们下面就对节点的数据进行判断是否相同 if(p->val!=q->val) { return false; } //走到这里说明两个二叉树结构相同,值也相同 //那么我们就递归这两个二叉树的左子树和右子树 return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);//只要前面的左子树返回的是true,那么我们才能进行后面的右子树的判断}
我们先判断两个数的结构是否相同
再判断节点的值是否相同
3.对称二叉树
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ bool isSameTree(struct TreeNode* p, struct TreeNode* q) //参数是指向两个二叉树根节点的指针{ //判断是否为空树 if(p==NULL&&q==NULL) { return true;//两个树都是空的,那么我们直接返回true } //或者其中一个为空 if(p==NULL||q==NULL) { return false; } //走到这里就说明都不为空,就说明整体结构是相同的,那么我们下面就对节点的数据进行判断是否相同 if(p->val!=q->val) { return false; } //走到这里说明两个二叉树结构相同,值也相同 //那么我们就递归这两个二叉树的左子树和右子树 return isSameTree(p->left,q->right)&&isSameTree(p->right,q->left);//只要前面的左子树返回的是true,那么我们才能进行后面的右子树的判断}bool isSymmetric(struct TreeNode* root){ //将这个二叉树看成是两棵树 return isSameTree(root->left,root->right);}
我们将这么一棵二叉树看成是两个数,左子树和右子树
我们先调用上面相同树的代码
判断这两个数是否为空
然后因为是对称的,所以我们判断左子树的左节点和右子树的右节点
以及右子树的左节点和左子树的右节点是否相等
满足了这么几个条件那么这个二叉树就是对称二叉树了
4.另一棵树的子树
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */typedef struct TreeNode TreeNode;bool isSameTree(struct TreeNode* p, struct TreeNode* q) //参数是指向两个二叉树根节点的指针{ //判断是否为空树 if(p==NULL&&q==NULL) { return true;//两个树都是空的,那么我们直接返回true } //或者其中一个为空 if(p==NULL||q==NULL) { return false; } //走到这里就说明都不为空,就说明整体结构是相同的,那么我们下面就对节点的数据进行判断是否相同 if(p->val!=q->val) { return false; } //走到这里说明两个二叉树结构相同,值也相同 //那么我们就递归这两个二叉树的左子树和右子树 return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);//只要前面的左子树返回的是true,那么我们才能进行后面的右子树的判断}bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){ if(root==NULL)//如果root是空树的话,那么我们就没必要进行比较了 { return false; } if(isSameTree(root,subRoot))//判断这两个树是不是相同的,根据一开始的根节点进行判断 { return true; } //走到这里说明root和subRoot不是相同的树 //那么我们还要递归root的左子树是不是和subRoot相等的 //如果不相等的话,我们还要递归root的右子树 return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);//我们这里用的是或,因为假如的话我们递归左子树的时候就判断出相等,那么就没必要递归右子树了//但是左子树不相等的话,那么我们就进行右子树的递归}/*我们先判断的是根节点和subRoot是否相等不相等的话我们进行根节点的左子树和右子树与subRoot是否相等的判断*/
5.二叉树的前序遍历
/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; *//** * Note: The returned array must be malloced, assume caller calls free(). */ //返回的是整型数组,数组必须是动态开辟的 /* 求二叉树节点的个数就是这个二叉树左子树节点和右子树节点的个数的总和*/ typedef struct TreeNode TreeNode; int TreeSize(TreeNode* root){ if(root==NULL) { return 0; } return 1+TreeSize(root->left)+TreeSize(root->right); //左子树节点的个数和右子树节点的个数}void _preorderTraversal(TreeNode* root,int *arr,int* pi)//进行递归的函数//第一个参数是根节点,第二个参数是存储的数组,将结果存储在数组中{ if(root==NULL)//如果节点为空的话,那么我们就不需要保存当前节点的值 { return; } //走到这里说明当前节点不为空,那么我们将当前节点的值保存在数组中 //题目中说的是前序遍历,那么就是根左右 arr[(*pi)++]=root->val; _preorderTraversal(root->left,arr,pi); _preorderTraversal(root->right,arr,pi);}int* preorderTraversal(struct TreeNode* root, int* returnSize){ //第一步:先求出二叉树节点的个数 //将节点个数放到* returnSize里面 * returnSize=TreeSize(root); //第二步:动态申请内存--arr的大小 //这个数组存放二叉树内节点的值 int*returnArr=(int*)malloc(sizeof(int)*(* returnSize)); int i=0; _preorderTraversal(root,returnArr,&i); return returnArr;}/*我们需要先知道二叉树节点的个数知道节点个数之后我们进行数组的动态内存的开辟然后我们又创建了一个函数专门进行递归,将节点中的数据存储在数组中在这个函数中,我们传过去的有根节点,数组,还有i的地址在函数内,如果节点为空的话,那么我们直接返回然后我们进行数组内元素的赋值我们利用传的指针作为下标为什么i要传地址呢?因为我们的i作为下标要一直进行++如果不传地址的话,传值的话,那么对于这个函数内的两个递归进行完左递归之后我们的i是不会有变化的所以我们要进行传地址操作我们将节点数值依次放到数组中,*pi一直在++操作,下标一直在变化我们先遍历左子树的值,再遍历右子树的值,那么i就要一直进行变化;最后我们还要返回数组的地址*/
6.二叉树的中序遍历
typedef struct TreeNode TreeNode;int TreeSize(TreeNode* root) { if (root == NULL) { return 0; } return 1 + TreeSize(root->left) + TreeSize(root->right);}void _inorderTraversal(TreeNode* root, int *arr, int* pi) { if (root == NULL) { return; } _inorderTraversal(root->left, arr, pi); // 先遍历左子树 arr[(*pi)++] = root->val; // 访问根节点 _inorderTraversal(root->right, arr, pi); // 再遍历右子树}int* inorderTraversal(struct TreeNode* root, int* returnSize) { *returnSize = TreeSize(root);//求节点个数 int* returnArr = (int*)malloc(sizeof(int) * (*returnSize));//开辟空间 int i = 0; _inorderTraversal(root, returnArr, &i);//递归函数 return returnArr;//返回存储数据的数组的指针}
7.二叉树的后续遍历
typedef struct TreeNode TreeNode;int TreeSize(TreeNode* root) { if (root == NULL) { return 0; } return 1 + TreeSize(root->left) + TreeSize(root->right);}void _inorderTraversal(TreeNode* root, int *arr, int* pi) { if (root == NULL) { return; } _inorderTraversal(root->left, arr, pi); // 先遍历左子树 _inorderTraversal(root->right, arr, pi); // 再遍历右子树 arr[(*pi)++] = root->val; // 访问根节点}int* postorderTraversal(struct TreeNode* root, int* returnSize) { *returnSize = TreeSize(root);//求节点个数 int* returnArr = (int*)malloc(sizeof(int) * (*returnSize));//开辟空间 int i = 0; _inorderTraversal(root, returnArr, &i);//递归函数 return returnArr;//返回存储数据的数组的指针}
8.二叉树的构建以及遍历
#include /*读取用户输入,保存在数组中,这个字符串的内容刚好是二叉树的前序遍历我们根据这个前序遍历的结果来创建二叉树再对二叉树进行中序遍历,输出中序遍历的结果*//*abc##de#g##f###对于这个字符串,c是b的左子树,b是a的左子树*//*若遍历中没有给出NULL节点的情况,是不能根据某一个遍历来创建二叉树的*/typedef struct BinaryTreeNode//定义二叉树节点结构类型{ char data; struct BinaryTreeNode*left; struct BinaryTreeNode*right;}BTNode;BTNode*buyNode(char x)//创建节点{ BTNode*newnode=(BTNode*)malloc(sizeof(BTNode)); newnode->data=x; newnode->left=newnode->right=NULL; return newnode;}BTNode*createTree(char*arr,int*pi)//创建二叉树,返回根节点,第一个参数是字符串,第二个参数是下标{ if(arr[*pi]==\'#\') { (*pi)++;//不要忘了往后面接着走 return NULL; } BTNode*root=buyNode(arr[(*pi)++]);//将数组中的数据存储在二叉树中,将数组中的数据传到二叉树节点内 root->left=createTree(arr,pi); //然后进行二叉树左节点的创建 root->right=createTree(arr,pi); //二叉树右节点的创建 return root;//返回根节点}void InOeder(BTNode*root){ if(root==NULL) { return; } InOeder(root->left);//递归左子树 printf(\"%c \",root->data);//打印根节点 InOeder(root->right);//递归右子树}int main(){ //读取用户输入的字符串保存在字符数组中 char arr[100]; scanf(\"%s\",arr); //根据字符串(前序遍历)创建二叉树 int i=0; BTNode*root=createTree(arr,&i);//这里的root是创建的二叉树的根节点 //输出二叉树的中序遍历 InOeder(root); return 0;}/*当pi=0时,我们创建了根节点a,然后创建a的左子树然后pi指向b,然后创建了根节点b,然后pi++,pi指向c,创建了根节点c,然后pi++,pi指向了#,为空,那么我们就return 了然后创建c的右子树那么pi++指向了#,那么就是空那么就返回,回到了b这颗树,那么b的左子树就创建完成,那么就进行b的右子树的创建了pi++指向了d,那么我们创建根节点d,然后pi++指向e,然后我们创建b的左节点epi++,指向#,所以为空,那么我们就返回了,进行e的右节点的创建pi++指向了g,创建了根节点g pi++,指向了# 所以我们在创建g的左节点的时候返回了那么我们就创建g的右节点pi++指向了#,所以为空,那么我们就返回了。返回到d,d的左子树已经创建好了那么我们就创建d的右子树pi++指向了f我们创建根节点f然后pi++,指向#,创建f的左节点,因为为空,所以我们返回,然后创建f的右节点,,Pi++,指向了#,为空,那么我们就返回了那么d的左右子树都已经创建完成了那么我们就返回到a了在a这棵树里面,a的左子树b已经创建好了那么我们就进行a的右子树的创建了pi++,指向了#,那么为空,我们就返回到这里,a这颗二叉树就创建好了*/
可以参考这个图片里面的二叉树
6.二叉树选择题
证明上述性质:假设⼀个⼆叉树有 a 个度为2的节点, b 个度为1的节点, c 个叶节点,则这个⼆叉树的边数是2a+b
另⼀⽅⾯,由于共有 a+b+c 个节点,所以边数等于 a+b+c-1
结合上⾯两个公式:
2a+b = a+b+c-1 ,即: a = c-1
就是度为2节点的个数等于叶子节点的个数-1
二叉树性质的题目:
- 某⼆叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该⼆叉树中的叶⼦结点数为(B )
A 不存在这样的⼆叉树
B 200 叶子节点的个数=节点数为2的节点的个数+1 n0=n2+1
C 198
D 199
2.在具有 2n 个结点的完全⼆叉树中,叶⼦结点个数为(A )
A n n0+n1+n2=2N 因为n0=n2+1 所以n0+n1+n0-1=2n0+n1-1=2N
B n+1 完全二叉树中有多少个度为1的节点呢?度为1的节点只可能为1个或者是0个
C n-1 那么我们带进去的话,当节点为0的时候,算出来的是小数,节点是1的时候,那么得
D n/2 到的就是n
3.⼀棵完全⼆叉树的结点数位为531个,那么这棵树的⾼度为( )
A 11
B 10 2^h-1=531 那么得到的h=log532 那么得到的只能是10层,9层是不够的
C 8
D 12
4.⼀个具有767个结点的完全⼆叉树,其叶⼦结点个数为(B)
A 383
B 384
C 385
D 386
这个题和第二题是差不多的
2n0+n1-1=767
n1只有1和0两种可能
那么我们得到的n0=384
二叉树遍历题目
1.某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为(A)
A ABDHECFG
B ABCDEFGH
C HDBEAFCG
D HDEBFGCA
2.⼆叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则⼆叉树根结点为(A)
A E
B F
C G
D H
3.设⼀课⼆叉树的中序遍历序列:badce,后序遍历序列:bdeca,则⼆叉树前序遍历序列为D。
A adbce 后序遍历的根节点肯定在最后面,那么a肯定是根节点,a的左节点是b,
B decab 根据后续遍历我们知道a的右节点是c
C debac 那么结合中序遍历来说,d是c的左子树,e是c的右子树
D abcde
4.某⼆叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右)的序列为(A)
A FEDCBA
B CBAFED
C DEFCBA
D ABCDEF
从后序遍历我们知道根节点是F
但是中序遍历中F在最后,那么说明F没有右子树,只有左子树
从中序遍历和后序遍历我们能知道E是F的左节点
E的左子树是D,D的左子树是C,C的左子树是B,B的左子树是A
那么最后的图形就是一根直线,最下面的是A
根据题目问题,那么最后得到的就是A
证明上述性质:假设⼀个⼆叉树有 a 个度为2的节点, b 个度为1的节点, c 个叶节点,则这个⼆叉树的边数是2a+b
另⼀⽅⾯,由于共有 a+b+c 个节点,所以边数等于 a+b+c-1
结合上⾯两个公式:
2a+b = a+b+c-1 ,即: a = c-1