数据结构之二叉树相关的计算

一.节点个数

上一篇对于树的遍历,进行了暴力构造树,那么该如何知道树有几个节点呢?

最简单的办法就是创建一个计数器size,跟随递归遍历进行统计.这是最简单最容易想到的办法,但不是最高效的办法.因为每次调用递归,创建一块函数栈帧,都有一个size.

那么这个思路的问题就在每个栈帧都会有个size,却没有累加起来,用的都是自己的size.

int TreeSize(TreeNode* root)
{
	int count = 0;
	if (root != NULL)
	{
		++count;
	}
	else
	{
		return;

	}
	TreeSize(root->left);
	TreeSize(root->right);
	return count;
}

那么你也许会想到加上static静态修饰变量.这个确实可以实现.但当调用第二次被static修饰的size时,会发现size变成了打印之前的两倍.因为局部的静态变量只会初始化一次,他的生命周期会在整个程序进程完成后才结束.所以用这种方法,在同一棵树或者不同树调用就会出问题.

而且size的访问区域只在被声明的函数里才能使用,不太可以在外面的函数进行修改.

所以转变一下,使用全局变量来进行统计,在每次调用函数前,先将size置为0即可.

//二叉树的节点个数
int size = 0;
void TreeSize(TreeNode* root)
{
	if (root == NULL)
	{
		return;
	}
	size++;

	TreeSize(root->left);
	TreeSize(root->right);
}


int main()
{
	size = 0;
	TreeSize(root);
	printf("%d\n", size);


	size = 0;
	TreeSize(root);
	printf("%d\n", size);
}

再来看一个比较好的方法,这里进行换行,方便阅读.

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

翻译一下,如果根节点为空那么返回0,如果不为空,则返回左子树的节点的个数加上右子树的节点的个数再加1(根节点).

形象一点,这就好比管理层两级的划分.在一所大学里,校长想统计学校人数,先派给两个院长,院长再分别分配给辅导员,导员再给班长,班长统计之后,再往回走.最后回到校长手上.

递归路线梳理:

走到根1,根再走到左子树2,再走到2的左子树3,进到3的左子树,为空返回0,再进右子树,也返回0.所以3这个节点的递归,就是左右子树各为0,再加上自己,结果就是1.

2的左子树返回了1,而2的右子树为空,返回0.再加上自己.那么1的左子树就是返回2.

走到1的左子树,走过4和5,来到5的左子树为空,返回0.右子树为空,返回0.加上自己,4的左子树就是1.同理4的右子树也是1,最后加上4自己.那么1的右子树就是3.

最后左子树的2加上右子树的3,再加自己.就是6.

二.叶子节点个数

继续用递归来实现,需要注意两点:

子问题的分治:

        左子树叶子节点个数 + 右子树叶子节点个数.

返回条件:

        1.空则返回0,

        2.叶子返回1

先来一个反面教材:

//叶子节点个数
int TreeLeafSize(TreeNode* root)
{
	return !root->left && !root->right ? 1 : TreeLeafSize(root->left) + TreeLeafSize(root->right);

}

首先如果是空树就已经不行了.

接着来递归分析:

进入树,一直来到2的左子树3,且3的左右子树均为空,则判断3为叶子,返回1.2的左子树结束,进入右子树,为空.那么就是这里程序出现了问题.返回条件出现了bug.

所以这个写法不仅仅空树不可行,而且有一个空结点也不行.返回条件有两个.这里可以分开来写.

来看纠正后的代码:

//叶子节点个数
int TreeLeafSize(TreeNode* root)
{
	//空,则返回0
	if (root == NULL)
	{
		return 0;
	}

	//不是空,是叶子,返回1
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}

	//不是空,也不是叶子则分治=左右子树叶子之和.
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

三.二叉树的高度

还是先来分析递归的两个问题:

  先思考,如果已知根的左子树的高度为2,右子树的高度为3.那么这棵树的高度为多少?

子问题分治:     

        1.空树,返回0.

        2.不是空,返回左右子树最大的高度,并加1.

那么该如何算出左右子树的高度呢?

模仿上面的举例,这次校长需要统计学校身高最高的,与上面的交接任务相同,最后到班长,收集班上最高的一个,返回给导员,导员经过对比每个专业,筛选最高的,返回给院长,院长再经过筛选,最终返回到校长手里的就是最高的.

所以这个高度问题也需要经过筛选操作.那么带入到图中.要知道1的高度,那就要知道2和4谁更高,得到2的高度就需要知道3的高度,3的左右子树为空,返回0,最后3返回给2的是0+1的高度.所以2的左子树高度为1,来到2的右子树,为空树,返回0.最后对比左右子树的高度,再加1,得到1的左子树的高度为2

1的右子树同理,不再赘述.返回的是3.最后对比,最高的是3,再加1.所以这棵树的高度就是4.

先来看一个反面教材:

//二叉树的高度计算
int TreeHeight(TreeNode* root)
{
	return root == NULL ? 0 : 
		TreeHeight(root->left) > TreeHeight(root->right) ? 
		TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;

}

这里有一个比较致命的问题就是,只进行了计算,没有进行保存.就会导致数据丢失.

换句话说就是这上面的人都有健忘症.上次给他传的结果会忘记一次.当院长1和2,分别返回数据后,校长会忘记一次数据.那院长只能又重新向下递归走一次.到了导员,也会忘了,再让班长进行统计传给导员,导员再给院长,最后给校长,这样就不会忘了.

单从描述来看,可能会觉得这个过程是原来的2倍,但远远不是,因为这里每一层都会忘记.

当班长1和2返回给导员时,导员会忘记一次.因为函数中只写了,将左右子树进行比较,没有保存数据,那么想要拿到数据就还需要递归调用一次高的,再返回才会得到数据.右边的情况相同.

当导员1和2返回给院长时,院长也会忘一次,也会让数据高的再传一次.假设导员1高,又会让导员去让班长统计,返回导员,这里导员也会忘记一次,让高的再传一次.这样折磨一趟,院长才能拿到数据.

这里只是阐释错误写法会造成的结果,可忽略这段内容.


正确写法:

//二叉树的高度计算
int TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

或者喜欢使用三目运算符可以使用这种写法.运用fmax函数,但需要运用math库

#include<math.h>

//二叉树的高度计算
int TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;

	return fmax(TreeHeight(root->left) , TreeHeight(root->right)) + 1;
}

四.第k层节点个数

例如 : 当需要求第3层的节点个数,可以转换成求第一层根左右子树的第二层的节点个数.

分治子问题:

        1.空,返回0.

        2.不为空,且k == 1.返回1.

        3.不为空,且k > 1, 返回  (左子树的k-1层) + (右子树的k-1层).

//求二叉树的第k层节点个数
int TreeLevelK(TreeNode* root,int k)
{
	assert(k > 0);
	//空树,返回0
	if (root == NULL)
		return 0;

	//不为空,且k == 1
	if (k == 1)
		return 1;

	//不为空,且k > 1
	return TreeLevelK(root->left, k - 1) + TreeLevelK(root->right, k - 1);
}

时间复杂度最差就是将整棵树遍历一遍为O(N).

四.二叉树查找值为x的节点

进行查找,可以使用前中后序,都可以实现查找.最好使用前序,当找到x后就可以直接返回,没有必要继续往下找.

继续先看最常见的代码,反面教材:

//二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return x;

	TreeFind(root->left, x);
	TreeFind(root->right, x);
}

先说明,这种写法在部分编译器上会报警告或者错误.

那这棵树来举例,当x输入的是3.运行环境是vs2022.警告是:不是所有的控件路径都有返回值.

//二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return x;

	TreeFind(root->left, x);
	TreeFind(root->right, x);
}

这个写法的本质问题就在认为当data等于x后,就立马返回给最外面.但左右子树都找完了,后面却没有返回值,所以才会报错.可以在最后加上返回空.

那么警告解决了,再继续运行会发现,打印出来的值却不对.明明有这个值却找不到.来看详细的递归展开图.

首先传入的参数是根1和要找的节点值3.一开始就找不到,所以进入1的左子树.

递归到了左子树2,也不是要找的3.再递归到2的左子树3.

递归之后发现左子树的值就是要找的3.那判断条件成立,返回这个值.但需要注意,这里if返回的值,是不是直接返回到最外面进行输出的吗?答案很明显不是,递归的返回值是一层一层进行返回.那么当返回要找的值后.但没有进行接收,这个值就会被丢失.所以代码需要进行改进.

//二叉树查找值为x的节点
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return x;

	TreeNode* ret1 = TreeFind(root->left, x);
	if (ret1)
	{
		return ret1;
	}
	TreeNode* ret2 = TreeFind(root->right, x);
	if (ret2)
	{
		return ret2;
	}

    return NULL;
}

再来看递归展开图:现在假设要找的是5.

首先找的是1.不是要找的,走左子树2,也不是,继续2的左子树3.

3也不是,再到3的左子树,为空则返回空.3再找右子树,也为空,返回.那么3这棵树就没有找到,意味着2的左子树没有找到,所以来到2的右子树,为空返回,再返回给1,在中1进行判断ret1,为空不返回,则继续执行.这样在1的左子树就没有找到.

再来到1的右子树4不是要找的,进到4的左子树5是要找的数,那么一层一层把结果返回,就不需要再进到4的右子树进行查找.

最后返回打印的是x值得地址,而不是值.


所以稍微的总结一下,对于递归的理解,是建立栈帧,而递归的返回不是一步完成直接返回给最外层输出,而是层层返回,每返回一层就会销毁上一层.

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值