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

news2024/12/29 19:49:21

目录

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时打印并返回N
		return;
	}

	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)//为空返回0
		return 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;
	}
	else
		return 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不断-1
	return 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);//将右子树的高度存在一个变量中

    //取两个变量的较大者加上该层的高度1
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

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

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

        二叉树查找代码如下:

TNode* BinaryTreeFind(TNode* root, TreeDataType x)//查找值为x的结点
{

	if (root == NULL)//为空返回NULL
		return 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");
	else
		printf("\n找到结点:%d", xpoi->data);

	printf("\n");
	LevelOrder(root);//层序遍历

	BinaryTreeDestory(root);//二叉树释放
	root = NULL;//手动置空
	return 0;
}

        运行结果:

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

结语:

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

(~ ̄▽ ̄)~

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

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

相关文章

从零开始的c语言日记day35——数据在内存中的储存

数据类型介绍 之前已经学了了一些基本的内置类型,以及空间大小。 类型的意义: 使用这个类型开辟内存空间的大小(大小决定了使用范围)。如何看待内存空间的视角 类型的基本归类 整形: 字符的本质是ASCLL码值&#x…

Java实现堆算法

堆是一种特殊的数据结构,它是一棵完全二叉树,且满足堆的性质:对于每个节点,它的值都不小于(或不大于)它的孩子节点的值。根节点的值就是堆中的最大值(或最小值)。 Java中提供了一个…

算法分析与设计课后练习22

设W(5,7,10,12,15,18,20)和M35,使用过程SUMOFSUB找出W种使得和数等于M的全部子集并画出所生成的部分状态空间树

Linux C 网络编程概述

网络编程 计算机网络概述分类网络体系结构通信协议通信流程网络通信帧格式以太网帧格式分析ARP 协议分析IP 数据报分析IP分类IP 分配子网掩码 TCP 段分析 TCP三次握手协议 ⭐TCP四次挥手协议 ⭐ TCP编程基于 TCP 客户端编程-步骤说明基于 TCP 服务器端编程-步骤说明基于 TCP 服…

TypeError: expected np.ndarray (got Tensor)解决办法

文章目录 一、错误展示二、错误分析三、解决办法四、其余解决办法总结 一、错误展示 二、错误分析 这个错误表示正在尝试将一个PyTorch的Tensor对象作为numpy的ndarray对象来使用。我们需要使用numpy的ndarray而不是PyTorch的Tensor。 三、解决办法 在我的程序中去掉这一行代…

Redis跳跃表

前言 跳跃表(skiplist)是一种有序数据结构,它通过在每一个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 跳跃表支持平均O(logN),最坏O(N),复杂度的节点查找,还可以通过顺序性来批量处理节点…

局域网文件共享神器:Landrop

文章目录 前言解决方案Landrop软件界面手机打开效果 软件操作 前言 平常为了方便传文件,我们都是使用微信或者QQ等聊天软件,互传文件。这样传输有两个问题: 必须登录微信或者QQ聊天软件。手机传电脑还有网页版微信,电脑传手机比…

鸿蒙4.0开发笔记之DevEco Studio启动时不直接打开原项目(二)

1、想要在DevEco Studio启动时不直接打开关闭前的那个项目,可以在设置中进行。 有两个位置可以进入“设置”,一个是左上角的File>Settings,二是右上方的设置图标。 2、进入Settings界面以后,选择Appearance&Behavior下面…

【C++】泛型编程 ⑨ ( 类模板的运算符重载 - 函数声明 和 函数实现 写在同一个类中 | 类模板 的 外部友元函数问题 )

文章目录 一、类模板 - 函数声明与函数实现分离1、函数声明与函数实现分离2、代码示例 - 函数声明与函数实现分离3、函数声明与函数实现分离 友元函数引入 二、普通类的运算符重载 - 函数声明 和 函数实现 写在同一个类中三、类模板的运算符重载 - 函数声明 和 函数实现 写在同…

AMEYA360:蔡司扫描电镜Sigma系列:扫描电子显微镜的用途原来这么多

扫描电子显微镜是一种全自动的、非破坏性的显微分析系统,可针对无机材料和部分有机材料,迅速提供在统计学上可靠且可重复的矿物学、岩相学和冶金学数据,在采矿业,可用于矿产勘查、矿石表征和选矿工艺优化,在石油和天然…

Oracle数据库笔记(一)

1.概述 Oracle版本 19c 在线迁移、自适应扫描、自适应数据共享11g 企业管理器、自动化诊断工具、自动化性能管理 Oracle特点 可用性强可扩展性强数据安全性强稳定性强 常见数据库 小 Access中 SQL Server、MySQL大 Oracle、DB2 2.数据、数据库、数据库管理系统、数据库系…

【项目管理】甘特图(1)——认识甘特图

哈喽,大家好,我是雷工! 今天学习下甘特图的绘制,以下为学习笔记。 一、什么是甘特图 甘特图是可以直观展示项目进展随时间走势和联系的条状图。是一种常见的项目管理工具。 项目的时间用横轴表示,项目的进度用纵轴表…

vue diff算法原理以及v2v3的区别

diff算法简介 diff算法的目的是为了找到哪些节点发生了变化,哪些节点没有发生变化可以复用。如果用最传统的diff算法,如下图所示,每个节点都要遍历另一棵树上的所有节点做比较,这就是o(n^2)的复杂度,加上更新节点时的…

可视化大屏时代的到来:智慧城市管理的新思路

随着科技的不断发展,智能芯片作为一种新型的电子元件,被广泛应用于各个领域,其中智慧芯片可视化大屏是一种重要的应用形式。 一、智慧芯片可视化大屏的优势 智慧芯片可视化大屏是一种将智能芯片与大屏幕显示技术相结合的产品,山海…

算法分析与设计课后练习23

求下面的0-1背包问题 (1)N5,M12,(p1,p2,…,p5)(10,15,6,8,4),(w1,w2,…,w5)(4,6,3,4,2) (2)N5,M15,(p1,p2,…,p5)(w1,w2,…,w5)(4,4,5,8,9)

Springboot+vue的社区医院管理系统(有报告),Javaee项目,springboot vue前后端分离项目

演示视频: Springbootvue的社区医院管理系统(有报告),Javaee项目,springboot vue前后端分离项目 项目介绍: 本文设计了一个基于Springbootvue的前后端分离的应急物资管理系统,采用M(model)V&am…

【Django使用】4大模块50页md文档,第4篇:Django请求与响应和cookie与session

当你考虑开发现代化、高效且可扩展的网站和Web应用时,Django是一个强大的选择。Django是一个流行的开源Python Web框架,它提供了一个坚实的基础,帮助开发者快速构建功能丰富且高度定制的Web应用 Django全套笔记地址: 请移步这里 …

动态规划十大经典问题

动态规划十大经典问题 动态规划十大经典问题 数塔取数问题、矩阵取数问题、最大连续子段和、最长递增子序列、最长公共子序列、最长公共子串、最短编辑距离、背包问题、正整数分组、股票买卖问题。 1、数塔取数问题 // 数塔取数问题 public static int dataTowerAccess(int[]…

一文读懂 Linux 网络 IO 模型

文章目录 1.从一个问题说起2.多进程模型3.多线程模型4.I/O 多路复用5.select、poll、epoll 的区别?5.1 select5.2 poll5.3 epoll5.4 两种事件触发模式 参考文献 1.从一个问题说起 互联网发展历史上,曾经有一个著名的问题:C10K 问题。 C 是 …

5-4计算一串字符的空格数字字符其他

#include<stdio.h> int main(){char c;int space0;//空格int letters0;//英文字母int numbers0;//数字int others0;//其他字符printf("请输入一行字符&#xff1a;");while((cgetchar())!\n)//获取字符的内容&#xff0c;到\n停止{if(c>a&&c<z|…