【数据结构指南】高频二叉树节点问题

【数据结构指南】高频二叉树节点问题

前言:      

        在熟练掌握二叉树四种基本遍历方法的基础上,本文将深入探讨以下进阶问题:节点总数统计、叶子节点计算、第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层节点数。

            ②明确最小子问题:若树为空(根节点null)或k < 1 → 第k层节点数为0;   若k = 1 → 只有根节点,数量为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; }

既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。

Read more

大家有空就去看这份前端宝典,真的能提高level

如果你感觉刷了无数八股文、背了各种框架API,面试时依然被问到哑口无言——问题可能不在于你不够努力,而在于你努力的方向,恰好错过了当前面试真正的筛选逻辑。 如今的前端面试,已经形成了一套高度标准化的「能力探测模型」: * 广度覆盖:从 JavaScript 核心原理,到框架设计思想,再到工程化、性能优化、网络乃至基础算法。 * 深度追问:不再满足于“知道是什么”,而是连续追问“为什么这样设计”与“如何自己实现”。 * 场景串联:将零散的知识点置于一个虚拟的高并发、高性能业务场景中,考察你的系统化思维。 这导致了一个必然结果:依靠碎片化学习和临时记忆,已难以通过中高级岗位的面试。 你需要一份结构化、有深度的知识体系。 八股文涵盖:JavaScript、CSS、HTML、React、Vue、算法、计算机网络、Node.js、TypeScript、性能优化、前端安全、小程序、ES6、编程题、设计模式、

By Ne0inhk

PMBus电压监测精度提升:核心要点之ADC前端电路

PMBus电压监测为何不准?揭秘ADC前端电路的设计玄机 你有没有遇到过这种情况:系统明明工作正常,PMBus上报的 READ_VOUT 却显示输出电压波动剧烈?或者在高低温环境下,电源监控数据“飘”得离谱,触发误告警?更让人头疼的是——换了个MCU、改了块PCB,同样的电源模块读数居然对不上。 问题往往不在于PMBus协议本身。 真正的“罪魁祸首”,藏在你看不见的地方: ADC前端模拟电路 。 为什么高分辨率ADC也救不了你的PMBus? 现代数字控制器普遍集成12位甚至16位ADC,理论精度看起来非常可观。但实际应用中,很多系统的有效位数(ENOB)只有8~10位,甚至更低。这意味着你花大价钱买的“精密测量”能力,被前端电路白白浪费了。 根源就在于: PMBus是数字总线,但它监控的是模拟世界 。 从真实电压到 READ_VOUT 字段之间的这段路径——也就是ADC前端电路——决定了最终数据的可信度。 举个例子:某通信设备使用12V供电,通过分压电阻接到MCU的ADC引脚。如果前端设计不当,哪怕ADC本身误差只有±1LSB,

By Ne0inhk
双剑破天门:攻防世界Web题解之独孤九剑心法(七)

双剑破天门:攻防世界Web题解之独孤九剑心法(七)

免责声明:用户因使用公众号内容而产生的任何行为和后果,由用户自行承担责任。本公众号不承担因用户误解、不当使用等导致的法律责任 **本文以攻防世界部分题为例进行演示,后续会对攻防世界大部分的web题目进行演示,如果你感兴趣请关注** 目录 一:Newscenter 二:upload1 三:Xff_referer 四:Command_execution 五:总结 1. Newscenter(SQL注入) 2. upload1(文件上传漏洞) 3. Xff_referer(HTTP头伪造) 4. Command_execution(命令注入) 一:Newscenter 打开为如下所示 经过尝试,得知在输入框中输入数字可得到不同内容 输入23就没有新闻 所以我们得知这个输入框和数据库有交互,那这题考察的可能就是SQL注入 发现将数据库中所有的内容都查询了出来,那这个题考察的就是SQL注入 字段长度为3 23' order by

By Ne0inhk
年度心得总结——前端领域

年度心得总结——前端领域

又是一年时光转,岁月如梭学习繁。 笔耕岁月求知路,心悟真谛志愈坚。 往昔耕耘结硕果,未来展望展宏愿。 共聚一堂话成就,再创辉煌谱新篇。 此刻,我暂且搁下手中的键盘,让思绪飘回那过往的日日夜夜。回望这一年的风雨兼程,心中不禁涌动着无尽的感慨。前端领域,这片充满无限可能的天地,又经历了一轮轰轰烈烈的蓬勃发展与变革。新技术如雨后春笋般涌现,旧框架在不断迭代中焕发新生,这一切都让我对这份事业充满了无尽的热爱与敬意。 同样是在这流转的一年里,我踏上了ZEEKLOG技术博主的星辰大海之旅,愿以我余温之烛,照亮同行者的征途,期盼自己能成为ZEEKLOG夜空中那颗即便只刹那闪耀,亦能点亮梦想的星辰。 文章目录 * 一、React 框架 * (一) React 优化 * (二) 开发效率提升 * (三) 服务端渲染(SSR)集成 * (四) 其他重要优化和功能支持 * 二、Vue 框架 * (一) Vue 版本与维护方面 * (二) 性能优化与增强 * 三、技术探索

By Ne0inhk