c语言-数据结构-链式二叉树

目录

1、二叉树的概念及结构

2、二叉树的遍历概念

2.1 二叉树的前序遍历

2.2 二叉树的中序遍历 

2.3 二叉树的后序遍历 

2.4 二叉树的层序遍历

3、创建一颗二叉树 

4、递归方法实现二叉树前、中、后遍历 

4.1 实现前序遍历

4.2 实现中序遍历

4.3 实现后序遍历 

5、求二叉树的结点总数

6、求二叉树叶子个数

7、求第k层结点总数

8、求二叉树的高度

9、从二叉树中查找值为x的结点

10、层序遍历

11、二叉树的销毁

12、测试功能

结语:


1、二叉树的概念及结构

        二叉树是由根节点和一个左子树以及一个右子树构成,且每一个结点的孩子节点可以少于两个但是不能多于两个,每个结点都带有一个数据,作为结点的有效值。二叉树示意图如下:

        从上图可以看出,位于根结点左半部分的称为左子树,位于根结点右半部分的称为右子树,二叉树的顺序不能颠倒。同时2既可以看出是根结点1的孩子结点,他也可以作为3的父母结点,因此2也可以看作是一个根结点。 

        因此二叉树通常都采用递归的方式来实现。

2、二叉树的遍历概念

        在学习数组的时候,有一个最基本的概念就是遍历数组,数组的很多问题都是在遍历数组的基础上完成的。学习链表的时候,链表的很多操作也是在遍历链表的前提下实现的。因此,要对二叉树进行一系列的操作,也需要遍历二叉树。

        二叉树的遍历一般采用递归的方式,对二叉树的每个结点进行相应操作。二叉树的遍历分为:前序遍历、中序遍历、后序遍历以及层序遍历。

2.1 二叉树的前序遍历

       前序遍历的顺序:根、左子树、右子树。结构图如下:

        上图的二叉树前序遍历:123NNN45NN6NN(N表示NULL)。 

        前序遍历详解:1为根节点,因此从1开始遍历,2是1的左子树也就是遍历到2这个位置(前序遍历顺序:根-左子树-右子树),这时候会把2看成一棵树(2为根结点),然后逻辑又回到了根-左子树-右子树,3是2的左子树,因此下一个遍历的就是3,这时候又把3看成了一棵树(3为根结点),遍历3结点的左子树也就是NULL,当遍历当NULL的时候就开始“往上收回”,这时候3的左子树收回后就去遍历右子树,而这里3的右子树也是NULL因此也发生收回,最后的结果就是3结点遍历完成,同时表示结点2的左子树遍历完成,接下来就是遍历结点2的右子树,最后收回到根结点1(表示根结点1的左子树遍历完成)。接下来就是去遍历根结点1的右子树,遍历右子树的逻辑也是一样,把4看成根节点,继续根-左子树-右子树的逻辑。

2.2 二叉树的中序遍历 

        中序遍历的结构图与前序遍历的结构图相似,只是中序遍历的顺序不一样,中序遍历顺序为:左子树、根、右子树。

        因此该二叉树的中序遍历的顺序为:N3N2N1N5N4N6N(N表示NULL)

        中序遍历详解:2可以看成是1的左子树,3可以看成是2的左子树,NULL是3的左子树。中序遍历的第一个是左子树,因此把3看成是一棵树并且从3入手,遍历3的左子树NULL,然后是根结点3,最后是3的右树NULL,所以顺序为N3N。接下来把3看成是2的左子树,逻辑一样为:左子树-根-右子树,3遍历完成代表2的左子树遍历完成,接下来是根结点2,然后是根结点2的右子树NULL,此时顺序为:N3N2N。2作为根结点遍历完成后,表示1的左子树遍历完成,接下来遍历的逻辑是根结点1-1的右子树,把4当成根结点执行同样的逻辑:左子树-根-右子树。

2.3 二叉树的后序遍历 

        接下来的后序遍历的顺序为:左子树、右子树、根。其逻辑与上述相似,只不过顺序做了调整。

        该二叉树的后序遍历顺序为:NN3N2NN5NN641(N表示NULL)。 

2.4 二叉树的层序遍历

        层序遍历顾名思义就是一层一层、自上而下从左到右的遍历,首先从第一层也就是根结点开始,其次是第二层,并且从左边到右边的遍历,以此类推。

        因此层序遍历的顺序为:1 2 4 3 5 6。

        下面使用代码来实现二叉树及各个功能。

3、创建一颗二叉树 

        从二叉树的结构分析可以得出,创建二叉树要满足三个条件:有效数据、指向左孩子的指针,指向右孩子的指针。

        创建二叉树代码如下:

typedef int TreeDataType;//int类型重定义
typedef struct TreeNode
{TreeDataType data;struct TreeNode* left;//指向左孩子指针struct TreeNode* right;//指向右孩子指针
}TNode;TNode* CreatTree()//创造二叉树
{//创建结点TNode* n1 = CreatTreeNode(1);TNode* n2 = CreatTreeNode(2);TNode* n3 = CreatTreeNode(3);TNode* n4 = CreatTreeNode(4);TNode* n5 = CreatTreeNode(5);TNode* n6 = CreatTreeNode(6);//TNode* n7 = CreatTreeNode(7);//构建树结点之间的关系n1->left = n2;n1->right = n4;n2->left = n3;n4->left = n5;n4->right = n6;//n5->left = n7;return n1;//返回该二叉树的根节点
}

        该二叉树的物理图:

4、递归方法实现二叉树前、中、后遍历 

4.1 实现前序遍历

        前序遍历代码如下:

void PreOrder(TNode* root)//前序遍历
{if (root == NULL){printf("N ");//当递归到NULL时打印并返回Nreturn;}printf("%d ", root->data);//打印根节点PreOrder(root->left);//打印左子树PreOrder(root->right);//打印右子树
}

        因为二叉树是由递归实现的,并且前序遍历的顺序为:根-左子树-右子树。进入函数PreOrder时如果结点root不是空结点,则可以将该结点看成根节点,按照前序遍历的逻辑打印该结点的值,然后继续遍历该结点的左子树,和右子树,当结果为NULL时就会跳出该函数。当结点3的函数走完了,就会收回至结点2的函数,说明结点2的左子树函数完成。

        具体步骤图如下:

4.2 实现中序遍历

        中序遍历的顺序与前序遍历顺序不一样,因此对前序遍历的代码稍作修改即可。

        中序遍历代码如下:

void InOrder(TNode* root)//中序遍历
{if (root == NULL){printf("N ");return;}//相比于前序遍历,把这两个语句的顺序交换了以下InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}

        可以发现,中序遍历中的打印根结点的代码与左子树代码只是做了简单的更替便可以实现中序遍历,交换后三个语句的顺序也刚好对应中序遍历的顺序:左子树-根-右子树。

4.3 实现后序遍历 

        经过上述的规律可以得出后序遍历的代码逻辑。

        后序遍历代码如下:

void PostOrder(TNode* root)//后序遍历
{if (root == NULL){printf("N ");return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}

5、求二叉树的结点总数

        求二叉树结点的总数,第一步就是要遍历二叉树。因此采用的是递归的方式,因此每次函数调用返回的时候要返回当前结点的个数。

        这里注意的点:当递归的函数返回一个值,是返回给上一层调用该函数的函数,如此层层返回最后返回给根结点函数。

        求二叉树结点总数代码如下:

int BinaryTreeSize(TNode* root)//结点个数
{if (root == NULL)//为空返回0return 0;//递归左右子树return BinaryTreeSize(root->left)+ BinaryTreeSize(root->right) + 1;//若执行到此语句说明root不为空//+1表示把当前的结点记录进去
}

6、求二叉树叶子个数

        把二叉树中没有孩子结点的结点称为叶子结点。

        因此叶子节点的特性是其他节点不具有的,既:左孩子和右孩子都为空。因此当递归至某个结点的时候发现其左孩子和右孩子都为空,则计数+1。

        求二叉树叶子个数代码如下:

int BinaryTreeLeafSize(TNode* root)//叶子个数
{if (root == NULL)//为空则不是叶子结点return 0;if (root->left == NULL && root->right == NULL)//左右孩子都为空则返回1{return 1;}elsereturn BinaryTreeLeafSize(root->left)//递归左子树+ BinaryTreeLeafSize(root->right);//递归右子树
}

7、求第k层结点总数

        比如求该二叉树的第三层结点总数。思路:从上往下看,如果求第三层,可以转换成求结点1的第三层,求结点2和4的第二层,求结点3 5 6 的第一层,都表示为该树的第三层,只是表达不一样。因此当k==1的时候说明这时候是在第k层。

        求第k层结点总数代码如下:

int BinaryTreeLevelKSize(TNode* root, int k)//求第k层结点的总数
{if (root == NULL)return 0;if (k == 1)return 1;//递归函数,k不断-1return BinaryTreeLevelKSize(root->left, k - 1)//递归左子树+ BinaryTreeLevelKSize(root->right, k - 1);//递归右子树
}

8、求二叉树的高度

        思路:先遍历到最底层,然后收回的时候每一层+1,取左子树递归函数的值与右子树递归函数的值的较大值加上该层高度1就是该层的高度。示意图如下:

        二叉树的高度代码如下:

int BinaryTreeHeight(TNode* root)//二叉树高度
{if (root == NULL)return 0;//递归函数int leftHeight = BinaryTreeHeight(root->left);//将左子树的高度存在一个变量中int rightHeight = BinaryTreeHeight(root->right);//将右子树的高度存在一个变量中//取两个变量的较大者加上该层的高度1return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

9、从二叉树中查找值为x的结点

        思路:若找到该节点,则返回该节点的地址,并且用一个指针变量来接收,之后的代码就不需要再运行。

        二叉树查找代码如下:

TNode* BinaryTreeFind(TNode* root, TreeDataType x)//查找值为x的结点
{if (root == NULL)//为空返回NULLreturn NULL;if (root->data == x)//若找到了则返回该结点的地址return root;TNode* xpoi= BinaryTreeFind(root->left, x);//递归左子树,找到了就存放在指针变量xpoi中if (xpoi != NULL)//如果没有找到就不执行if语句,则继续找return xpoi;xpoi= BinaryTreeFind(root->right,x);//递归右子树if (xpoi != NULL)return xpoi;return NULL;//若都没有找到则返回NULL给上层函数
}

10、层序遍历

        层序遍历的逻辑与前、中、后遍历的逻辑不一样,前、中、后遍历用的是递归的逻辑,而层序遍历则是采用非递归的逻辑。

        层序遍历的顺序如上图:1 2 4 3 5 6,他的思路是把树节点放入队列中,队列的逻辑是先进先出、后进后出, 因此先把根节点1放入队列中,然后出队的时候是先出的1,同时把1的两个孩子入队,此时队列中存放的是2 4,并且下一次出队先将2出掉,同时把2的孩子入队,此时队列里存放的是4 1,如此下去,最后出队的顺序为1 2 4 3 5 6,与层序遍历的顺序一样。

        因此层序遍历的代码涉及队列的创建:

//队列结构体
typedef struct TreeNode* QueueDataType;
typedef struct QNode
{struct QNode* next;QueueDataType data;
}QNode;typedef struct Queue
{struct QNode* head;struct QNode* tail;int size;
}Queue;void QueueInit(Queue* pq)//队列初始化
{assert(pq);pq->head = NULL;pq->tail = NULL;pq->size = 0;
}void QueuePush(Queue* pq, QueueDataType x)//入队
{assert(pq);QNode* newnode = BuyNode(x);if (pq->head == NULL){assert(pq->tail==NULL);pq->head = pq->tail = newnode;}else{pq->tail->next = newnode;pq->tail = newnode;}pq->size++;
}bool Empty(Queue* pq)//判空
{assert(pq);return pq->head == NULL|| pq->tail==NULL;
}QueueDataType QueueFront(Queue* pq)//显示队头数据
{assert(pq);assert(!Empty(pq));return pq->head->data;
}void QueuePop(Queue* pq)//出队
{assert(pq);assert(!Empty(pq));if (pq->head == pq->tail)//一个节点{free(pq->head);pq->head = pq->tail = NULL;}else//多个节点{QNode* poi = pq->head;pq->head = pq->head->next;free(poi);}pq->size--;}void QueueDestroy(Queue* pq)//释放队列
{assert(pq);QNode* cur = pq->head;while (cur){QNode* poi = cur->next;free(cur);cur = poi;}pq->head = pq->tail = NULL;pq->size = 0;
}//层序遍历
void LevelOrder(TNode* root)
{Queue q;QueueInit(&q);if(root!=NULL)QueuePush(&q, root);while (!Empty(&q)){TNode* front=QueueFront(&q);QueuePop(&q);printf("%d ", front->data);if(front->left)QueuePush(&q, front->left);if(front->right)QueuePush(&q, front->right);}QueueDestroy(&q);
}

11、二叉树的销毁

        由于二叉树是在堆上申请而来的,因此再使用完之后要对申请的空间进行释放。这里选择用后序的方法进行释放,原因是后序的顺序是:左子树-右子树-根,根是最后才释放的,如果用前序遍历释放就会出现先把根释放了,就不好找根的左子树和右子树了,中序遍历也同理。

        二叉树销毁代码如下:

void BinaryTreeDestory(TNode* root)//二叉树销毁
{if (root == NULL)return;//后序遍历BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);free(root);
}

12、测试功能

        上述解析了如此多的功能,接下来对其进行测试,观察运行结果。

        测试代码如下:


int main()
{TNode* root = CreatTree();//创建树,并返回根结点PreOrder(root);//前序遍历printf("\ntreesize:%d", BinaryTreeSize(root));//树的结点个数printf("\ntreesize:%d", BinaryTreeSize(root));printf("\nLeafSize:%d", BinaryTreeLeafSize(root));//叶子个数printf("\nLevelKSize:%d", BinaryTreeLevelKSize(root, 3));//第k层结点个数printf("\nheight:%d", BinaryTreeHeight(root));//树的高度TNode* xpoi = BinaryTreeFind(root, 3);//查找结点if (xpoi == NULL)printf("二叉树无该结点\n");elseprintf("\n找到结点:%d", xpoi->data);printf("\n");LevelOrder(root);//层序遍历BinaryTreeDestory(root);//二叉树释放root = NULL;//手动置空return 0;
}

        运行结果:

        从运行结果来看,以上功能均可正常运行。 

结语:

        以上就是关于二叉树以及相关功能的实现与解析,二叉树的重点在于对函数递归的形象理解,本质上二叉树就是运用函数不断递归实现的,看似一小段代码实则可以延长出很多信息。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!

(~ ̄▽ ̄)~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/155589.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CV计算机视觉每日开源代码Paper with code速览-2023.11.16

点击CV计算机视觉,关注更多CV干货 论文已打包,点击进入—>下载界面 点击加入—>CV计算机视觉交流群 1.【基础网络架构】ConvNet vs Transformer, Supervised vs CLIP: Beyond ImageNet Accuracy 论文地址:https://arxiv.org//pdf/23…

深度模型压缩研究回顾

深度模型压缩研究回顾 作者:安静到无声 个人主页 目录 深度模型压缩研究回顾推荐专栏 在本节中,主要介绍了目前主流的深度神经网络压缩与加速方法,主要包括轻量化网络设计、参数量化、知识蒸馏、模型剪枝和硬件加速等,其中模型剪…

【算法】最小生成树——普利姆 (Prim) 算法

目录 1.概述2.代码实现2.1.邻接矩阵存储图2.2.邻接表存储图2.3.测试 3.应用 1.概述 (1)在一给定的无向图 G (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边,而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集且为无循…

湖科大计网:应用层

一、应用层概述 交互,实现特定问题! 二、客户与服务器模型 一、C/S 客户/服务器方式 服务与被服务的关系。 二、P2P方式 对等方式 P2P方式是对等的,没有固定的服务器。 三、DNS域名系统 DNS(Domain Name System) 一、域…

2018年计网408

第33题 下列 TCP/P应用层协议中, 可以使用传输层无连接服务的是()A. FTPB. DNSC. SMTPD. HTTP 本题考察TCP/IP体系结构中,应用层常用协议所使用的运输层服务。 如图所示。这是TCP/IP体系结构中常见应用层协议各自所使用的运输层端口,。在这些应用层协议中&#x…

Vue Router的使用

Vue.js是一个流行的JavaScript框架,用于开发单页面应用程序。Vue提供了一个强大的路由系统,可以帮助我们管理应用程序中的不同页面。在本文中,我们将详细讲解Vue路由的使用方法。 目录 1. 安装Vue Router2. 创建路由实例3. 配置路由4. 在模板…

关闭bitlocker加密

windows11的笔记本电脑买回来发现分驱都处于bitlocker状态,上网上搜索都是说进入控制面板的安全项进行关闭,包括去搜索栏搜索“管理 BitLocker”,对搜索出来的项打开,经过试验,它们进入的是同一个位置,只有…

【docker】虚拟化和docker容器概念

基础了解 IAAS: 基础设施服务,(只提供基础设施,没有系统) **SAAS: ** 软件即服务,(提供基础设施和系统) PAAS: 平台即服务,(提供基…

SwiftUI 如何动态开始和停止播放永久重复(repeatForever)动画

0. 功能需求 在 SwiftUI 丰富多彩的动画世界中,我们有时希望可以随意开始和停止永久循环(repeatForever)的动画,不过这时往往会产生错误的动画“叠加”效果。 从上图可以看到:虽然我们希望密码输入框背景只在用户输入密码时才发生闪烁,但顶部的密码输入框随着不断输入其…

Linux02 VIM编辑器

Linux02 VIM编辑器 基本上 vi/vim 共分为三种模式, 分别是命令模式(Command mode),输入模式(Insert mode)和底线命令模式(Last line mode)。 三种状态进行切换 插入模式&#xff1a…

Java入门篇 之 内部类

本篇碎碎念:本篇没有碎碎念,想分享一段话: 你不笨,你只是需要时间,耐心就是智慧,不见得快就好,如果方向都不对,如果心术不正,如果德不配位,快就是对自己天分的…

【数值计算方法】矩阵特征值与特征向量的计算(一):Jacobi 旋转法及其Python实现

文章目录 一、Jacobi 旋转法1. 基本思想2. 计算过程演示3. 注意事项 二、Python实现迭代过程(调试) 矩阵的特征值(eigenvalue)和特征向量(eigenvector)在很多应用中都具有重要的数学和物理意义。Jacobi 旋转…

六、流量监管、流量整形

流量监管、流量整形 流量监管、流量整形1.1.定义1.2.简单流分类应用场景举例1.3.简单流分类的配置1.4.复杂流分类1.5.复杂流分类在产品中的实现 ————————————————— 流量监管、流量整形 当报文的发送速率大于接收速率,或者下游设备的接口速率小于…

GitHub 2023报告-开源和AI的现状

GitHub 2023报告-开源和AI的现状 深入探讨人工智能如何与开源互动,以及未来几年可能出现的趋势。 背景介绍 2023年,开源已成为全球软件开发的标准。无论是大公司还是小团队,都广泛使用开源技术进行项目开发。此外,随着机器学习和…

【Java】java | CacheManager | redisCacheManager

一、说明 1、查询增加缓存,使用Cacheable注解 2、项目中已经用到了ehcache,现在需求是两个都用 二、备份配置 1、redisConfig增加代码 Bean("redisCacheManage")Primarypublic CacheManager redisCacheManager(RedisConnectionFactory fact…

CKD TransBTS:用于脑肿瘤分割的具有模态相关交叉注意的临床知识驱动混合转换器

CKD-TransBTS: Clinical Knowledge-Driven Hybrid Transformer With Modality-Correlated Cross-Attention for Brain Tumor Segmentation CKD TransBTS:用于脑肿瘤分割的具有模态相关交叉注意的临床知识驱动混合转换器背景贡献实验方法how radiologists diagnose b…

【19年扬大真题】已知a数组int a[ ]={1,2,3,4,5,6,7,8,9,10},编写程序,求a数组中偶数的个数和偶数的平均值

【18年扬大真题】 已知a数组int a[ ]{1,2,3,4,5,6,7,8,9,10}&#xff0c;编写程序&#xff0c;求a数组中偶数的个数和偶数的平均值 int main() {int arr[10] { 1,2,3,4,5,6,7,8,9,10 };int os 0;//偶数个数int sum 0;//偶数和float ave 0;//偶数平均值for (int i 0;i <…

关于2023年11月25日PMI认证考试准考信下载及考场规定等事项通知

各位考生&#xff1a;为保证参加2023年11月25日PMI项目管理资格认证考试的每位考生都能顺利进入考场参加考试&#xff0c;请完整阅读本通知内容。 一、关于准考信下载为确保您顺利进入考场参加11月份考试&#xff0c;请及时登录本网站个人系统下载并打印准考信&#xff0c;准考…

Google codelab WebGPU入门教程源码<6> - 使用计算着色器实现计算元胞自动机之生命游戏模拟过程(源码)

对应的教程文章: https://codelabs.developers.google.com/your-first-webgpu-app?hlzh-cn#7 对应的源码执行效果: 对应的教程源码: 此处源码和教程本身提供的部分代码可能存在一点差异。点击画面&#xff0c;切换效果。 class Color4 {r: number;g: number;b: number;a…

论文阅读:“基于特征检测与深度特征描述的点云粗对齐算法”

文章目录 摘要简介相关工作粗对齐传统的粗对齐算法基于深度学习的粗对齐算法 特征检测及描述符构建 本文算法ISS 特征检测RANSAC 算法3DMatch 算法 实验结果参考文献 摘要 点云对齐是点云数据处理的重要步骤之一&#xff0c;粗对齐则是其中的难点。近年来&#xff0c;基于深度…