前言:
在熟练掌握二叉树四种基本遍历方法的基础上,本文将深入探讨以下进阶问题:节点总数统计、叶子节点计算、第k层节点数量确定、节点的查找以及树高测量。
这些内容将帮助读者深化对二叉树结构的理解与应用能力,以及深入理解递归分治思想。

一、前置说明:
本文所描述的二叉树都是链式二叉树,其定义方式如下所示:
typedef char BTDataType;
typedef struct BinaryTree
{
BTDataType data;
struct BinaryTree* left;
struct BinaryTree* right;
}BTNode;
二、二叉树的创建及销毁
通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树,其中'#'表示该节点为NULL,二叉树如下图所示:

前序遍历的思想为: 先访问根节点 -> 再访问左子树 -> 最后访问右子树
依照前序遍历的思想,我们可以得出核心构建二叉树的逻辑:“先处理当前节点,再递归构建左子树,最后递归构建右子树 ”。
BTNode* BinaryTreeCreate(char* a, int n, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
核心逻辑:整个递归从根节点 A 开始,按 “当前节点→左子树→右子树” 的前序逻辑推进
①先取 A 为节点,递归构建其左子树(以 B 为节点)。
②B 节点下先递归构建左子树(D 节点,左右均为 #则返回),再构建右子树(E 节点,左为 #,右递归到 H 节点,H 左右均为 #则返回)。
③A 的右子树以 C 为节点,递归构建左子树(F 节点,左右均为 #返回)和右子树(G 节点,左右均为 #返回),遇 #则终止当前分支递归,逐层完成构建。
对于二叉树的销毁而言,我们需要按照后序遍历的思想:先访问左子树 -> 再访问右子树 -> 最后访问根节点
这里有帅观众问,为什么一定需要按照后序的遍历思想?
答:若按照前序遍历 或者 中序遍历的思想,根节点会提前释放,导致左子树和右子树所开辟的空间不能被释放,造成内存泄漏的严重后果。
依照后序遍历的思想,我们可以得出销毁二叉树的逻辑:“先递归处理左子树,再递归处理右子树,最后销毁根节点 ”。
void TreeDestory(BTNode** root)
{
if (*root == NULL) return;
//销毁左树
TreeDestory((*root)->left);
//销毁右树
TreeDestory((*root)->right);
//销毁根
free(*root);
*root = NULL;
}
三、二叉树的结点统计与高度计算
温馨提示:下文中对如图所示的二叉树进行节点与高度的计算

3.1二叉树节点总数的统计
思路一: 通过定义计数变量,通过遍历整棵二叉树进行统计节点个数。
思路二:利用分治思想,结合递归函数,将大问题化成若干个子问题。
整棵树的结点总数 = 左子树结点数 + 右子树结点数 + 1(根结点),空树结点数为 0。
思路一看似很合理,但实际上会出现问题
具体问题如下:
①若使用局部变量:
递归遍历左 / 右子树时,每层递归的局部计数变量会被重新初始化,无法累计整棵树的节点数
②若使用全局 / 类成员变量:
虽然能累计计数,但多次调用统计函数时,全局变量不会自动重置,会导致后续统计结果错误。
例如:统计了A树的节点个数,再统计B树的节点个数就会因没重置计数变量,而导致统计结果错误。
下面基于思路二的思想进行代码展示:
//树的节点个数
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
3.2叶子节点的计算
思路:①由叶子结点是 “左、右子树均为空” 的结点,得出判断条件
②由分治思想,将大问题化成若干个子问题,整棵树的叶子结点数 = 左子树叶子结点数 + 右子树叶子结点数。
//叶子节点个数
int TreeLeafSize(BTNode* root)
{
//若根节点为空,直接返回0
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
3.3第k层节点的数量
思路:①由分治思想,将大问题化成若干个子问题,第k层节点数 = 左子树的第k-1层节点数 + 右子树的第k-1层节点数。
②明确最小子问题: 若k = 1 → 只有根节点,数量为1;
1.生活中的示例:公司职级
假设我们有一个大公司(一棵大树):
第 1 层: 总公司 CEO(整棵树的根结点 root)
第 2 层: 两位副总裁:左副总(root->left)和右副总(root->right)
第 3 层: 各部门总监
第 4 层: 各部门经理
假设你是CEO,你的目标是:计算整个公司“第 4 层(k=4)”有多少个经理。
CEO(根结点)不想自己去数整栋楼,于是他把任务拆分,分配给了他的左副总(左子树)和右副总(右子树)。
对于 CEO 来说,目标是总公司的第 4 层。
但是,当任务交到左副总手里时,参考系变了!
在左副总自己分管的“左边分公司(左子树)”里,左副总认为自己是第 1 层(左子树的根结点)。
那么,总公司里的第 4 层经理,在左副总的独立分公司里,排在第几层呢? 答案是:第 3 层。
所以,CEO 对两位副总下的命令并不是“去数第 4 层”,而是:
“左副总,去数出你那个分公司里的第 3 层(k-1)有多少人。”
“右副总,去数出你那个分公司里的第 3 层(k-1)有多少人。”
然后 CEO 把他俩汇报上来的数字加在一起,这就是整个公司第 4 层的人数。
2. 回到二叉树的数学和逻辑
在二叉树中,“左子树”并不是大树的一部分残肢,而是一棵拥有自己新根结点(原来的左孩子)的完整的树。
当我们说“左子树第 k-1 层”时,实际上是在经历一次坐标系的重新映射:
绝对距离: 目标层(第 k 层)距离大树的根结点,需要往下走 k-1 步(跨过 k-1 条边)。
已经走的一步: 当代码执行到
root->left时,我们已经从大树的根往下走了一步,站在了左孩子上。
剩余距离: 此时,我们距离真正的目标层,还剩下 (k-1) - 1 = k-2 步。
重新编号: 现在,我们把左孩子当成一棵新树的“第 1 层”。在这棵新树里,往下走 k-2 步到达的层数,就是 1 + (k-2) = k-1 层。
int TreeLevelKSize(BTNode* root, int k)
{
//第k层 的节点数 ->第k-1层的节点数 ->第k-1层左子树+第k-1层右子树的节点数
if (root == NULL|| k<0) return 0;
//第一层的节点数为1
if (k == 1) return 1;
return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);
}
3.4二叉树的高度测量
思路:由分治思想,将大问题化成若干个子问题,二叉树的高度=max( 左子树 , 右子树 )+ 1
写法一:
//树的高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return max(TreeHeight(root->left),TreeHeight(root->right))+1;
}
写法二:
//树的高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
错误写法:因为没有记录左右子树的高度,导致需要进行多次重复冗余的递归,使得增加栈溢出的风险。
//树的高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}
3.5节点的查找
思路: ①由分治思想,将大问题化成若干个子问题,将在整棵树查找节点-> 根查找 左子树查找 右子树查找
②边界条件:遇到空子树返回NULL,遇到值等于查找目标返回该节点。
③温馨提示:在查找到目标节点需要进行保存后,逐层返回。
BTNode* TreeFind(BTNode* root, BTDataType x)
{
//查找到空节点直接返回
if (root == NULL) return NULL;
//查找到目标节点的判定
if (root->data == x) return root;
BTNode* retleft = TreeFind(root->left, x);
//在左子树找到,保存并直接返回
if (retleft) return retleft;
BTNode* retright = TreeFind(root->right, x);
//在右子树找到,保存并直接返回
if (retright) return retright;
//左右子树都没找到
return NULL;
}
3.6测试函数功能
void TestFun()
{
char a[] = "ABD##E#H##CF##G##";
int sz = sizeof(a) / sizeof(char);
int i = 0;
BTNode* root = BinaryTreeCreate(a, sz, &i);
// 测试各功能
printf("节点总数为:%d\n", TreeSize(root)); // 预期8
printf("叶子节点数为:%d\n", TreeLeafSize(root)); // 预期4(D、H、F、G)
printf("树的高度为:%d\n", TreeHeight(root)); // 预期4(A→B→E→H)
printf("第3层的节点数:%d\n", TreeLevelKSize(root, 3)); // 预期4(D、E、F、G)
// 测试查找功能
BTNode* findNode = TreeFind(root, 'H');
if (findNode)
{
printf("找到节点:%c\n", findNode->data);
}
else
{
printf("未找到节点\n");
}
// 销毁二叉树
BinaryTreeDestroy(root);
root = NULL;
}
既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。


529

被折叠的 条评论
为什么被折叠?



