【二叉树】第二章:实现二叉树相关函数,感受递归分治的魅力

server/2024/11/13 13:56:06/

😎感受递归分治的魅力吧!

本章节一共可以实现下面几个功能函数:

Size 二叉树节点个数、
Height 树的高度、
KNum 第k层节点个数、
Find 查找节点、
Leaves 叶子节点个数、
Destroy 销毁

在这里插入图片描述

前言:本章节函数的实现有些会借助到 前序遍历、层序遍历…等函数
这些函数 在 【二叉树】第一章:相关性质与四种遍历 有讲解
可以先把 第一章内容先学习完



正文

😀 递归分治 思想 与 应用:二叉树相关函数的实现

分治分治,分而治之:将大问题分解成同类型的小问题,解决小问题后,大问题也就解决了

此谓之,递归分治之精髓

遇到需要递归的题目,直接想 递归分治两部曲,这部分看不懂,记住递归分治两部曲,多看章节下部分的多个函数的实现应用,就可以逐步明白理解

递归分治两部曲
1.子问题是什么:不要想其他步骤怎么来的,你就想你当前的子问题 怎么得出来的
2.返回条件是什么(最小子问题 或 递归出口):即最小的子问题到哪结束
同时,在 二叉树章节,递归分治问题中,NULL 都肯定可以算作一个最小子问题,但是往往最小子问题不唯一
⭐例如:算二叉树的 高度
前言:因为二叉树的高度取决于 自己下面的 子树最多可以延伸多少层,所以求 cnt 的最大值
1.子问题:当前节点的 最大高度为: 1 + m a x ( H e i g h t ( r o o t − > L e f t ) , H e i g h t ( r o o t − > R i g h t ) ) 1 + max(Height(root->Left), Height(root->Right)) 1+max(Height(root>Left),Height(root>Right)) : 即当前这层也算一层,加上左右子树的 高度最高的那个
至于左右子树的高度(Height(root->Left), Height(root->Right) 怎么算出来的不要管,你明白子问题是什么,单独处理子问题就行
2.返回条件是什么:当遍历到 叶子节点,已经没有左右子树了,此时已经结束延伸了,可以 return 结束了: i f ( r o o t = = N U L L ) if(root == NULL) if(root==NULL)


一、Size 计算节点总数

递推分治法

1.子问题是什么:

每一个节点代表 以 该节点 为根的子树 的节点个数
每个根节点的节点个数来源有两方面:左子树 和 右子树
return 1 + Size(root->Left) + Size(root->Right);
1 表示 当前这个根节点自己算作一个
Size(root->Left) + Size(root->Right) :表示 左子树节点总数 和 右子树节点总数

2.返回条件是什么

分解成最小子问题:当前根节点的 左右子树都是 NULL,即到叶子节点了 : return 1 + 0 + 0;
这样就收集到了一个节点:return 1;
依次类推,宏观的从叶子节点向上推到累加,最终大树的根节点就是 全部节点的总数了
// 方法:递归分治
// 每个根节点所代表的节点总数 = 1 + 左子树节点个数 + 右子树节点个数
int Size(BTNode* root)
{if (root == NULL) { // 到 NULL return 0 : NULL 这个子树的节点个数 肯定是 0 呀return 0;}return 1 + Size(root->Left) + Size(root->Right);
}


二、Height 算二叉树的 高度

因为二叉树的高度取决于 自己下面的 子树最多可以延伸多少层
因此 我们求 二叉树的高度就是 求从根节点走到叶子节点 的最长路径

方法一、回溯法:

注意两种特例

测试用例一:root = [0] 输出:1

测试用例二:root = [] 输出:0

叶子节点作为最小子问题:容易 非法访问

若下面以 叶子节点 作为最小子问题递归出口( if (root->left == NULL && root->right == NULL)),则这里必须加上 这句话(if (root == NULL) return 0; ),否则 比如 当前遍历到 2 的右节点,是 root == NULL,但是下面的 判断叶子节点的语句就会出现非法访问:NULL 是没有左右节点 的,不能 root->Left 和 root->Right 这样访问

回溯法思路:cnt 逐步记录层数,当遍历到 叶子节点,就记录当前 层数(ans 记录 最大的 cnt)
// cnt 是当前的层数:因为从根节点开始,初始化为 第一层
// ans 是最长的 cnt
int ans = -1, cnt = 1;  
int maxDepth(struct TreeNode* root) {if (root == NULL)  //  遇到 root == NULL 必须返回,若不返回,会进入到下面 判断 root->left 或 root->Right, NULL 本身 是没有左右节点 的,会造成 非法访问 的错误return 0;if (root->left == NULL && root->right == NULL) {   // 当遍历到 叶子节点,就记录当前 层数(ans 记录 最大的 cnt)ans = max(ans, cnt);return 1;}cnt++;  // 进入下一层:层数就 + 1maxDepth(root->left);cnt--;  // 递归退回来了:代表回到上一层了,层数 - 1cnt++;maxDepth(root->right);cnt--;return ans;
}

方法二、递归分治

递归分治两部曲:1.子问题;2.返回条件
1.子问题:当前节点的 最大高度为: 1 + m a x ( H e i g h t ( r o o t − > L e f t ) , H e i g h t ( r o o t − > R i g h t ) ) 1 + max(Height(root->Left), Height(root->Right)) 1+max(Height(root>Left),Height(root>Right)) : 即当前这层也算一层,加上左右子树的 高度最高的那个
至于左右子树的高度怎么算出来的不要管,你明白子问题是什么,单独处理子问题就行
2.返回条件是什么:当遍历到 叶子节点,已经没有左右子树了,此时已经结束延伸了,可以 return 结束了: i f ( r o o t = = N U L L ) if(root == NULL) if(root==NULL)
// 因为上面的 回溯法 以叶子节点作为 最小子问题会  非法访问,下面方法直接就 以 NULL 为最小子问题
int Height(BTNode* root)
{if (root == NULL) {return 0;}return 1 + max(Height(root->Left), Height(root->Right));
}


三、KNum 计算第 k 层有多少个节点

方法一、回溯法:两种写法都行

回溯法思路:cnt 逐步记录层数,当遍历到 第k层时,就记录 ans++ 表示第k层节点又多一个
// 回溯法:两种写法都行
int ans = 0, cnt = 1;
// cnt 为 当前在第几层
// ans 记录第 k 层有多少个节点// 有返回值写法
int KNum(BTNode* root, int k, int cnt)
{if (root == NULL) return 0;if (cnt == k) {  // 若默认开始层为 第0层, 传过来的 cnt == 0 这里就是 cnt == k-1,ans++;return 0;}KNum(root->Left, k, cnt + 1);KNum(root->Right, k, cnt + 1);return ans;
}// 无返回值写法
int cnt = 1; // 默认从第一层开始
void KNum(BTNode* root, int k) {if (root == NULL) return;if (cnt == k) {ans++;return;}cnt++;KNum(root->Left, k);cnt--;cnt++;KNum(root->Right, k);cnt--;
}

方法二、递归分治法

递归分治两部曲:1.子问题;2.返回条件
1.子问题:每一个节点都代表 以当前节点为根的 子树一共有多少个第 k 层节点:左子树第 k 层节点个数 + 右子树第 k 层节点个数
2.返回条件:到了第 k 层, 算一个节点,直接返回 1 :代表找到该层一个节点
// 递归分治法:
// 注意 cnt 为当前层数:初始化值为 1:即 根节点默认为 第一层
int KNum(BTNode* root, int k, int cnt)
{if (root == NULL) return 0;if (cnt == k) {   // 到了第 k 层, 算一个节点,直接返回return 1;}return KNum(root->Left, k, cnt + 1) + KNum(root->Right, k, cnt + 1);
}


四、Find 查找 x 所在的节点

查找功能有两种类型:

1、找到 x 的节点位置,返回指针;
2、找到 x 就返回 true,即只用看 x 在不在

1、找到 x 的节点位置,返回指针;

方法一:前序遍历

bool flag = false;
BTNode* tmp;  // tmp 要么就传地址调用,要么开全局变量,要么使用静态,千万别直接作为 形参传进函数中
BTNode* Find(BTNode* root, int x) {if (root == NULL) {return NULL;}if (flag) return tmp;if (root->value == x) {  // 若找到了,将 flag 置为true,便于上一行的 if(flag) 将递归函数一直回退出去flag = true;tmp = root;  // 临时变量 tmp 记录 root return tmp;}Find(root->Left, x);Find(root->Right, x);return NULL;  // 最后都没有 return 回去,说明都找不到,这里就 return NULL
}

方法二:递归

递归分治两部曲:1.子问题;2.返回条件
1.子问题:以当前节点为 根的子树,是否找到 == 左子树是否找到 或 右子树是否找到
2.返回条件:一旦找到 目标,返回该节点的 地址,即 root
// 方法二:BTNode* Find(BTNode* root, int x) {if (root == NULL) {return NULL;}if (root->value == x) {return root;}BTNode* res1 = Find(root->Left, x);if (res1) return res1;BTNode* res2 = Find(root->Right, x);if (res2) return res2;return NULL; // 最后谁也没找到,就返回 NULL; 
}

2、找到 x 就返回 true,即看 x 在不在

按位或 || : 只要 一个找到 return true, 另一个就不会进行了,直接总体 return true
一旦函数 Find() 存在一次 true 就会 一直 return 到最外层,直到返回到 main 函数中,

bool Find(BTNode* root, int x)
{if (root == NULL) return false;if (root->value == x) return true;return Find(root->Left, x) || Find(root->Right, x);
}


五、Leaves 计算 叶子节点个数

其实这个算法 和 【KNum 计算第 k 层有多少个节点】是一样的,只是 ”2.返回条件“ 不同罢了

方法一:

int ansLeaves = 0;
void  Leaves(BTNode* root)
{if (root == NULL)return;if (root->Left == NULL && root->Right == NULL) {ansLeaves++;return;}Leaves(root->Left);Leaves(root->Right);
}

方法二:递归分治法

1.子问题:叶子节点总数 = 左子树叶子节点数量 + 右子树叶子节点数量
2.返回条件:到叶子节点就 return 1 代表找到一个
int Leaves(BTNode* root)
{if (root == NULL) return 0;if (root->Left == NULL && root->Right == NULL) return 1;return Leaves(root->Left) + Leaves(root->Right);
}


六、Destroy 二叉树的销毁

前序遍历可以销毁,但是相对比较麻烦
前序销毁,销毁当前根节点前,必须先保存前后节点,再递归,相对麻烦
最优选择是 后序遍历
// 采用后序遍历 // 错误写法:不能直接对 root 置为空指针,此处的root是形参
void TreeDestroy(BTNode* root)
{if (root == NULL) return;TreeDestroy(root->Left);TreeDestroy(root->Right);free(root);root = NULL;   // 不能直接对 root 置为空指针,此处的root是形参
}// 两种正确写法:1.传二级指针;2.函数里面只free节点,在外面再将根节点(第一个根)置为空
void TreeDestroy(BTNode* root)
{if (root == NULL) return;TreeDestroy(root->Left);TreeDestroy(root->Right);free(root); // 函数里面只free节点
}
/*
int main()
{// .....TreeDestroy(root);root = NULL;  // 在外面再将根节点(第一个根)置为空
}
*/
// 

为什么只需要对第一个根节点置空,而二叉树内部的其他根节点无需置空呢?

解释:平时我们需要指针置为空原因是:我们自己的程序操作,可能会不小心再次使用到 free掉的那片空间的指针,从而出错(指针不为空,但空间又被释放了,(你的房间钥匙还在,但是你的房间 却被炸没了(doge),那你的再次使用钥匙,就会使系统出错)),因此要置空,而本题中人为已经取不到里面的指针了,一般出不了错,(将你的房间给释放掉,房间钥匙给置为NULL之后,这样你原本房间里面 的若有个柜子 即使有该柜子钥匙,你也拿不到钥匙了(注:该柜子钥匙和在房间里面),也就一般不会出错了)所以只关心空间有没有 free 掉(防止内存泄露)就行了

当最后一个节点的内存被释放后,整个二叉树已经不存在了。于是我们在外部将表示整棵树的指针设置为NULL,是为了确保后续操作不会误操作一个已经销毁的树结构,(即先炸掉房子,再丢掉房子的钥匙)



七、IsCompleteBinaryTree 判断是否为完全二叉树

以下两种方法都基于 层序遍历:bfs

前言:

完全二叉树有个特性:从第一个非空节点到最后一个非空节点,中间是不可能出现 空节点 的

方法一:中途判断

我写的
思路:先算二叉树的非空节点个数,再层序遍历二叉树,每遍历过一个非空节点,k–,
当中途遇到一个 NULL,但是 k>0 (表示 非空节点 的 连续序列 中出现了一个 NULL:即代表该二叉树不是完全二叉树)
// 层序遍历:就是用 bfs
BTNode* arr[2];
bool IsCompleteBinaryTree(BTNode* root, int k)
{Queue q;QueueInit(&q);if (root != NULL) {QueuePush(&q, root);}while (!QueueEmpty(&q)) {BTNode* tmp = QueueFront(&q);QueuePop(&q);if (tmp != NULL) {printf("%d ", tmp->value);k--;}else {if (k > 0) return false;  //  当中途遇到一个 NULL,但是 k>0 ,代表该二叉树不是完全二叉树continue;}arr[0] = tmp->Left, arr[1] = tmp->Right;for (int i = 0; i < 2; ++i) {BTNode* t = arr[i];//if (t == NULL) continue; // 不阻碍 t 为 NULL的继续,否则就难判断中途有无NULL了QueuePush(&q, t);}}printf("\n");QueueDestory(&q);return true;
}
int main()
{BTNode* root = TreeCreate();// 算二叉树的非空节点个数int size = Size(root);  // 这是章节前面 计算二叉树节点个数的函数 Size()bool flag = IsCompleteBinaryTree(root, size);if (flag) printf("Ture\n");else printf("False\n");
}

方法二:遇到N再向后判断

老师的
思路:一直层序遍历,遇到 NULL break循环,再继续向后遍历,一旦遇到一个非空的,就 不是完全二叉树
BTNode* arr[2];
bool IsCompleteBinaryTree(BTNode* root)
{Queue q;QueueInit(&q);if (root != NULL) {QueuePush(&q, root);}while (!QueueEmpty(&q)) {BTNode* tmp = QueueFront(&q);QueuePop(&q);if (tmp != NULL) {printf("%d ", tmp->value);}else break;arr[0] = tmp->Left, arr[1] = tmp->Right;for (int i = 0; i < 2; ++i) {BTNode* t = arr[i];//if (t == NULL) continue; // 不阻碍 t 为 NULL的继续,否则就难判断中途有无NULL了QueuePush(&q, t);}}// 循环遍历判断后面是否有非空节点while (!QueueEmpty(&q)) {BTNode* t = QueueFront(&q);QueuePop(&q);if(t != NULL)return false;}printf("\n");QueueDestory(&q);return true;
}


http://www.ppmy.cn/server/15161.html

相关文章

【鸿蒙开发】闪屏页面练习

1. 创建页面 Index.ets Entry Component struct Index {build() {Column() {Text("首页").fontSize(50).fontWeight(FontWeight.Bold)}.width(100%).height(100%)} }2. 创建页面 SplashScreen.ets Entry Component struct SplashScreen {State message: string Sp…

使用微信开发者工具模拟微信小程序定位

哈喽&#xff0c;各位同僚们&#xff0c;我们平时在测试微信小程序的时候&#xff0c;如果小程序中有获取定位或者地图的功能&#xff0c;测试场景中常常需要去模拟不同的位置&#xff0c;例如我们模拟在电子围栏的外面、里面和边界区域等。那么&#xff0c;我们如何在模拟微信…

python数字验证码自动识别

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在网络上&#xff0c;许多网站和应用程序使用验证码&#xff08;Completely Automated Publ…

MySql CPU激增原因分析

QPS激增会导致CPU占用升高 分析 可以使用监控工具&#xff0c;查看CPU利用率曲线图和QPS曲线图进行对比。如果CPU曲线图波动情况跟QPS曲线图波动情况基本保持一致&#xff0c;可以明确明确CPU升高时QPS上升导致。反之&#xff0c;CPU曲线图对比QPS曲线图有不同步的峰值抖动&am…

【教程】MySQL数据库学习笔记(五)——约束(持续更新)

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 【MySQL数据库学习】系列文章 第一章 《认识与环境搭建》 第二章 《数据类型》 第三章 《数据定义语言DDL》 第四章 《数据操…

Ubuntu 20.04.6下载、安装

一、下载 下载地址&#xff1a;https://cn.ubuntu.com/download 下载版本&#xff1a;ubuntu-20.04.6-desktop-amd64.iso 二、安装 参考博客&#xff1a; https://blog.csdn.net/lhl_blog/article/details/123406322 https://www.cnblogs.com/fieldtianye/p/17879840.html…

C语言(1):初识C语言

0 安装vs2022 见 鹏哥视频即可 1 什么是C语言 c语言擅长的是底层开发&#xff01; 现在一般用的是C89和C90的标准 主要的编辑器&#xff1a; 2 第一个C语言项目 .c 源文件 .h头文件 .cpp c文件 c语言代码中一定要有main函数 标准主函数的写法&#xff1a; int main() { …

GitHub的使用

文章目录 一、什么是Git1.1、与其他版本控制系统的区别概念上的差异本地操作数据的完整性附加模型 1.2、三种状态和基本Git工作流程Git的基本工作流程 二、首次Git设置2.1、Git的安装&#xff08;Linux&#xff09;2.2、Git的安装&#xff08;Windows&#xff09;2.3、Git配置2…