一.节点个数
上一篇对于树的遍历,进行了暴力构造树,那么该如何知道树有几个节点呢?
最简单的办法就是创建一个计数器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值得地址,而不是值.
所以稍微的总结一下,对于递归的理解,是建立栈帧,而递归的返回不是一步完成直接返回给最外层输出,而是层层返回,每返回一层就会销毁上一层.