hehe在二叉树这里,我们经常要使用递归来解决问题,所以深刻理解递归解决问题的过程是极为重要的,在这里我不详细展开递归的介绍,我想先让你们深刻理解递归是如何将问题缩小,最终拆解成小问题并解决的。
hehe我们可以举这样例子来理解:一个学校要统计报名参加六级考试的人数,校长将任务分给院长,院长就要求自己院的人数,院长就把任务分给辅导员,辅导员就要求自己管理班级的人数,辅导员又把任务分配给班长,班长就很轻松的得到自己班级的人数,班长汇总给辅导员,辅导员汇总给院长,院长汇总给校长,校长得到最终的结果。
hehe在上级向下级分配任务的过程就是“递”的过程,在不断的拆解问题,而下级将任务结果返回给上级就是“归”的过程,从而得到最终的结果。
这是函数递归具体介绍和相关练习帮助理解的文章想看具体有关函数递归内容的老铁请点该链接
hehe还要再声明的一点就是,在代码的注释中,由书写代码要注意的问题,和在实现过程中仍然需要注意的小细节,所以要仔细阅读注释,才能真正理解所有题目是如何实现的
首先先将树的结构体介绍一下
typedef struct BTNode //树的结点的结构体
{
BTDataType x; //存该结点的值 //BTDdataType是指存储的数据类型
struct BTNode* leftchild; //储存左孩子的地址
struct BTNode* rightchild; //储存右孩子的地址
}BTNode;
(1)前序遍历:对于每一个结点,先访问该结点本身,再去访问它的左孩子,最后再去访问它的右孩子。
1:先访问本身1,再去访问1的左子树
2:先访问2本身,再去访问2的左子树4
3:先去访问4本身,再去访问4的左子树
4:4的左子树为空,所以返回至4
5:再去访问4的右子树,右子树为空,所以返回至4,此时4已经访问结束,返回至2,再去访问2的右子树
6:先访问5本身,再去访问5的左子树
7:左子树为空,返回至5
8:再去访问5的右子树,右子树为空,所以返回至5,此时5已经访问结束,所以返回至2,2也访问结束,所以返回至1
9:再去访问1的右子树3,先去访问3本身
10:再去访问3的左子树,左子树为空,返回至3,再去访问3的右子树
11:3的右子树为空,返回至3,3已经访问结束,返回至1,1访问结束,完成遍历
就是始终保证一个原则,就是遇到每一个结点先访问自己,再去访问它的左子树,然后再先访问左子树的根,再去访问左子树的根的左子树。。。无限套娃。当遇到空结点时时就返回,再去访问右子树,对于右子树也是先访问它的根,再去访问根的左子树。。。当一个结点右子树也遍历结束,那么这个结点就遍历结束。返回至它的双亲结点。当整棵树的根的右子树也遍历结束,说明整个树遍历结束。
(2)中序遍历:对于每一个结点,先访问该结点的左子树,再访问该节点本身,最后访问该节点的右子树
void InOrder(BTNode* root)
{
if (root == NULL) //访问到空树,直接返回
{
printf("NULL ");
return;
}
InOrder(root->leftchild);//先去访问根的左子树(会不断递归,直到叶子结点)
printf("%c ", root->x); //再访问根本身
InOrder(root->rightchild);//最后访问根的右子树(会不断递归,直到叶子结点)
}
(3)后序遍历:对于每一个结点,先访问该结点的左子树,再访问该节点的右子树,最后访问该节点本身。
void PostOrder(BTNode* root)
{
if (root == NULL) //遇到空树直接返回
{
printf("NULL ");
return;
}
PostOrder(root->leftchild); //先访问根的左子树(直到叶子节点)
PostOrder(root->rightchild); //再访问根的右子树(直到叶子节点)
printf("%c ", root->x); //最后访问根
}
我们始终要牢记,在二叉树这里,我们能用递归就要使用递归,将大问题分解成一个一个小问题,简化问题,才能更加思路清晰的写代码实现相应功能。
而对于求结点个数的问题,我们可以拆解问题。
求:左子树的结点个数+右子树的结点个数+自身根
而左子树又可以分为:左子树的结点个数 = 左子树的左子树结点个数+左子树的右子树结点个数+左子树自身
右子树又可分为:右子树的结点个数 = 右子树的左子树结点个数+右子树的右子树结点个数+右子树自身
int BTNodeSize(BTNode* root)
{
//如果访问到空树,说明没有结点,那么值返回0
if(root == NULL)
return 0;
//不是空树,就计算左子树个数+右子树个数+自身的1个
return BTNodeSize(root->left) + BTNodeSize(root->right) + 1;
}
叶子结点就是左右孩子都是空的结点。也是不断拆解问题
树的叶子结点个数 = 左子树的叶子结点个数 + 右子树的叶子结点个数
左子树的叶子结点个数 = 左子树的左子树叶子结点个数 + 左子树的右子树叶子结点个数
右子树的叶子结点个数 = 右子树的左子树叶子结点个数 + 右子树的左子树叶子结点个数
不断套娃套娃
int BTLeafSize(BTNode* root)
{
//访问到空结点,返回值0
if (root == NULL)
return 0;
//如果是叶子结点 root->left == NULL root->right = NULL
if (root->leftchild == NULL && root->rightchild == NULL)
return 1;
//计算对应左子树+右子树叶子结点并返回
return BTLeafSize(root->leftchild) + BTLeafSize(root->rightchild);
}
hehe还是一样,直接去求第K层的结点个数对于二叉树来说是很难做到的,我还是需要用分治的思想,使用递归的方式来解决问题。
hehe求相对于第一层求第K层,就是先对于第二层求K-1层……直到K=1时,转变成求该层的结点个数,这样就把问题彻底拆解,变成小问题了。
int BTKLevelSize(BTNode* root, int k) //k值代表层数
{
if (root == NULL) //当访问到空时,说明无,返回0
return 0;
if (k == 1) //k == 1时说明,到达所求层,并且该层有一个所求结点,返回1
return 1;
return BTKLevelSize(root->leftchild, k - 1) + BTKLevelSize(root->rightchild, k - 1); //k != 1说明还没到所求的层数,则返回该结点左右子树的第k-1层结点数
}
hewh该题目是判断这棵树所有结点的值是否相同,按照大问题转化为小问题的思想,那就是判断,一个结点的值与它左右孩子的值是否相同,如果每一个结点的值都与自己左右孩子结点的值相同,那么整棵树每个结点的值都相同。
hewh所以,小问题就是判断“根节点”的值与左右孩子值是否相等,再判断,左右孩子值是否与左右孩子的左右孩子值相等,这样不断递归,就完成了整棵树的判断。
bool isUnivalTree(struct TreeNode* root)
{
//访问到空,直接返回真
if(root == NULL)
return true;
//首先要判断left是否为空,因为如果是空的话,无法解引用
//再判断根与左孩子值是否相等
//注:如果判断是相等,那么无法拿出结果,还要继续往下判断直到结束
//所以我们需要判断是否不等,有一个不等就说明不是单值,直接返回false
if(root->left && root->left->val != root->val)
return false;
//右孩子与左孩子同理
if(root->right && root->right->val != root->val)
return false;
//走到这里说明该结点与它的左右孩子值相等,这时就要继判断左右孩子的
//左右孩子值与各自双亲结点值是否相等,就要递归。
//因为有一个不相等就返回false,所以用&&连接
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
hewh求整棵树的最大深度,就是求树的高度,还是按照将问题分解,问题小化的原则。求树的最大高度,那么就要去求左右子树的高度中最大的+1,就得到整棵树的高度。同样左右子树的高度就是它们各自的左右子树的高度的最大值+1……如此循环往复。
//求最大值的函数,用于求左右子树中高度最大的
int Max(int a,int b)
{
return a>b?a:b;
}
int maxDepth(struct TreeNode* root){
//如果是空,说明高度为0,返回0
if(root == NULL)
return 0;
//返回左右子树中最大的+1(自己这一层)
return Max(maxDepth(root->left),maxDepth(root->right))+1;
}
hewh观察上图,我们可以很清楚发现,反转就是每个结点都要将自己的左右子树交换,这直接就给了我们递归的实现思路,就是交换一个结点的左右子树之后,再递归实现分别交换左右孩子的左右子树。而交换一个结点的左右子树的操作是很简单的,就是将这个结点的左孩子指针和右孩子指针指向的内容交换就ok。
struct TreeNode* invertTree(struct TreeNode* root){
//如果是空,直接返回即可,不需要操作
if(root == NULL)
return root;
//将根的左右子树交换
struct TreeNode* left = root->left;
struct TreeNode* right = root->right;
root->left = right;
root->right = left;
//递归交右孩子的左右子树
invertTree(right);
//递归交换左孩子的左右子树
invertTree(left);
return root;
}
hewh这个题其实比较简单,就是有两棵树,把问题分解成:如果这两棵树的根相同,并且两棵树的左右子树分别相同,那么返回true,三部分(根、左子树、右子树)有一处不相等,那么就返回false。
hewh唯一需要注意的问题就是:空的处理。
1.一个是空,一个非空返回false
(1)第一颗树是空,第二棵树不是空
(2)第二棵树是空,第一棵树不是空
2.两个都是空,返回true
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//两棵树都等于NULL为真
if(p == NULL && q == NULL)
return true;
//到这里只存在都不是空或者有一个为空的情况,所以如果有一个是空则为假
if(q == NULL || p == NULL) //只有一个为NULL才进入if语句里
return false;
//如果根不相等就直接返回false,根相等再去比较左右子树
if(p->val != q->val)
return false;
//递归比较左右子树
//因为左右子树都要相等才为真,有一个为假就是假,所以用&&连接
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
hewh通过观察,就是将树反转得到一个对称树,如果原树与对称树相同,则原树对称
直接复用上面几个题的代码
//反转树
struct TreeNode* invertTree(struct TreeNode* root){
if(root == NULL)
return root;
struct TreeNode* left = root->left;
struct TreeNode* right = root->right;
root->left = right;
root->right = left;
invertTree(right);
invertTree(left);
return root;
}
//判等树
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if(p == NULL && q == NULL) //都等于NULL为真
return true;
//存在不为空
if(q == NULL || p == NULL) //只有一个为NULL才为真
return false;
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
//判别树是否对称
bool isSymmetric(struct TreeNode* root)
{
struct TreeNode* tree1 = root;
struct TreeNode* tree2 = invertTree(root);
return isSameTree(tree1,tree2);
}
hewh这个题简单之处就是子树必须是一直到结束,不能仅仅是中间一段,所以这就是一个将问题十分简化的重要条件,给我们解决问题提供了极大的可能。
hewh所以问题就可以变成,每遇到一个结点,我就拿子树与该节点为树的这两棵树比较,是否相等,如果相等,说明是原树的子树,如果原树所有节点都被比较结束,还没有相等的,那么就不是原树的子树。(还是会复用到“相同的树”题目的代码)
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if(p == NULL && q == NULL) //都等于NULL为真
return true;
//存在不为空
if(q == NULL || p == NULL) //只有一个为NULL才为真
return false;
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
//判断是否是子树
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
//如果都到空了,说明不是子树,直接返回false
if(root == NULL)
return false;
//如果不是空,那么就与子树比较是否相等,相等返回true
if(isSameTree(root,subRoot))
return true;
//走到这里,说明,该节点不是空,且不相等,那么就判断该节点的左右子树
//用||连接,因为左右子树中有一个是子树,那就说明是子树。
return isSubtree(root->right,subRoot) || isSubtree(root->left,subRoot);
}
hewh这个题的要求是,需要自己开辟一个数组,将树的内容通过前序遍历保存到数组中,最后将开辟的这个数组返回。
hewh1.我们首先需要知道树有几个结点,才能malloc开辟数组。
hewh2.前序遍历,先访问根再访问左右子树。注意,这里的访问不是打印,而是将内容放进数组中。
hewh3.既然要将内容放进数组,我们就需要知道到数组的哪个位置了。具体小细节在代码中具体说明。
//求结点个数
int size(struct TreeNode* root)
{
return root == NULL? 0 : size(root->left) + size(root->right) + 1;
}
//分装的子函数
void _preorderTraversal(struct TreeNode* root,int* a,int* i)
{
//前序遍历,遇到空,返回
if(root == NULL)
return;
//不是空,就将数据放入数组
a[*i] = root->val;
(*i)++;
//递归遍历左右子树
_preorderTraversal(root->left,a,i);
_preorderTraversal(root->right,a,i);
}
//问题主函数
//1.因为我们要在这个函数里开空间等准备工作,所以不能反复迭代这个函数,所以
//将递归这个小问题单独分装成一个子函数_preorderTraversal
int* preorderTraversal(struct TreeNode* root, int* returnSize){
//由于遍历放到了子函数,我们就要时刻知道到数组的那个位置了
//所以就需要一个变量i一直指向数组当前位置,来保存数据
int i = 0;
//我们要开空间,所以就要知道树中结点的个数,分装一个函数求结点个数
*returnSize = size(root);
int* a = (int*)malloc(sizeof(int)*(*returnSize));
//子问题(前序遍历),需要1.树2.开辟的数组3.指向数组位置的变量(必须传地址,才能保证改变的始终是一个变量)
_preorderTraversal(root,a,&i);
//完成任务后,返回数组(题目要求)
return a;
}
hewh这两道题跟上一道题的思路完全一致,代码逻辑也是完全一样,唯一的区别就是处理根的实际不一样。
hewh中序:左子树、根、右子树
hewh后序:左子树、右子树、根
//中序
void _preorderTraversal(struct TreeNode* root,int* a,int* i)
{
_preorderTraversal(root->left,a,i);
if(root == NULL)
return;
a[*i] = root->val;
(*i)++;
_preorderTraversal(root->right,a,i);
}
//后序
void _preorderTraversal(struct TreeNode* root,int* a,int* i)
{
_preorderTraversal(root->left,a,i);
_preorderTraversal(root->right,a,i);
if(root == NULL)
return;
a[*i] = root->val;
(*i)++;
}
hewh这篇博文是我将自己在学习二叉树这块内容时,遇到的难题与难于实现的二叉树操作方面的总结。我知道肯定还有不少题在文章中没有涉及。但是这篇博文的最大意义就是,能够通过各式各样的例子,让我们更加深刻的理解,递归对于二叉树操作的重大意义。对于二叉树的逻辑结构和存储结构天生是极度适合递归的逻辑的。因此恰当的使用递归便可以使二叉树的大问题划分为小问题,在正常的顺序逻辑下,便可以轻松解决问题。但是最重要的及时抓住递归过程中的递归条件和边界。因为在不断“递”的过程中,要不断逼近终止条件,才可以开始“归”。
hewh老铁们,对于二叉树或者数据结构这里还有什么问题可以私信我哦,我一定会尽力解答。让我们共同努力哦~