c语言实现数据结构---二叉树

二叉树的性质

有了之前堆和数的基础,那么我们这里的二叉树那我们这里学习二叉树就变得十分的简单,那么我们这里就直接来看看这里的二叉树的性质有哪些。

  1. 若规定根节点的层数为1 ,则一颗非空二叉树的第i层上最多有2^(i-1)个节点。
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是(2^h)-1.
  3. 对任何一棵二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0=n2+1。大家看到这个性质有没有想过为什么,是不是感觉很奇怪,那么我们这里就可以这么来解释,当我们只有一个结点的时候,我们度为0的结点的个数是不是就是1,而度为2的结点的个数就是0,当我们添加一个结点的时候原本度为0的那个结点就变成了度为1,但是这时又添加了一个度为0的结点,所以此时度为0的结点的个数还是1,度为1的结点个数是1 ,而度为2的结点的个数还是0,但是当我们在添加一个结点进去的时候,原本度为1的结点变成了度为2,然后又多了一个度为0的结点,那么此时度为0的结点就变成了2,而度为1的结点就变成了0,度为2的结点就变成了1 ,那么这里大家可以自行下去画图看一看这里是可以类似的推导的。
  4. 若规定根节点的层数为1,且有n个结点的满二叉树的的深度为h=log(n+1),其中这个log是以2为底。
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有的结点从0开始编号则对于序号为i的结点右:
    1.若i>0,i位置结点的双亲结点的序号:(i-1)/2,i为根节点编号时,无双亲结点。
    2.若2i+1<n,左孩子序号为2i+1,2i+1>=n,则无左孩子。
    3.若2i+2<n, 右孩子序号为2i+2,2i+2>=n,则无右孩子。

链式二叉树

我们之前在实现堆的时候,是通过顺序表来实现的,但是我们这里的二叉树则采用链式来进行储存,那么我们这里的每个元素都是一个结构体,然后在这个结构体里面要存放三个东西一个就是我们存放的数据,一个就是指向左孩子的指针,最后一个就是指向有孩子的指针就像下面这样:
在这里插入图片描述
当然在有些二叉树里面还有三个指针,那这时这个三个指针一般都会指向左孩子,右孩子和他的双亲结点。那么这里我们的图就不多画了。

链式二叉树的遍历

那么我们知道了二叉树的结构之后我们得想办法将这些二叉树中的数据按照一定的顺序打印出来,那么我们这里的打印的顺序就分为三种:前序遍历,中序遍历,后序遍历。那么我们这里就来看看这些不同顺序遍历的意思:
前序遍历:先访问根,再访问根的左子树,最后再访问根的右子树。
中序遍历:先访问根的左子树,再访问根,最后访问根的右子树。
后序遍历:先访问根的左子树,再访问根的右子树,最后访问根。
那么这里大家要注意的一定就是我们在遍历树的时候不要将其简单的分为左结点或者右节点的去访问,我们要将他的树看成一个整体的去访问,大家看我们上面的在遍历的时候就可以返现我们并没说是左结点或者是右结点而是说的左子树或者右子树,那么看到这里想必大家肯定还是不大清楚我们这里的遍历到底是怎么做的,所以我们以下面的这个例子来进行讲解:请添加图片描述
首先来看看我们的前序遍历,前序遍历是先访问根再访问左子树,最后访问右子树,那么将这个规律用到我们这个图上就是先访问这里的根节点1,然后再访问1的左子树,但是左子树他也是树,所以我们这里还是访问这个左子树的根也就是2 ,然后再访问2的左子树,同样的道理左子树也是树,所以还是先访问这个左子树的根也就是3,再访问3的左子树,但是此时3的左子树为空所以我们就停止访问他的左子树,跑去访问访问它的右子树,但是右子树也为空啊,所以我们这里就回到这个3的双亲结点2这里开始访问2的右子树,又因为2的右子树也为空,所以我们这里又回到2的双亲的结点的位置1,再开始访问1的右子树,那么这里右子树的根节点就是4,再访问4的左子树,而这个左子树的根节点是5,因为5的左右子树都为空所以我们这里就回到5的双亲结点的位置也就是4,再访问4的右子树,右子树的根节点是6,而这个根节点的左右子树都是空,所以我们这里就回到它的双亲结点4,而4的左右子树都访问完了所以我们这里再回到它的双亲结点1,而1的左右子树也都访问完了,所以我们这个前序遍历也就访问完了,那么我们这里的访问顺序就是这样1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL,那么这里大家要是还是看不太懂的话可以看看这张图片:
在这里插入图片描述
那么这是我们的前序遍历,那我们的中序遍历也是同样的道理,我们的中序遍历是先访问我们的左子树再访问根结点最后访问我们的右子树,那么我这里还是用我们上面的例子来看看:
请添加图片描述
首先还是先来到我们的根节点,因为这里是中序遍历所以在访问1之前首先得访问1的左子树,那么这里就来到了2,但是2是这个左子树的根节点,所以在访问2之前得先2的的左子树那么这里就来到3,同样的道理我们这里不会访问3而是先访问3的左子树,那么这里的3的左子树是空,所以访问完之后就会回到3这里,因为3作为根节点它的左子树访问完了之后就会访问这里的根节点也就是3,等3访问完之后就会再访问3的右节点但是这里还是为空就会再放回到我们这里的根节点3,此时根节点3访问完了它的左右子树也访问完了那么此时就会返回3的双亲结点2这里,因为2的左子树已经访问完了,那么这里就可以访问这里的根结点也就是2,2访问完之后就可以访问2的右节点,但是这里是空也就会返回这里的根节点2 ,因为2的左子树访问完了右子树访问完了,根结点2也访问完了所以此时就会来到2的双亲结点也就是1,然后再访问1 ,因为1是根节点,所以根节点访问完之后就会访问这里1 的右子树,那么这里也就是同样的原理我们这里就不过多的赘述了,那么我们这里访问的顺序就是
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL,那么同样的道理后序遍历就是先访问左子树再访问右子树,最后访问根节点,那么我们这里遍历的结果就是:NULL NULL 3 NULL 2 NULL NULL 4 NULL NULL 5 NULL NULL 6 4 1 。那么这里就是我们遍历的结果,那我们的代码又是如何来实现的呢?首先我们得把这个树创建出来,那么我们这里为了方便我们直接创建一个结构体出来,然后通过动态开辟空间的方法给这些结构体里面的变量进行赋值,那么我们这里的代码就是这样:

BTNode* CreatTree()
{
	BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
	assert(n1);
	BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
	assert(n2);
	BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
	assert(n3);
	BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
	assert(n4);
	BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
	assert(n5);
	BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
	assert(n6);
	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n4->data = 4;
	n5->data = 5;
	n6->data = 6;
	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n2->right = NULL;
	n3->left = NULL;
	n3->right = NULL;
	n4->left = n5;
	n4->right = n6;
	n5->left = NULL;
	n5->right = NULL;
	n6->left = NULL;
	n6->right = NULL;
	return n1;
}

我们构建了一个函数,在这个函数里面开辟了动态的空间在这个空间里面进行一系列的赋值,这样我们就可以快速的构建出来我们这里的树,那么树创建好了接下来要做的就是遍历我们这里的树,那么首先我们实现这里的前序遍历吧,大家根据我们上面的讲解也不难看出我们这里的遍历其实就是一个递归的过程那么既然事递归的话我们这里就得先干一件事情就是递归结束的条件是什么?那么我们这里就很简单,当这个函数传过来的地址为空的实话我们就直接结束当前的递归回到上一层递归,并且我们这里是前序遍历,所以我们每层函数在判断是否需要执行递归之后就得来打印当前树的根节点的值,那么这时我们大致的代码就如下:

void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
}

那么我们这里的打印数据结束之后我们就得来到这个结点的左子树再进行一次打印,那么我们这里就得进行递归,所以我们得在这个函数里面再一次调用这个函数,并且将左子树的根节点传给这个函数,那等左子树递归完之后就得再递归我们的右子树,所以这时我们得再一次调用这个函数并且将右子树的根节点作为参数传给这个函数,那么我们这里完整的代码就如下:

void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

那么我们这里就可以来测试测试这个函数的真确性:

int main()
{
	BTNode* root=CreatTree();
	PreOrder(root);
	return 0;
}

我们来运行一下这个代码看看这个代码的打印结果为:
在这里插入图片描述
那么我们这里打印的结果却是和我们之前想的一模一样,那我们这里的中序遍历又该如何来做出改变呢?我们之前是先打印根结点的值再来遍历其左子树,接着再右子树,那我们这里的中序遍历是先遍历左子树再打印该子树根节点的值,再遍历右子树,那么我们这里直接将左子树的递归放到前面不就够了嘛,那么我们的代码就如下:

void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

我们来测试一下这个代码的正确性:
在这里插入图片描述
跟我们之前想的一模一样,那么我们的后序遍历就是将打印数据的printf函数放到最后面,那么我们的代码就如下:

void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

我们测试的结果就是这样:
在这里插入图片描述
也跟我们想的一模一样,那么我们这里的链表的遍历就到了这里想必大家能够理解这里的代码,大家要是实在理解不了可以把这个代码复制多份然后模拟这个递归的过程,毕竟一开始接触却是有点难受。

求结点的数目

那么我们上面知道了如何打印数据,那么接下来我们就来看看如何来求结点的数目,那么我们这里的思路就非常的简单,我们通过递归来实现这个这个函数,每次递归前判断一下这个传过来的结点是不是空结点,然后我们就创建一个变量用来计数,然后我们就对这个变量的值进行加一,然后再开始遍历他的左子树,等左子树遍历完之后我们就开始遍历右子树,那么我们这里的代码就如下:

void TreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	int count=0;
	count++;
	TreeSize(root->left);
	TreeSize(root->right);
}

如果哪个兄弟写出这样的代码那c语言可以回头重新学一遍了,因为我们这里的变量是在循环内部进行创建的,也就是之前函数加的值是不会算到新的函数里面去的,并且这个是局部变量我们在这个函数的递归的的外面完全使用不了这个函数里面的值,所以这种方法肯定是行不通的,那么这里我们就可以可以想到另外一种方法来解决这个问题就是使用全局变量,我们上面的这个方法是因为局部变量的作用域和生命周期受到了限制而无法访问到我们计数的值,那这下好了我直接将你改成全局变量你的作用域和生命周期全部都变成了整个程序,这下我总可以正常的访问了吧,那么这时我们的代码就如下:

int count = 0;
void TreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	count++;
	TreeSize(root->left);
	TreeSize(root->right);
}

那么我们这里再来测试一下代码:

int main()
{
	BTNode* root=CreatTree();
	TreeSize(root);
	printf("结点的个数为:%d", count);
	return 0;
}

那么我们这里就来看看这个代码测试的结果:
在这里插入图片描述
测试的结果为6确实符合我们这里的预期,但是看到这里有些小伙伴就还想到了之前我们学的静态变量这个概念,我们这里不需要创建一个全局变量,我们在前面创建的局部变量的前面加一个static将其变成一个静态变量是不是也可以达到同样的效果啊,因为我们这里的静态变量他是放到静态区里面的,他不会随着函数的结束而自动的销毁,那么我们这里的代码就如下:

void TreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	static int count = 0;
	count++;
	TreeSize(root->left);
	TreeSize(root->right);
}

大家觉得上面的代码是真确的吗?答案是不正确的,因为我们这里创建的静态变量他的生命周期确实与我们这个程序一模一样了,但是他的作用域却并不是所有的递归都能够使用的,他只有在第一层递归的时候才能使用这个静态的变量,那么我们这里就得对其进行修改,我们将这个函数改成有返回值的,并且返回的值就是我们这里的count变量,那么这样的话我们这里的静态变量的作用域就可以扩展到其他的递归函数里面,那么我们的代码就修改成了这样:

int TreeSize(BTNode* root)
{
	static int count = 0;

	if (root == NULL)
	{
		return count;
	}
	count++;
	TreeSize(root->left);
	TreeSize(root->right);
	return count;
}

那么我们来测试一下这个函数:

int main()
{
	BTNode* root=CreatTree();
	printf("结点的个数为:%d", TreeSize(root));
	return 0;
}

运行结果为:
在这里插入图片描述
那么这里的运行结果也跟我们预期的一模一样,那么看到这里想必大家对这两个方法来求结点的个数已经非常的熟悉了,那么接下来我们来看看第三种方法,首先我们这里的树他是有层次感的,这里的所谓的层次感就是和我们的官僚等级是一样的,比如说我们这里的根节点他就相当于一个学校的校长,根结点下面的两个子节点就相当于副校长,而这两个子节点的子节点就相当于年级主任这样等等依次往下推,那么我们这里要求结点的个数就相当于有一天校长说我们要统计一下我们学校一共有多少个人一样,校长想要统计人数,他就会叫他的两个副校长去来统计然后将他们统计出来变的结果加上自己本身算一个人最为整个学校的人数,但是我们的副校长他不会自己一个一个的去数,他会叫他的下属年级主任去数,然后将年级主任数的人数加上副校长本身算一个人汇报给校长,同样的道理年纪主任也不会自己去数,他会叫班主任去数,这样依次类推一直推到学生本身上报自己是不是人的人数才停止向下传递,那这就很简单我们自己肯定算人对吧,那么运用到我们这里就是当我们递归到结点为空的时候就返回0,并且停止递归,如果不是0,他汇报的人数就是他左子树的人数加上右子树的人数加上自己本身的1,那么我们这里的代码就是这样:

int TreeSize(BTNode* root)
{
	return root==NULL?0:1+ TreeSize( root->left)+ TreeSize(root->right);
}

我们可以来测试一下这个代码的正确性:

int main()
{
	BTNode* root=CreatTree();
	printf("结点的个数为:%d", TreeSize(root));
	return 0;
}

我们运行的结果如下:
在这里插入图片描述
那么看到这里我们使用了三种方法来解决这个问题,想必大家应该能够理解的十分的透彻,那么我们接着往下看。

求叶子节点的个数

那么我们这里要求的东西就与上面的不同我们上面是求结点的个数,而我们这里是求叶子节点的个数,那这里就有个区别就是不是所有的结点都是叶子结点,我们的叶子节点有个特点就是他的左子树和右子树都是空得为空,那么我们这里就可以根据这个特点来对我们上面的代码进行改进,我们之前讲的例子是说校长要统计整个学校的人数,我们是一层一层的向下询问人数,并且讲问到的人数加上自己本身向上级汇报,那么现在不同了我们的校长不想统计了全校的人数,他要统计全校180以上的学生的人数,那么这时情况就不一样了,因为你自己可能不是180的身高,所以你在向上级汇报的人数的时候你就不一定得将自己的上报上去,你得加一个条件上去,那么我们这里的条件就是当这个节点的左右子树都为空的时候我们就返回一个1上去,并且我们这里还是采用递归的方式来解决这个问题,那么我们这里的代码就如下:

int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

我们来测试一下代码的正确性:

int main()
{
	BTNode* root=CreatTree();
	printf("叶子结点的个数为:%d", TreeLeafSize(root));
	return 0;
}

那么我们来看看代码的测试结果:
在这里插入图片描述
那么这里我们的代码就是正确的,我们接着往下看下一个问题。

求第k层节点的个数

那么我们有了上面那道题的经验我们再来看这道题的时候就特别的容易我们这里是求第k层节点的个数 ,那么首先我们这里的函数的参数就会发生一些变化,我们这里得传两个值过来一个是根节点的地址,另外一个就是你要求的层数,那么我们这里的头部的代码就如下:

int TreeKLevel(BTNode* root, int k)
{

}

然后我们这里就还是用我们上面相同的思路来做这道题,我们就得改变一些东西,首先就是我们这里的条件就不能再是左子树右子树为空的时候返回1 了,而得是当我们的层数为k的时候得返回1,那这个条件我们怎么使用呢?大家这么想啊我们这里是递归来实现的这个函数,那我一开始在执行这个函数的时候传过来的值是没有进行更改的,那我每次递归的时候都将这个k的值减一作为参数来传给我们要递归的函数不就够了嘛,那么我们这里的第k层不就是当函数传过来的那个k等于1 的时候不就是的了嘛,那么我们的代码实现就如下:

int TreeKLevel(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

那么我们这里测试的代码就如下:

int main()
{
	BTNode* root=CreatTree();
	printf("第三层节点的个数为:%d", TreeKLevel( root, 3));
	return 0;
}

在这里插入图片描述
那么我们这里的代码实现就是正确的,我们接着往下看。

二叉树的查找

我们来看看最后一个问题二叉树的查找,那么这个问题想必应该难不倒大家,我们这里思路也是采用递归的方法来进行查找,首先对传过来的地址进行判断如果为空的话我们就返回一个空指针回去,如果找到了我们想要找到的值我们就返回那个值的地址,那么我们这里的代码就如下:

	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}

那么如果我们这里如果既没有找到这个数据也不为空的话我们是不是得往他的子树去查找,那么我们这里首先往他的左子树去查找,既然是查找的话我们这里肯定就得创建一个变量来对其进行一下接收来看他找到了没,如果找到了我们就向上面返回这个地址,如果没有找到我们就不返回,左子树是这样的,那么我们的右子树是不是也该有着相同的步骤啊,那么我们这里的代码就如下:

BTNode* TreeFind(BTNode* root, int x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* lret ,* rret;
	// 先去左树找
	lret = TreeFind(root->left, x);
	if (lret)
	{
		return lret;
	}
	// 左树没有找到,再到右树找
	rret = TreeFind(root->right, x);
	if (rret)
	{
		return rret;
	}
}

但是写到这里还没有结束,如果我们的左子树右子树都没有找到呢?那么我们是不是得返回一个空指针啊,不然我们有时候的递归没有返回值啊,那么我们这里完整的代码就如下:

BTNode* TreeFind(BTNode* root, int x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* lret ,* rret;
	// 先去左树找
	lret = TreeFind(root->left, x);
	if (lret)
	{
		return lret;
	}
	// 左树没有找到,再到右树找
	rret = TreeFind(root->right, x);
	if (rret)
	{
		return rret;
	}
	return NULL;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶超凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值