简单网站html模板下载/外贸谷歌推广怎么样
目录
4、二叉树链式结构的实现:
4.1、前置说明:
4.2、二叉树的遍历:
4.2.1、前序、中序以及后序遍历:
4.2.2、层序遍历:
4.3、二叉树基础oj练习:
4.3.1、例题1:
4.3.2、例题2:
4.3.3、例题3:
4.3.4、例题4:
4.3.5、例题5:
4.4、二叉树的创建和销毁:
4.5、常见的例题:
4.5.1、例题1:
4.5.2、例题2:
4.5.3、例题3:
4.5.4、例题4:
4、二叉树链式结构的实现:
4.1、前置说明:
在堆中的向上调整算法和向下调整算法都可以使用递归来实现,但是当能使用递归实现,也可使用循环实现的时候,尽量选择使用循环而不选择
使用递归,当二叉树是完全二叉树,包括满二叉树时,可以使用顺序表来实现堆,也可以直接使用数组来实现堆,但是当二叉树不是完全二叉
树,包括满二叉树时,就不再适合使用顺序表或者数组来实现堆了,具体原因见上一篇博客,所以对于非完全二叉树,包括满二叉树而言,一般
通过链式结构进行存储,链式存储分为二叉链和三叉链,对于三叉链而言,相对于二叉链多了一个指向该节点的父亲的一个指针,在此要注意的
是,由于这是普通二叉树结构,即非完全二叉树,非满二叉树,所以,之前总结的父子间的计算关系就不再适用了,因为这些计算关系是针对于
堆而言的,而堆是通过完全二叉树来实现的,此处不是完全二叉树,所以不能通过下标来计算父子间的关系,这时候父亲指针就起到了很大的作
用,即可以通过孩子找父亲,关于二叉链和三叉链本质上类似于单链表和双链表,在普通二叉树中,一般也是常使用二叉链,对于三叉链一般常
使用于红黑树,B树和avl树中,对于B树而言,不完全使用三叉链,但是他们都会存储一个副指针进行调平衡,具体在后面再进行阐述,数据结
构就是用来在内存中管理数据,但是对于链式二叉树,即普通二叉树而言,一般不太关注其增删查改,因为,普通二叉树的增删查改没有意义,
如果为了单纯的存储数据,价值不大,不如使用线性表,所以学习普通二叉树更重要的是学习它的结构,求其高度,遍历方式等,更好的控制其
结构,主要是用来为后续学习更复杂的搜索二叉树打基础,其次就是很多二叉树中的OJ题都出在普通二叉树上,就像单链表一样,如果仅仅是为
了存储数据的话,直接使用带头双向循环链表即可,为什么要学习单链表呢,是因为,是因为学习单链表是为了学习后期更复杂的数据结构做基
础,当然,对于单链表而言也可以进行增删查改,但是对于普通二叉树而言,增删查改是没有意义的,没有规律可循,除此之外,一般很多链表
中的OJ题都出在单链表上,如果真的要对普通二叉树增删查改的话,一般针对于搜索二叉树进行的,此处的普通二叉树指的是非完全二叉树,搜
索二叉树即指,左子树中所有节点都比当前所在树的根节点要小,右子树中所有节点都比当前所在树的根节点要大,此时的搜索二叉树进行增删
改查是有意义的,因为存在规律可循,搜索二叉树没有规定必须是完全二叉树,是不是完全二叉树都是可以的,搜索二叉树主要是用来进行搜索
数据的,搜索某一个数据时,最多搜索该搜索二叉树的高度次就能找到,不需要遍历整个搜索二叉树,但是当该搜索二叉树的高度很大时,这时
候若要进行搜索某一个值的话,效率就会变低,所以要控制其平衡,则衍生出了平衡搜索二叉树,主要包括:红黑树,AVL树,,若要放在磁盘
中进行搜索的话,则衍生出了多叉平衡搜索树,主要包括B树系列,而B树系列又分为:B树,B+树,B*树,这些树形结构主要和磁盘中的搜索有
关,主要作为数据库的引擎进行使用、
一般情况下常说的二叉树,若未明确标明是完全二叉树,则指的就是普通二叉树,即非完全二叉树,并且默认该二叉树的度为2、
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>//二叉链、
typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;//创建一个新的节点、
BTNode* BuyBTNode(BTDataType x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){//动态开辟内存空间失败、printf("malloc fail\n");return NULL;}else{//动态开辟内存空间成功、node->data = x;node->left = node->right = NULL;return node;}
}
BTNode* CreatBinaryTree()
{BTNode* node1 = BuyBTNode(1);BTNode* node2 = BuyBTNode(2);BTNode* node3 = BuyBTNode(3);BTNode* node4 = BuyBTNode(4);BTNode* node5 = BuyBTNode(5);BTNode* node6 = BuyBTNode(6);node1->left = node2;node1->right = node4;node2->left = node3;//此处可以忽略不写,因为在定义node2时,已经把节点2内指向其右孩子的指针变量right定义成了空指针NULL、//node2->right = NULL;//同理,下面的代码也可不写、//node3->left = NULL;//node3->right = NULL;node4->left = node5;node4->right = node6;//node5->left = NULL;//node5->right = NULL;//node6->left = NULL;//node6->right = NULL;//返回根节点的地址、return node1;
}int main()
{BTNode* tree=CreatBinaryTree();return 0;
}
4.2、二叉树的遍历:
4.2.1、前序、中序以及后序遍历:
按照规则,二叉树的遍历有:前序/中序/后序/的递归结构遍历,层序的非递归结构遍历:
广度优先遍历需要把下一步所有可能的位置全部遍历完,才会进行更深层次的遍历,层序遍历就是一种广度优先遍历、
深度优先遍历是先遍历完一条完整的路径(从根到叶子的完整路径),才会向上层折返,再去遍历下一个路径,前序遍历就是一种深度优先遍历、

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//二叉链、
typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;//创建一个新的节点、
BTNode* BuyBTNode(BTDataType x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){//动态开辟内存空间失败、printf("malloc fail\n");return NULL;}else{//动态开辟内存空间成功、node->data = x;node->left = node->right = NULL;return node;}
}
BTNode* CreatBinaryTree()
{BTNode* node1 = BuyBTNode(1);BTNode* node2 = BuyBTNode(2);BTNode* node3 = BuyBTNode(3);BTNode* node4 = BuyBTNode(4);BTNode* node5 = BuyBTNode(5);BTNode* node6 = BuyBTNode(6);node1->left = node2;node1->right = node4;node2->left = node3;//此处可以忽略不写,因为在定义node2时,已经把节点2内指向其右孩子的指针变量right定义成了空指针NULL、//node2->right = NULL;//同理,下面的代码也可不写、//node3->left = NULL;//node3->right = NULL;node4->left = node5;node4->right = node6;//node5->left = NULL;//node5->right = NULL;//node6->left = NULL;//node6->right = NULL;//返回根节点的地址、return node1;
}//二叉树的前序遍历并打印、
//在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
void PrevOrder(BTNode* root)
{//对于最外层的这个二叉树而言,若存放数据1的这个节点的内存空间动态开辟失败的话,则结构体指针变量node1中存放的就是空指针NULL,则指针变量tree中存放的就是空指针NULL,//则该调用函数形参中的指针变量root就是空指针NULL,即为空树,打印NULL,然后直接返回、if (root == NULL) {//空树、printf("NULL ");return;}printf("%d ", root->data);PrevOrder(root->left);PrevOrder(root->right);
}//二叉树的中序遍历并打印、
//在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
void InOrder(BTNode* root)
{//对于最外层的这个二叉树而言,若存放数据1的这个节点的内存空间动态开辟失败的话,则结构体指针变量node1中存放的就是空指针NULL,则指针变量tree中存放的就是空指针NULL,//则该调用函数形参中的指针变量root就是空指针NULL,即为空树,打印NULL,然后直接返回、if (root == NULL){//空树、printf("NULL ");return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}//二叉树的后序遍历并打印、
//在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
void NextOrder(BTNode* root)
{//对于最外层的这个二叉树而言,若存放数据1的这个节点的内存空间动态开辟失败的话,则结构体指针变量node1中存放的就是空指针NULL,则指针变量tree中存放的就是空指针NULL,//则该调用函数形参中的指针变量root就是空指针NULL,即为空树,打印NULL,然后直接返回、if (root == NULL){//空树、printf("NULL ");return;}NextOrder(root->left);NextOrder(root->right);printf("%d ", root->data);
}//计算该二叉树中 有效节点 的个数、
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
方法一:
该方法是不正确的,是因为这是递归算法,而此时的变量count是局部变量,每一层递归中所定义的局部变量count的生命周期都是在当前所在的函数内,即第一层递归中定义的局部变量count的生命周期只在第一层递归中,第二层递归内不属于第一层递归中定义的局部变量count的生命周期,
所以在不同生命周期内定义局部变量count的话,不会造成重定义,即在不同的{}内,但{}与{}不属于包含关系,而是平等关系,则定义的局部变量不造成重定义,并且每一次递归都是在调用函数,函数与函数之间是平等的,而不是包含关系,就比如main和test这样的关系,所以每一层递归栈帧中都会创建一个局部变量count,这些存在于不同栈帧中的局部变量count
虽然变量名字是一样的,但是由于在不同的栈帧中,所以每一次count++,都不会加到同一个局部变量count上,而是加在了不同的局部变量count上,在此可以把局部变量count定义在
调用函数BTreeSzie外部,即把该局部变量count改变定义成全局变量,如方法二所示、
//int BTreeSzie(BTNode* root)
//{
// int count = 0;
// if (root == NULL)
// {
// //空树、
// return count;
// }
// else
// {
// //前序遍历、
// count++;
// BTreeSzie(root->left);
// BTreeSzie(root->right);
// 中序遍历、
// //BTreeSzie(root->left);
// //count++;
// //BTreeSzie(root->right);
// 后序遍历、
// //BTreeSzie(root->left);
// //BTreeSzie(root->right);
// //count++;
// return count;
// }
//}方法二:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
全局变量和静态变量的线程不是安全的、
//int count = 0;
//int BTreeSzie(BTNode* root)
//{
// if (root == NULL)
// {
// //空树、
// return count;
// }
// else
// {
// //前序遍历、
// count++;
// BTreeSzie(root->left);
// BTreeSzie(root->right);
// 中序遍历、
// //BTreeSzie(root->left);
// //count++;
// //BTreeSzie(root->right);
// 后序遍历、
// //BTreeSzie(root->left);
// //BTreeSzie(root->right);
// //count++;
// return count;
// }
//}方法三:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
全局变量不存在栈区内,而是存在于静态区内,所以全局变量不会随着栈帧的销毁而销毁,全局变量的生命周期是整个工程、
全局变量和静态变量的线程不是安全的,具体在学完Linux就会明白,由于全局变量的生命周期是整个工程,适用性太强,谁都可以操作该全局变量,即多线程调用该函数时,多个调用函数同时对全局变量count进行++操作,就会错乱,导致出现线程安全问题、
//int count = 0;
//void BTreeSzie(BTNode* root)
//{
// if (root == NULL)
// {
// //空树、
// return ;
// }
// else
// {
// //前序遍历、
// count++;
// BTreeSzie(root->left);
// BTreeSzie(root->right);
// 中序遍历、
// //BTreeSzie(root->left);
// //count++;
// //BTreeSzie(root->right);
// 后序遍历、
// //BTreeSzie(root->left);
// //BTreeSzie(root->right);
// //count++;
// return ;
// }
//}方法四:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
全局变量和静态变量的线程不是安全的、
对于静态变量,包括静态局部变量,静态全局变量,才考虑作用域和生命周期,对于非静态变量只需要考虑生命周期即可、
静态局部变量的生命周期是整个工程,但是其作用域和普通局部变量的生命周期是一样的,所以在考虑其作用域时,就把该静态局部变量看做普通局部变量,该普通局部变量的生命周期就是当前所在的函数,即函数 BTreeSzie,则静态局部变量的作用域即在当前所在的函数 BTreeSzie内才能使用
出了当前所在的 BTreeSzie函数,生命周期不会结束,但是不能使用该静态局部变量,而对于递归函数而言,不管调用了几次递归,都是调用的 BTreeSzie函数本身,都还是在 BTreeSzie函数内进行的,所以说对于该静态局部变量而言,无论调用多少次递归,都是在 BTreeSzie函数内部,并没有出去他的作用域,所以每一次递归中的count++是可以进行的、
但是若在mian函数中使用count是不可以的,即使静态局部变量的生命周期没有结束,但是由于出了静态局部变量的作用域,所以不能在使用该静态局部变量count了、
静态全局变量的生命周期是整个工程,并且其作用域也是整个工程,所以对于静态全局变量和全局变量而言,作用是一样的,两者的生命周期都是整个工程,而静态全局变量的作用域是整个工程,而非静态变量的作用域和其生命周期相同,所以全局变量的作用域也是
整个工程,则静态全局变量和全局变量的效果是一样的,所以使用这两种方法都是可以的、
总结:若静态局部变量定义在非递归函数中,比如定义在函数test中的话,则该静态局部变量的生命周期是整个工程,但是其作用域是在test函数内部,所以在该test内部是可以使用静态局部变量的,但是出去该tets函数,则就不能够再使用该静态局部变量了,即使其生命周期没有结束,也不能再使用了、
//
//int BTreeSzie(BTNode* root)
//{
// static int count = 0;
// //把局部变量count定义成静态局部变量,对于静态变量而言也是放在静态区中的,其生命周期为整个工程、
// //对于静态局部变量而言,若其所在的调用函数不是递归函数的话,则出了其调用函数之后,该静态局部变量不会随着栈帧的销毁,但是出了调用函数后,就不能再使用该静态局部变量了, 若该静态局部变量所在的调用函数是递归函数的话,不管递归调用
// //多少次,则只有第一次递归时会执行该静态局部变量所在的这一行代码,当再次递归时,则会直接跳过该行代码,不会执行,所以不会每次递归都把静态局部变量count置为0,由于是在进行递归,所以在第一次定义静态局部变量后再进行递归的话,实际上调用该函数本身,则对于静态局部变量而言
// //并没有出去静态局部变量的作用域,则第二次及以后中虽然不会再次定义静态局部变量count,但是是可以使用该静态局部变量的,即第二次及以后的count++是可以使用的,,则所有的count++就会加到同一个静态局部变量count上,所以这是可以的,但是对于该方法是有缺陷的,即如果在
// //main函数中将其打印函数多调用几次的话,则count的值会累加起来,而不会每次调用printf函数都从0开始,这是因为第一次打印时打印出来时6,然后第二次打印时,还会调用BTreeSzie函数,而这一次调用BTreeSzie
// //函数时,即使在新一轮的递归中,即第二轮的递归中,第二轮递归中的第一次递归时,也不会执行定义静态局部变量count的这一行代码,因为前面有第一轮的递归,所以第二轮及以后的递归都不是第一次调用BTreeSzie将函数,
// //所以就不会执行定义静态局部变量count的这一行代码,所以说只有第一次打印时,第一轮中第一次递归才会执行定义静态变量count所在的这一行代码,当第一轮后面再进行递归,或者第二轮,第三轮递归时,都不会在执行该代码,此处所值的第一轮,第二轮,第三轮指的是三次打印printf函数中的调用递归函数
// //所以第一次打印出来是6,但是打印第二次时,即进行第二轮递归时,再调用该递归函数时,由于不是第一次调用该函数,所以不会执行定义静态变量count所在的这一行代码,故不能从0开始,而是在前面的基础上进行累加,此时最重要是的也不能够手动在第二,第三个printf前手动置为0,是因为,
// //已经出去了该静态局部变量的作用域,即使生命周期不结束,但是也不能使用该静态局部变量、
//
// //静态局部变量的生命周期确实是整个工程,但是静态局部变量的作用域和局部变量的生命周期一样,即只能在调用函数内存使用该变量,出了该调用函数之外就不能再使用该变量了,即使生命周期未结束,但仍不能进行使用、
// if (root == NULL)
// {
// //空树、
// return count;
// }
// else
// {
// //前序遍历、
// count++;
// BTreeSzie(root->left);
// BTreeSzie(root->right);
// 中序遍历、
// //BTreeSzie(root->left);
// //count++;
// //BTreeSzie(root->right);
// 后序遍历、
// //BTreeSzie(root->left);
// //BTreeSzie(root->right);
// //count++;
// return count;
// }
// //所以对于静态局部变量方法而言,有两个缺点,一就是其线程安全存在问题,而是,若在main函数中多次打印时,则不能每次打印都从0开始计算,会进行累加,这个问题不仅静态局部变量会有,静态全局变量也会有,但不同的是,静态全局变量的话,他的作用域和生命周期都是整个工程,所以可以在main函数中
// //在第二次即后面的多次print前面手动置静态全局变量count为0,这样就可以每次打印都从0开始计算了,但对对于静态局部变量而言,由于其作用域的问题,所在无法在main函数中第二次及以后printf前手动置count为0,所以只能累加存在问题、
//}方法五:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
定义静态全局变量、
//static int count = 0;
//int BTreeSzie(BTNode* root)
//{
// if (root == NULL)
// {
// //空树、
// return count;
// }
// else
// {
// //前序遍历、
// count++;
// BTreeSzie(root->left);
// BTreeSzie(root->right);
// 中序遍历、
// //BTreeSzie(root->left);
// //count++;
// //BTreeSzie(root->right);
// 后序遍历、
// //BTreeSzie(root->left);
// //BTreeSzie(root->right);
// //count++;
// return count;
// }
//}方法六:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
该方法线程也是安全的,在栈区内每个线程都是独享的、
思想:遍历+计数、
//void BTreeSzie(BTNode* root,int* pCount)
//{
// if (root == NULL)
// {
// //空树、
// return ;
// }
// else
// {
// //前序遍历、
// (*pCount)++;
// BTreeSzie(root->left, pCount);
// BTreeSzie(root->right, pCount);
// 中序遍历、
// //BTreeSzie(root->left, pCount);
// //(*pCount)++;
// //BTreeSzie(root->right, pCount);
// 后序遍历、
// //BTreeSzie(root->left, pCount);
// //BTreeSzie(root->right, pCount);
// //(*pCount)++;
// return ;
// }
//}方法七:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
思路:子问题、
1、空树,最小规模子问题,节点个数是0,返回0、
2、非空树,左子树节点个数+右子树节点个数+1 ,此时根节点不为空,则也要把根节点算上,1代表的就是当前的根节点、
分治思想,把复杂的问题分成更小规模的子问题,子问题再继续分成更小规模的子问题,直到最后子问题不可再分割直接能得出结果、
//int BTreeSzie(BTNode* root)
//{
// //后序遍历、
// return root == NULL ? 0 : BTreeSzie(root->left) + BTreeSzie(root->right) + 1;
// //前序遍历、
// //return root == NULL ? 0 : 1+BTreeSzie(root->left) + BTreeSzie(root->right);
// //中序遍历、
// //return root == NULL ? 0 : BTreeSzie(root->left) +1+ BTreeSzie(root->right);
//
//}//计算该二叉树中叶子节点的个数、
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
方法一:
遍历+计数,判断是否为叶子节点,左子树为空,右子树为空,则是叶子节点就++,否则就不++,该方法线程也是安全的,在栈区内每个线程都是独享的,空树没有叶子节点、
//void BTreeLeafSize(BTNode* root, int* pCount)
//{
// if (root == NULL)
// {
// //空树、
// return ;
// }
// else
// {
// //前序遍历、
// if (root->left==NULL && root->right==NULL)
// {
// (*pCount)++;
// }
// BTreeLeafSize(root->left, pCount);
// BTreeLeafSize(root->right, pCount);
// 中序遍历、
// //BTreeLeafSize(root->left, pCount);
// //if (root->left == NULL && root->right == NULL)
// //{
// // (*pCount)++;
// //}
// //BTreeLeafSize(root->right, pCount);
// 后序遍历、
// //BTreeLeafSize(root->left, pCount);
// //BTreeLeafSize(root->right, pCount);
// //if (root->left == NULL && root->right == NULL)
// //{
// // (*pCount)++;
// //}
// return ;
// }
//}方法二:
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
分治思想、
//int BTreeLeafSize(BTNode* root)
//{
// if (root == NULL)
// {
// //空树无叶子结点、
// return 0;
// }
// else
// {
// //非空树、
// if (root->left == NULL && root->right == NULL)
// {
// return 1;
// }
// else
// {
// return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
// }
// }
//}计算该二叉树中第K层上有效节点的个数,K>=1,默认根节点所在的层为第一层、
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、
分治思想,K有可能超出该二叉树的高度,即K有可能大于h,若超出其高度的话,则 有效节点的个数就等于0个、
若为空树,则无论K为多少,则有效节点都是0,若为非空树,且K==1的话,则只有根节点一个有效节点,返回1,,若非空树,且K>1,转换成求左子树的第K-1层有效节点个数加右子树的第K-1层的有效节点的个数、
//int BTreeKLevelSize(BTNode* root, int k)
//{
// assert(k >= 1);
// if (root == NULL)
// {
// //空树、
// //若为空树,则无论K为多少,则有效节点都是0、
// return 0;
// }
// else
// {
// //非空树、
// if (k == 1)
// {
// //默认根节点所在的层为第一层、
// return 1;
// }
// else
// {
// return BTreeKLevelSize(root->left, k - 1) + BTreeKLevelSize(root->right, k - 1);
// }
// }
//}计算该二叉树的深度/高度、
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
分治思想,取左子树的高度和右子树的高度的较大值,然后再+1,此处的1代表的是根节点所在的一层也要算进二叉树的高度中,默认根节点所在的层数为第一层、
//int BTreeDepth(BTNode* root)
//{
// if (root == NULL)
// {
// //空树,则高度为0、
// //默认根节点所在的层数为第一层、
// return 0;
// }
// else
// {
// //非空树、
// //分别计算出左子树和右子树的高度、
// int leftDepth = BTreeDepth(root->left);
// int rightDepth = BTreeDepth(root->right);
// //若两个子树的高度相等的话,则可以任选一个即可,在此选的是右子树的高度+1、
// return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
// }
//}//二叉树查找值为x的节点、
在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可
//若找到了则返回其地址,若找不到则返回空指针NULL、
//可以使用 前序/中序/后序遍历 去查找,但是比较麻烦,是因为,该调用函数要求具有返回值,处理返回值时就比较麻烦、
BTNode* BTreeFind(BTNode* root, BTDataType x)
{//前序查找,也可使用中序和后序查找,只需要改变一下代码即可、if (root == NULL) {//空树则一定找不到、return NULL;}else{//非空树、//前序查找,先查找根节点,若根节点不是要找的数据x,则再去左子树查找,若再找不到,则去右子树查找、//若在根节点中找到了,则就不再去左右子树找了,同理,若在左子树中找到了,则就不再去右子树中找了、if (root->data == x){//根节点中存储的数据是要找的数据x,则直接返回该根节点的地址,就不再去左子树中找了、return root;}else{//根节点中存储的数据不是要找的数据x,则需要再去左子树中去查找、BTNode* left = BTreeFind(root->left, x);if (left){//在左子树中找到了要查找的数据x,直接返回该节点的地址即可,不需要再去右子树中查找了、return left;}else{//在左子树中未找到要查找的数据x,则需要再去右子树中查找了、BTNode* right = BTreeFind(root->right, x);if (right){//在右子树中找到了要查找的数据x,直接返回该节点的地址即可、return right;}else{//在右子树中未找到要查找的数据x,则代表整个二叉树中就不存在要查找的数据x,即未找到,则返回空指针NULL、return NULL;}//return BTreeFind(root->right, x);//在此,也可以写成上述代码,直接返回 BTreeFind(root->right, x),当未在根节点和左子树中找到要查找的数据x时,那么若能够在右子树中找到的话,则代表在该整个二叉树中找到了,应该返回的是数据x所在节点的地址,而该函数的返回值中存储的就是存储数据x的节点的地址//若找不到的话,则代表着整体二叉树就找不到数据x,则应该返回的是空指针NULL,而该函数的返回值中存储的就是NULL,所以不需要再进行判断了,但是最好要判断一下,这样代码的可读性就比较高、}}}
}二叉树的销毁、
在调用函数内部改变了指针变量*pproot的值,可采用传址调用,直接在调用函数内部把指针变量*pproot置为空指针NULL、
方法一:
传址调用、
只能采用后序遍历加销毁,是因为,若采用前序遍历加销毁的话,把根节点所占内存空间释放后,就找不到根节点的两个孩子了,即无法完成左右子树的销毁,若采用中序遍历加销毁的话,
先销毁左子树,然后再销毁根节点,此时,就找不到根节点的右孩子了,即无法完成右子树的销毁,所以只能采取后序遍历加销毁的方法、
//void BinaryTreeDestory(BTNode** pproot)
//{
// if (*pproot == NULL)
// {
// //空树、
// return;
// }
// else
// {
// //非空树、
// BinaryTreeDestory((*pproot)->left);
// BinaryTreeDestory((*pproot)->right);
// free(*pproot);
// }
// *pproot = NULL;
//}//二叉树的销毁、
//方法二:
//在调用函数内部改变了指针变量root的值,可采用传值调用,但是要在调用函数外面手动的把指针变量tree置为空指针NULL、
//传值调用、
//只能采用后序遍历加销毁,是因为,若采用前序遍历加销毁的话,把根节点所占内存空间释放后,就找不到根节点的两个孩子了,即无法完成左右子树的销毁,若采用中序遍历加销毁的话,
//先销毁左子树,然后再销毁根节点,此时,就找不到根节点的右孩子了,即无法完成右子树的销毁,所以只能采取后序遍历加销毁的方法、
void BinaryTreeDestory(BTNode* root)
{if (root == NULL){//空树、return;}else{//非空树、BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);free(root);root = NULL;}//此处的置空是不起作用的,因为此处的root为形参中的变量,它的改变不会影响实参部分中的root,所以要在调用函数外面手动置实参中的root为空指针NULL,虽然不起作用,但是最好也要置为空指针、
}
int main()
{//将该二叉树中的根节点的地址存放在结构体指针变量tree中、BTNode* tree = CreatBinaryTree();前序遍历并打印、//PrevOrder(tree);//printf("\n");//中序遍历并打印、//InOrder(tree);//printf("\n");后序遍历并打印、//NextOrder(tree);//printf("\n");计算该二叉树中有效节点的个数、//方法一:printf("%d\n",BTreeSzie(tree));//方法二:printf("%d\n",BTreeSzie(tree));方法三://BTreeSzie(tree);//printf("%d\n",count);//count = 0;//BTreeSzie(tree);//printf("%d\n", count);//count = 0;//BTreeSzie(tree);//printf("%d\n", count);方法四://printf("%d\n",BTreeSzie(tree)); //6//printf("%d\n", BTreeSzie(tree)); //12//printf("%d\n", BTreeSzie(tree)); //18//方法五://printf("%d\n",BTreeSzie(tree)); //6//printf("%d\n", BTreeSzie(tree)); //12//printf("%d\n", BTreeSzie(tree)); //18//printf("%d\n", BTreeSzie(tree)); //6//count=0; //这就是静态全局变量和静态局部变量的区别,是由于其作用域不同而导致的,具体见上面的解析、//printf("%d\n", BTreeSzie(tree)); //6//count=0;//printf("%d\n", BTreeSzie(tree)); //6方法六://int count1 = 0;//BTreeSzie(tree, &count1);//传址调用、//printf("%d\n", count1);//int count2 = 0;//BTreeSzie(tree, &count2);//传址调用、//printf("%d\n", count2);方法七://printf("%d\n", BTreeSzie(tree));//printf("%d\n", BTreeSzie(tree));//printf("%d\n", BTreeSzie(tree));//计算该二叉树中叶子节点的个数、方法一://int count1 = 0;//BTreeLeafSize(tree, &count1);//传址调用、//printf("%d\n", count1);方法二://printf("%d\n", BTreeLeafSize(tree));计算该二叉树中第K层上有效节点的个数,K>=1,默认根节点所在的层为第一层、//int k = 3;//printf("%d\n",BTreeKLevelSize(tree, k));//计算该二叉树的深度/高度、//printf("Depth Size: %d\n", BTreeDepth(tree));//二叉树查找值为x的节点并修改、for (int i = 1; i <= 7; i++){printf("Find: %d %p\n",i, BTreeFind(tree, i));}BTNode* ret = BTreeFind(tree, 5);if (ret){ret->data = 50;}//前序遍历并打印、PrevOrder(tree);printf("\n");//二叉树的销毁、//方法一://传址调用、//BinaryTreeDestory(&tree);//方法二://传值调用、BinaryTreeDestory(tree);tree = NULL;return 0;
}
下面主要分析前序递归遍历,中序与后序图解类似、
前序遍历递归图解:
4.2.2、层序遍历:
一、test.c源文件:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> #include"Queue.h"//#include"Queue.h" //在此是调用各种头文件,而头文件的调用一般都是直接替换,而当调用头文件"Queue.h"时就会进行头文件的替换,当替换时,就会把头文件"Queue.h"中的所有的代码 //替换至test.c文件中的第652行代码处,而头文件"Queue.h"中有 typedef BTNode* QDataType; 这一行代码,系统只会往 上 找BTNode*,但是发现 上面 找不到BTNode*的定义,所以会报错、//二叉链、 typedef int BTDataType; typedef struct BinaryTreeNode {BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right; }BTNode;//#include"Queue.h" //若把头文件的调用代码 #include"Queue.h" 放在BTNode的定义下面能不能行呢? //结果还是会报错,这是因为不仅仅只有在test.c文件中对头文件"Queue.h"进行了调用,而Queue.c文件中也对头文件"Queue.h"进行了调用,而在Queue.c文件中对头文件"Queue.h"进行替换后,仍会出现前面相同的问题, //所以在此处调用头文件"Queue.h",只解决了test.c文件中的问题,而未解决Queue.c文件中的问题,所以这不能完全解决问题、//创建一个新的节点、 BTNode* BuyBTNode(BTDataType x) {BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){//动态开辟内存空间失败、printf("malloc fail\n");return NULL;}else{//动态开辟内存空间成功、node->data = x;node->left = node->right = NULL;return node;} }BTNode* CreatBinaryTree() {BTNode* node1 = BuyBTNode(1);BTNode* node2 = BuyBTNode(2);BTNode* node3 = BuyBTNode(3);BTNode* node4 = BuyBTNode(4);BTNode* node5 = BuyBTNode(5);BTNode* node6 = BuyBTNode(6);BTNode* node7 = BuyBTNode(7);BTNode* node8 = BuyBTNode(8);node1->left = node2;node1->right = node4; node2->left = node3;//此处可以忽略不写,因为在定义node2时,已经把节点2内指向其右孩子的指针变量right定义成了空指针NULL、node2->right = node7;//同理,下面的代码也可不写、//node3->left = NULL;//node3->right = NULL;node4->left = node5;node4->right = node6;//node5->left = NULL;//node5->right = NULL;//node6->left = NULL;//node6->right = NULL;//node6->right = node8;//返回根节点的地址、return node1; }//二叉树的层序遍历并打印、 //在调用函数内部并没有改变指针变量root的值,只需要 传值 调用即可、 void LevelOrder(BTNode* root) {//定义结构体变量q、Queue q;//初始化、QueueInit(&q);//若二叉树中某个节点的地址为空指针NULL,即该节点不存在时,则不需要进行入队操作、//此处只有当某一个节点的地址不是空指针NULL时,才进入if语句进行入队操作,若某一个节点的地址是空指针NULL时,则不进入if语句,即相当于是不进行入队操作、if (root) {//二叉树的根节点的地址不为空指针NULL、//入队数据、QueuePush(&q, root);}//判断队列是否为空队列,当队列为空队列时,则层序遍历结束、while (!QueueEmpty(&q)){//队列不是空队列,出队头的数据、BTNode* front = QueueFront(&q);//出队数据、QueuePop(&q);//打印、printf("%d ", front->data);//把队头数据的孩子节点的地址放进队列中,当孩子节点的地址为空指针NULL时,则不需要入队数据、if (front->left){QueuePush(&q, front->left);}if (front->right){QueuePush(&q, front->right);}}printf("\n");//销毁队列、QueueDestory(&q);printf("队列销毁成功\n"); }//二叉树的销毁、 //方法二: //在调用函数内部改变了指针变量root的值,可采用传值调用,但是要在调用函数外面手动的把指针变量tree置为空指针NULL、 //传值调用、 //只能采用后序遍历加销毁,是因为,若采用前序遍历加销毁的话,把根节点所占内存空间释放后,就找不到根节点的两个孩子了,即无法完成左右子树的销毁,若采用中序遍历加销毁的话, //先销毁左子树,然后再销毁根节点,此时,就找不到根节点的右孩子了,即无法完成右子树的销毁,所以只能采取后序遍历加销毁的方法、 void BinaryTreeDestory(BTNode* root) {if (root == NULL){//空树、return;}else{//非空树、BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);free(root);root = NULL;}//此处的置空是不起作用的,因为此处的root为形参中的变量,它的改变不会影响实参部分中的root,所以要在调用函数外面手动置实参中的root为空指针NULL,虽然不起作用,但是最好也要置为空指针、 }//判断二叉树是否是完全二叉树、 //空树默认是完全二叉树、 //思路一: //算出二叉树的高度,假设为h,然后再计算出二叉树中有效节点的实际个数,看是否与2^h - 1相等,但是这种方法只能用来判断是否是满二叉树,这是因为2^h - 1是根据满二叉树的情况计算出来的, //而对于非满二叉树的完全二叉树是不适用的,因为这种完全二叉树的有效节点的实际个数是在一个范围内的,并不是准确的某一个值,所以思路一只能用来判断是否是满二叉树,并不能用来判断是否是完全二叉树、//思路二: //使用层序遍历的变形来解决该问题,在层序遍历时,只把地址不是空指针NULL的节点入队数据,而对于地址是空指针NULL的节点不入队数据,在判断是否是完全二叉树中,不管某一个节点的地址是否是空指针NULL,都把该 //节点的地址入队数据,当出队头数据出到空指针NULL时,就不再入队数据了,则就出队列中的剩下的所有的数据,在出队列中剩下的所有的数据的过程中再去判断这些数据中是否存在不为空指针NULL的数据,若存在则不是完全二叉树,当把队列中所有的数据都出完直到队列为空时,若还不存在,则是完全二叉树、 bool BinaryTreeComplete(BTNode* root) {//定义结构体变量q、Queue q;//初始化、QueueInit(&q);//不管二叉树中某一个节点的地址是否为空指针NULL,都进行入队操作、QueuePush(&q, root);//判断队列是否为空队列、while (!QueueEmpty(&q)){//队列不是空队列,取队头的数据、BTNode* front = QueueFront(&q);//出队头数据、QueuePop(&q);//判断所出的队头数据是否为空指针NULL、if (front == NULL)break;//把队头数据的孩子节点的地址放进队列中,不管孩子节点的地址是否为空指针NULL时,都需要入队数据、QueuePush(&q, front->left);QueuePush(&q, front->right);}//当所出队头数据为空指针NULL时,要进行判断、//判断队列是否为空队列、while (!QueueEmpty(&q)){//队列不是空队列,取队头的数据、BTNode* front = QueueFront(&q);//出队头数据、QueuePop(&q);//若所出队头数据为非空数据,则不是完全二叉树、if (front){//在return之前要进行销毁队列、QueueDestory(&q);return false;}}//当把队列中剩余的数据都出队完毕后,若没有发现非空数据,则说明是完全二叉树、//在return之前要进行销毁队列、//由于之前的队列是使用单向不带头不循环链表来实现的,而当程序执行到此,队列为空队列,则队列中所有的节点已经被全部释放了,为什么还要再调用一下QueueDestory(&q)函数呢?//是因为之前实现队列的时候使用的是单向无头不循环链表来实现的,若队列的底层是使用顺序表来实现的话,虽然队列不适合使用顺序表来实现,但是万一真是使用了顺序表来实现的话//此处若不调用QueueDestory(&q)函数,则是不可以的,是因为在该种结构下,Pop之后,顺序表并没有被释放,或者如果队列使用了带头链表的话,Pop操作不会把哨兵位的头节点删除掉,所以这里最好要调用一下//该函数,这样的话,不管以何种形式来实现队列的底层结构,都是满足要求的、QueueDestory(&q);return true; }int main() {//将该二叉树中的根节点的地址存放在结构体指针变量tree中、BTNode* tree = CreatBinaryTree();//层序遍历并打印、LevelOrder(tree);//判断二叉树是否是完全二叉树、printf("%d\n", BinaryTreeComplete(tree)); //bool值的本质就是整型、//二叉树销毁,方法二:传值调用、BinaryTreeDestory(tree);tree = NULL;printf("二叉树销毁成功\n");return 0; }
二、Queue.c源文件:
#define _CRT_SECURE_NO_WARNINGS 1 #include"Queue.h"//初始化两个指针变量,不是初始化节点、 //对于单链表而言,节点不需要进行初始化、 void QueueInit(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、pq->head = NULL;pq->tail = NULL; }//销毁队列、 void QueueDestory(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、QNode* cur = pq->head;while (cur){//记录指针变量cur所指节点的下一个节点的地址、QNode* next = cur->next;free(cur);cur = next;}pq->head = pq->tail = NULL; }//入队(尾插)、 void QueuePush(Queue* pq, QDataType x) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、//创建新节点,只有在入队时需要开辟新的节点,所以不需要把该过程封装成一个函数、QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){//动态开辟失败、printf("malloc fail\n");return;}else{//动态开辟成功、newnode->data = x;newnode->next = NULL;}//判断不带头单向不循环链表,即这里的队列是否为空、//if (pq->tail == NULL)if (pq->head == NULL) {//队列中不存在节点,即队列为空、//进一步保证两个指针变量都为空指针NULL、assert(pq->tail == NULL);pq->head = newnode;pq->tail = newnode;}else{//队列中存在节点,即队列不为空、pq->tail->next = newnode;pq->tail = newnode;} }//出队(头删)、 void QueuePop(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、//当队列为空时,就不能再出队了、assert(pq->head && pq->tail);//判断队列中是否只有一个节点、if (pq->head->next == NULL){//队列中只有一个节点、free(pq->head);pq->head = pq->tail = NULL;}else{//队列中有多个节点、//记录头指针所指节点的下一个节点的地址、QNode* next = pq->head->next;free(pq->head);pq->head = next;} }//判断队列是否为空、 bool QueueEmpty(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、//return pq->tail == NULL;return pq->head == NULL; }//计算队列的长度、 size_t QueueSize(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、//方法一:size_t size = 0;QNode* cur = pq->head;while (cur){size++;cur = cur->next;}return size;//方法二://在结构体Queue成员变量中增加变量size_t size , 初始化时给成0,入队则++,出队则--,到最后直接在该调用函数内返回size即可,不需要再进行计算了,如果使用该方法//那么在该函数内部时间复杂度就是:O(1)、//不可以使用指针减指针的方法,是因为对于链表而言,物理结构是不连续的,所以不可以使用该方法、 }//取出队头的数据、 QDataType QueueFront(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、assert(pq->head);//在保证队列不是空队列的情况下,再去取队头的数据、return pq->head->data; }//取出队尾的数据、 QDataType QueueBack(Queue* pq) {//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、assert(pq);//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址//传过来,即传址调用、assert(pq->tail);//在保证队列不是空队列的情况下,再去取队尾的数据、return pq->tail->data; }
三、Queue.h头文件:
#pragma once #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <assert.h>//extern是声明全局变量的,此处的前置声明并不是声明全局变量,而声明的是一个类型,所以不会使用到extern、 //前置声明、 //前置声明只能使用原始的模样,不要使用typedef后的内容、 //和函数声明的功能是一样的、 struct BinaryTreeNode; //即告诉编译器BinaryTreeNode是一个结构体,先给一个该结构体的声明,该结构体的定义在其他的地方、//此处的BTNode*中的BTNode是typedef出来的,在此不使用typedef出来的BTNode,而使用typedef之前的内容,即使用:struct BinaryTreeNode、 //按说,若写成这样的话,若test.c文件中仍在定义二叉链之前调用头文件"Queue.h"还是会继续报错,原因和之前的一样,若test.c文件中在定义二叉链之后调用头文件"Queue.h"的话,则对于test.c文件而言不会出错、 //但是对于Queue.c文件仍会出错,原因和之前的一样,但是现在发现,不管在test.c文件中在定义二叉链之前还是之后调用"Queue.h"头文件,并且也在Queue.c文件中调用头文件"Queue.h",发现竟然没有报错,这可能是编译器 //对其进行了优化,按理说应该是编不过去的,在其他编译器下可能就编译不过去,所以最好要在代码 typedef struct BinaryTreeNode* QDataType; 之前加一个前置声明,这样的话,不管哪个编译器都是可以通过的、 typedef struct BinaryTreeNode* QDataType; typedef struct QueueNode {QDataType data;struct QueueNode* next; //不可以使用QNode* next来代替该行代码,这个重定义只有在12行以下才生效、 }QNode; //在此由于带不带哨兵位的头节点影响不大,所以选择不带头的单链表,即不带头单向不循环链表、 // //typedef struct QueueNode //{ // QDataType data; // struct QueueNode* next; //}*QNode; //此时,QNode是struct QueueNode* 的重定义、//为了方便,将 头指针和尾指针 写入一个结构体中、 typedef struct Queue {QNode* head;QNode* tail; }Queue;//初始化两个指针变量,不是初始化节点、 //对于单链表而言,节点不需要进行初始化、 void QueueInit(Queue* pq);//销毁队列、 void QueueDestory(Queue* pq);//入队、 void QueuePush(Queue* pq,QDataType x);//出队、 void QueuePop(Queue* pq);//判断队列是否为空、 bool QueueEmpty(Queue* pq);//计算队列的长度、 size_t QueueSize(Queue* pq);//取出队头的数据、 QDataType QueueFront(Queue* pq);//取出队尾的数据、 QDataType QueueBack(Queue* pq);
4.3、二叉树基础oj练习:
4.3.1、例题1:
//若想通过前序,中序,后序来遍历,让该二叉树中的每一个节点都与某一个值去比较,比如示例1,都与1进行比较,若相等则继续往后遍历,若不相等,则代表就不是单值二叉树,当所有的节点都遍历结束后,若还想等的话,则就是单值二叉树,但是该方法不太好,因为该调用函数要求具有返回值,使用前中后序遍历时就不方便进行操作、
//在OJ题中尽量不要使用静态局部变量,因为OJ有多个测试用例,则会多 轮 调用该函数,则可能会因为多轮调用导致出现错误,就比如之前的静态局部变量count++就会导致累加的情况,也最好不要使用全局变量和静态全局变量,是因为,他们不够自动化,就比如之前定义的全局变量和静态全局变量count,他们不会自动的在第二轮及以后的调用该函数前把count置为0,需要手动置为0才可以,对于OJ题来说不太合适,所以尽量不要采用,即再OJ题中尽量不使用静态变量和全局变量、
bool isUnivalTree(struct TreeNode* root)
{if(root==NULL){//空树,空树不违反单值二叉树的要求,所以默认空树为单值二叉树、return true;}else{//非空树、if(root->left && root->left->val!=root->val){//左孩子存在,当根节点和其左孩子比较后发现已经不符合单值二叉树了,则就直接返回false即可,不需要再比较根节点和其右孩子了、return false;}else{//当根节点和其左孩子比较后符合单值二叉树,则需要再比较根节点和其右孩子、if(root->right && root->right->val!=root->val){//当右孩子存在并且根节点和其右孩子比较后不符合单值二叉树的定义,则直接返回fasle、return false;}else{//当右孩子存在并且根节点和其右孩子比较后仍符合单值二叉树的定义,则还需要进一步递归、//方法一://继续检查左子树、bool ret1 = isUnivalTree(root->left);if(!ret1){//不是单值二叉树、return false;}else{//继续检查右子树、bool ret2 = isUnivalTree(root->right);if(!ret2){//不是单值二叉树、return false;}else{//是单值二叉树、return true;}//此时二叉树中的左边已经满足了单值,若右边再满足单值的话,则整体二叉树就是单值二叉树,否则就不是单值二叉树、//若isUnivalTree(root->right)的返回值为true,则整体就是单值二叉树,就应该返回true,若isUnivalTree(root->right)的返回值为false,则整体就是非单值二叉树,就应该返回false、//若右边不是单值的话,则整体即为非单值二叉树,应该返回false,而isUnivalTree(root->right)返回值就是false,若右边满足单值的话,则整体就是单值二叉树,应该返回true,而isUnivalTree(root->right)返回值就是true,所以写成下面的代码也可以、//return isUnivalTree(root->right);//方法二://若左边满足是单值,则还要继续与右边进行比较,若左边不满足单值,即isUnivalTree(root->left)的返回值是false,则右边的isUnivalTree(root->right)就不会再执行了,在本题中,遍历该二叉树,使得每个根都与其孩子进行了比较,但凡出现了不相等的情况,则就return fasle,只有左边和右边都返回了true,则最后的结果才是true、//return isUnivalTree(root->left) && isUnivalTree(root->right);}} }}
}
4.3.2、例题2:
相同的树,力扣
//思路://此处所说的相同的树,不仅要保证对应节点上的值相同,还要保证两个树的结构是相同的、//使用递归,分三部分进行比较,分别是根节点,左子树,右子树,在比较根节点时,若结构不同或者结构相同但对应位置上节点中的值不相同的情况有一个时,则就不是相同的树,就不需要再去比较左子树了,直接返回fasle,只有结构相同并且对应位置上节点中的值都相同的情况下,再去比较左子树,比较左子树时,若结构不同或者结构相同但对应位置上节点中的值不相同的情况有一个时同,则就不是相同的树,就不需要再去比较右子树了,直接返回fasle,只有结构相同并且对应位置上节点中的值都相同的情况下,再去比较右子树,比较右子树时,若结构不同或者结构相同但对应位置上节点中的值不相同的情况有一个时,则就不是相同的树,直接返回false,只有结构相同并且对应位置上节点中的值都相同的情况下,则就是相同的树,返回true、
bool isSameTree(struct TreeNode* p, struct TreeNode* q){if(p==NULL && q==NULL){//两个二叉树都是空树,则认为这两个二叉树是相同的树、return true;}//到此,则两个二叉树中至少有一个二叉树不为空树、if(p==NULL || q==NULL){//两个二叉树一个为空树,另外一个不是空树,则两个二叉树必定不是相同的树、return false;}//到此,两个二叉树一定都不是空树,但,是不是相同的树还要根据其val值再去判断一下、if(p->val != q->val){//不是相同的树、return false;}else{//继续比较左子树、bool ret1=isSameTree(p->left,q->left);if(!ret1){//不是相同的树、return false;}else{//继续比较右子树、bool ret2=isSameTree(p->right,q->right);if(!ret2){//不是相同的树、return false;}else{//是相同的树、return true;} //此时两个二叉树的根,左子树部分的结构相同并且对应位置上节点中的值都相同,只有两个右子树结构相同并且对应位置上节点中的值都相同时,整体才为真,否则为假、//当比较右子树时,若结构不同或者结构相同但对应位置上节点中的值不相同的情况有一个时,则整体就不是相同的树,应该返回的是false,而isSameTree(p->right,q->right)返回的就是false,若右子树中结构相同并且对应位置上节点中的值都相同时,则整体就是相同的树,应该返回true,而isSameTree(p->right,q->right)返回的就是true,所以可写成如下所示的代码、//若isSameTree(p->right,q->right)的返回值是true,则整体就是相同的树,则就应该返回true,若isSameTree(p->right,q->right)的返回值是false,则整体就是不相同的树,则就应该返回false、//return isSameTree(p->right,q->right);}//递归比较左右子树、//若两个二叉树中的左子树的结构和值有一个不相同则返回的是false,则&&前为假,而&&后就不再计算了,直接返回false,只有当左子树中的结构和值都相同时,再去比较右子树,若右子树中的结构和值有一个不相同则返回的是false,则&&后为假,整体即为假,只有&&前后都为真时,结果才为真,即只有两个二叉树的根,左子树,右子树结构和值都相同时,则才是相同的树、// return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);}
}
4.3.3、例题3:
//本题是判断是否是相同的树一题的变形,在相同的树该题中,要比较的是两个二叉树,而在本题中,若要进行判断是否是对称二叉树,关键的是要判断该题中的二叉树中的左右子树是否对称,至于根节点是不需要进行判断的,因为根节点在对称轴上,不需要进行判断,所以可以把该题中左右子树看成相同的树一题中的两个二叉树,但是判断方法有所不同,是因为对称的话,要保证结构是对称的而不是相同的,并且对应的节点中的val值是相等的,和上一题是存在差距的、//本题要借助一个辅助函数,要借鉴相同的树一题的解决方法,而在相同的树一题中,是把要进行比较的两个二叉树的根节点的地址传给了指针变量p和q,所以在本题中,要写一个辅助函数,由于在此我们把本题中的二叉树中的左右子树看做了是两个二叉树,模仿相同的树的做法去求解,所以构造一个辅助函数,把该题中的两个子树的根节点的地址传给辅助函数,在辅助函数的形参上使用指针变量p和q来接收,就和相同的树一题靠的更近了、
//若在函数名前加上_,则一般代表的是子函数、
bool _isSymmetric(struct TreeNode* p,struct TreeNode* q)
{//思路://此处所说的对称的树,不仅要保证结构是对称的而不是相同的,还要保证对称位置上的节点的值是相等的、//注意:在此所说的两个二叉树为本题中最大的二叉树中的两个子树,即左子树和右子树,即左子树设为二叉树1,右子树设为二叉树2、//使用递归,把这两个二叉树分三部分进行比较,分别是根节点,左子树,右子树,在比较根节点时,若结构不对称或结构对称但对称位置上节点中的值不相同的情况二者有一个时,则代表着这两个二叉树不是对称的树,就不需要再去对二叉树1中的左子树与二叉树2中的右子树进行比较了,,直接返回fasle,只有结构对称并且对称位置上的节点中的值也相等的情况下,再去比较二叉树1中的左子树和二叉树2中的右子树,在比较二叉树1中的左子树和二叉树2中的右子树时,若结构不对称或结构对称但对称位置上节点中的值不相同的情况二者有一个时,则代表着这两个二叉树不是对称的树,就不需要再去比较二叉树1中的右子树和二叉树2中的左子树了,直接返回fasle,只有结构对称并且对称位置上的节点中的值也相等的情况下,再去比较,在比较二叉树1中的右子树和二叉树2中的左子树时,若结构不对称或结构对称但对称位置上节点中的值不相同的情况二者有一个时,则代表着这两个二叉树不是对称的树,直接返回false,只有结构对称并且对称位置上的节点中的值也相等的情况下,则代表着这两个二叉树是对称的树,返回true、if(p==NULL && q==NULL){//两个二叉树都是空树,则认为这两个二叉树是对称的树、return true;}//到此,则两个二叉树中至少有一个二叉树不为空树、if(p==NULL ||q==NULL){//两个二叉树一个为空树,另外一个不是空树,则两个二叉树必定不是对称的树、return false;}//方法一:/*//到此,两个二叉树一定都不是空树,但,是不是对称的树还要根据其val值再去判断一下、if( p->val != q->val){//一定不是对称的树、return false;}else{//继续比较二叉树1中的左子树和二叉树2中的右子树、bool ret1=_isSymmetric(p->left,q->right);if(!ret1){//一定不是对称的树、return false;}else{//继续比较二叉树1中的右子树和二叉树2中的左子树、bool ret2=_isSymmetric(p->right,q->left);if(!ret2){//一定不是对称的树、return false;}else{//是对称的树、return true;}//此时两个二叉树的前两个部分的结构对称并其对称位置上的节点中的值都相同,只有最后一部分中结构对称并其对称位置上的节点中的值都相同时,整体才为真,否则为假、//当比较最后一部分时,若结构不对称或结构对称但对称位置上节点中的值不相同的情况二者有一个时,则整体就不是对称的树,应该返回的是false,而_isSymmetric(p->right,q->left)返回的就是false,若结构对称并且对称位置上的节点中的值都相等的话,则整体就是对称的树,应该返回true,而_isSymmetric(p->right,q->left)返回的就是true,所以可写成如下所示的代码、//若_isSymmetric(p->right,q->left)的返回值是true,则整体就是对称的树,则就应该返回true,若_isSymmetric(p->right,q->left)的返回值是false,则整体就是不对称的树,则就应该返回false、//return _isSymmetric(p->right,q->left);}}*///方法二:return p->val==q->val && _isSymmetric(p->left,q->right) && _isSymmetric(p->right,q->left);}
bool isSymmetric(struct TreeNode* root)
{if(root==NULL){//空树则满足对称二叉树、return true;}else{//非空树,模仿相同的树一题去解决、return _isSymmetric(root->left,root->right);}
}
4.3.4、例题4:
//计算该二叉树中 有效节点 的个数、int TreeSize(struct TreeNode* root){return root==NULL?0:TreeSize(root->left)+TreeSize(root->right)+1;}
//前序遍历、void _preorder(struct TreeNode* root,int* a,int* pi){if(root==NULL){//空树、return;}else{//非空树、a[(*pi)++]=root->val;_preorder(root->left,a,pi);_preorder(root->right,a,pi);}}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
//Node:若在调用函数内部定义局部数组的话,当出了该调用函数之后,则局部数组就会被销毁,即使把该数组的首元素的地址返回出去也是无用的,是因为局部数组在出了调用函数之后就会被销毁,所以返回出去的指针就变成了野指针,就找不到该局部数组了,再对该野指针进行解引用的话,就是非法访问了,这里的销毁和free动态开辟的内存空间的意思是一样的,都是把该所占用的空间返回给操作系统,只不过是若在堆区上动态开辟内存空间的话,出了调用函数后不会主动销毁动态开辟的内存空间,其生命周期是整个工程,所以需要手动把该动态开辟的内存空间释放,即还给操作系统、
//所以在此不可以定义成局部数组,是因为最后要把该数组的地址返回出去,若定义成局部数组的话,当把该数组的地址返回出去之后,局部数组就会被销毁,此时该局部数组的地址就是一个野指针,就找不到该局部数组了,所以必须要动态开辟数组,这样再把该数组的地址返回出去,即使出了调用函数该动态开辟的数组也不会被销毁,是因为在堆区上进行开辟的,其生命周期是整个工程,当再使用其地址时, 还是可以根据其地址找到该动态开辟的数组,只不过在最后要手动释放该动态开辟的内存空间,而题目已经告诉假设该动态开辟的内存空间由调用者去释放,所以在此就不需要对该动态开辟的空间进行释放了、//计算该二叉树中 有效节点 的个数、
int size=TreeSize(root);int* a=(int*)malloc(sizeof(int)*size);
*returnSize=size;
assert(a);
//此处不可以直接调用preorderTraversal函数自身,否则的话,每次都要进行malloc操作,所以要写一个子函数来进行递归操作、
int i=0;
_preorder(root,a,&i);
return a;
}
//在OJ题中一般不使用静态变量和全局变量、
4.3.5、例题5:
//Node:平常所说的二叉树的两个子树一般即指二叉树的左子树和右子树,和本题中的子树的概念有所差距,本题中所谓的子树是指,该二叉树的左子树和右子树属于子树,左子树的左子树和右子树也属于该最大的二叉树的子树,同理,右子树的左子树和右子树也属于该最大的二叉树的子树,即指该最大的二叉树的子树包括该二叉树中某一个节点以及该节点所有的后代,这就属于该最大的二叉树的一个子树,并且,该最大的二叉树也可以看做是该最大的二叉树的一个子树,但是平常所说的两个子树指的就是二叉树的左子树和右子树、//思路:
//让二叉树subRoot与二叉树root中的每一个子树比较一下是否是相同的树,而二叉树root中的每一个节点都是其某一个子树的根节点,则可以遍历二叉树root,拿到该二叉树root的每一个节点作为二叉树root的某个子树的根节点,与二叉树subRoot去比较是否是相同的树,即调用isSameTree函数、
bool isSameTree(struct TreeNode* p, struct TreeNode* q){if(p==NULL && q==NULL){//两个二叉树都是空树,则认为这两个二叉树是相同的树、return true;}//到此,则两个二叉树中至少有一个二叉树不为空树、if(p==NULL || q==NULL){//两个二叉树一个为空树,另外一个不是空树,则两个二叉树必定不是相同的树、return false;}//到此,两个二叉树一定都不是空树,但,是不是相同的树还要根据其val值再去判断一下、return p->val == q->val && isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){/*//方法一://三次比较为一组、//该方法不太好,中间存在重复的部分、//Node:在本题中,默认二叉树subroot不是空树,若是空树的话,则二叉树subroot是任意一个二叉树root的子树了,因为,空树是任意一个二叉树的子树,就没有意义、//所以在此subRoot一定不是空指针NULL,则当调用函数isSameTree(root,subRoot)时,若root等于NULL,则进入第二个if语句,若root不等于NULL时,则进入第三个if语句,所以,假设root等于NULL,则进入第二个if语句,则返回的是false,此时isSameTree(root,subRoot)函数的返回值就是false,假,则不进入下面的第一个if语句,直接进入第一个else语句,当调用函数isSameTree(root->left,subRoot)时,由于root为空指针NULL,所以root->left,就会出错,所以,当root为空指针时,直接返回false即可,因为二叉树subroot不是空树,而二叉树root是空树,那么二叉树subroot肯定不是二叉树root的子树,直接返回fasle,只有当root不是空树的时候,再去判断、if(root==NULL){return false;}//比较二叉树root中以3为根节点,即以二叉树root的第一个节点为根节点的子树与二叉树subroot,判断两个二叉树是否是相同的树、if(isSameTree(root,subRoot)){//若是相同的树,则说明二叉树subroot是二叉树root的一个子树,直接返回true即可,不需要再进行以二叉树root第二个节点为根节点,即以二叉树root中以4为根节点的的子树与二叉树subroot之间的比较了、return true;} else{//若是不相同的树,则需要再进行以二叉树root第二个节点为根节点,即以二叉树root中以4为根节点的的子树与二叉树subroot之间的比较了、if(isSameTree(root->left,subRoot)){//若是相同的树,则说明二叉树subroot是二叉树root的一个子树,直接返回true即可,不需要再进行以二叉树root第三个节点为根节点,即以二叉树root中以5为根节点的的子树与二叉树subroot之间的比较了、return true;}else{//若是不相同的树,则需要再进行以二叉树root第三个节点为根节点,即以二叉树root中以5为根节点的的子树与二叉树subroot之间的比较了、if(isSameTree(root->right,subRoot)){//若是相同的树,则说明二叉树subroot是二叉树root的一个子树,直接返回true即可,不需要再进行以二叉树root第四个节点为根节点,即以二叉树root中以1为根节点的的子树与二叉树subroot之间的比较了、return true;}else{//若是不相同的树,则需要再进行以二叉树root第四个节点为根节点,即以二叉树root中以1为根节点的的子树与二叉树subroot之间的比较了,再次使用递归即可、//继续比较左子树、bool ret1= isSubtree(root->left,subRoot);if(ret1){//二叉树subroot是二叉树root的子树、return true;}else{//继续比较右子树、bool ret2= isSubtree(root->right,subRoot);if(ret2){//二叉树subroot是二叉树root的子树、return true;}else{//二叉树subroot不是二叉树root的子树、return false;}}}}}*//*//方法二://一次比较为一组,中间不存在重复的部分、if(root==NULL){return false;}//比较二叉树root中以3为根节点,即以二叉树root的第一个节点为根节点的子树与二叉树subroot,判断两个二叉树是否是相同的树、if(isSameTree(root,subRoot)){//若是相同的树,则说明二叉树subroot是二叉树root的一个子树,直接返回true即可,不需要再进行以二叉树root第二个节点为根节点,即以二叉树root中以4为根节点的的子树与二叉树subroot之间的比较了、return true;}else{//若是不相同的树,则需要再进行以二叉树root第二个节点为根节点,即以二叉树root中以4为根节点的的子树与二叉树subroot之间的比较了、//继续比较左子树、bool ret1= isSubtree(root->left,subRoot);if(ret1){//二叉树subroot是二叉树root的子树、return true;}else{//继续比较右子树、bool ret2= isSubtree(root->right,subRoot);if(ret2){//二叉树subroot是二叉树root的子树、return true;}else{//二叉树subroot不是二叉树root的子树、return false;}}}*///方法三://一次比较为一组,中间不存在重复的部分、if(root==NULL){return false;}else{return isSameTree(root,subRoot) || isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);}
}
4.4、二叉树的创建和销毁:
#include<stdio.h>
typedef struct BinaryTreeNode
{char data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;//二叉树的构建也使用 递归 方法去写、
BTNode* CreateBTreeNode(char* arr,int* pi)
{//Node:若输入的字符串一上来就是#开头并且#后面还有其他的字符的话,该字符串肯定是不对的,若是如此的话,//则整个二叉树的根节点的地址就是空指针NULL,即整个二叉树就是一个空树,但是这第一个#后面就不会再有其他的字符了,因为整个//二叉树已经为空树了,空树里面不会再有东西了、//若只输入一个#,则代表着构建了一个空树,打印输出则为空、if(arr[*pi]=='#'){//空树、(*pi)++;return NULL;}//非空树、BTNode* root=(BTNode*)malloc(sizeof(BTNode));//在牛客或者力扣上,一般不需要对动态开辟的内存空间的地址进行检查、root->data=arr[(*pi)++];//递归、root->left=CreateBTreeNode(arr,pi);root->right=CreateBTreeNode(arr,pi);return root;
}//中序遍历并打印、
void InOrder(BTNode* root)
{if(root==NULL){return ;}InOrder(root->left);printf("%c ",root->data);InOrder(root->right);
}
int main()
{//输入包括1行字符串,长度不超过100、char arr[100]={0};scanf("%s",arr);int i=0;//传址调用、BTNode* tree=CreateBTreeNode(arr,&i);//传值调用、InOrder(tree);return 0;
}
4.5、常见的例题:
4.5.1、例题1:
4.5.2、例题2:
4.5.3、例题3:
4.5.4、例题4:
关于二叉树的初阶,到此已经全部结束,感谢大家点赞关注加收藏、