文章目录
前言
二叉树部分重点考察我们的递归,倘若你对递归不熟悉或不了解递归,可以先去熟悉熟悉哦~
一、树
1.1、树的定义及概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的
- 有一个特殊的结点,我们成为根结点,根节点没有前驱;
- 除根节点外,其余结点又被分为一颗颗结构和树相似的子树,与根节点不同的是,这里的每一颗子树有且仅有一个前驱,可以有多个或者没有后继;
- 树,是递归定义的;
这里值得注意的是 :树形结构中,子树之间不能有交集,否则就不是树形结构!!!
- 子树是不相交的。
- 除了根节点,每个结点有且仅有一个根节点结点,即父亲结点。
- 一颗N个结点的树有N-1条边。
1.2、树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为3。
- 叶节点或终端节点:度为0的节点称为叶节点; 如上图:J、K、L、H、I节点为叶节点
- 非终端节点或分支节点:度不为0的节点; 如上图:B、C、D、E…等节点为分支节点
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
- 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为3。
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
- 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:F、G互为兄弟节点。
- 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
- 森林:由m(m>0)棵互不相交的树的集合称为森林。
1.3、树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
1.4、树在实际中的运用
- 表示文件系统的目录树结构
二、二叉树的概念与结构
2.1、概念
一棵二叉树是结点的一个有限集合,该集合:
-
- 或者为空。
-
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2、特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3、二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 个结点.
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 .
- 对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有 = +1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= . (ps: 是log以2
为底,n+1为对数) - 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
- 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
- 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
三、二叉树的实现(链式)
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,这里我们用二叉链表实现即可。
3,1、二叉树结构设计
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}TreeNode;
由上可以看出结构体中共有三个变量,一个放数据,一个指向左子树,另一个指向右子树。
注:待会实现二叉树的层序遍历和判断是否是完全二叉树都需要用到队列,因为这种方式实现起来比较简单,而且咱们在之前也实现过队列,可以直接拿过来用,相当方便。
3.2、函数的功能接口
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a,int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
1、二叉树的创建
创建二叉树,我们首先要创建根节点,并向结点里面保存数据,然后接着指向左孩子,然后往里面存入数据,最后是右孩子,其实这种先访问根节点后访问左节点最后访问右节点的顺序就是二叉树的前序遍历,后面慢慢讲。
假设,我们要创建出这样一颗二叉树需要什么步骤呢?
我们可以这样:先创建根节点A,然后是B、C…,那么我们怎么判断一个结点下面是否存在一个或两个结点呢?又或者是一个结点都没有呢?对于每一个非叶子结点,如果没有两个结点,就对另一个结点置空,那么怎么置空呢?那么我们在输入数据的时候对于没有数据的结点也就是空结点,我们输入:“#”,为"#"就返回空(NULL)。
递归实现:
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if ((*pi) == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
printf("malloc fail");
exit(-1);
}
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
2、二叉树的销毁
这里采用的是二级指针
//二叉树的销毁
void DestroyTree(BTNode** root)
{
if (*root)
{
DestroyTree(&(*root)->left);
DestroyTree(&(*root)->right);
free(*root);
*root = NULL;
}
}
3、二叉树结点个数
先看代码:
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
这里我就偷懒拿个简单的二叉树来说明了,如下图所示二叉树:
递归每一个结点
4、二叉树叶子结点个数
如果根节点为空,则说明为空树,直接返回0,根不为空但是左右子树都为空,说明只有一个根结点,返回个数1。
代码:
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
画太潦草了,希望各位能理解
5、二叉树第K层结点个数
第一层就一个根结点,若此树只有一个根结点,则返回个数为1,否则依次向下访问K-1层,一直到K-1=0也就是到K等于1为止。也是采用递归思想
代码:
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
图像依旧抽象,嘻嘻~但其实理解递归思想还是很快能看懂的。
6、查找二叉树中值为X的结点
依次遍历查找,找到了返回该结点,没找到继续往下找,直到找到为止,若要找的值不存在这棵树中,则程序返回空。
代码:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
//下面代码注释部分也是一种方法,看个人喜好
// BTNode* ret1=BinaryTreeFind(root->_left,x);
// if(ret1)
// return ret1;
// BTNode* ret2=BinaryTreeFind(root->_right,x);
// if(ret2)
// return ret2;
// return NULL;
return BinaryTreeFind(root->left, x) || BinaryTreeFind(root->right, x);
}
7、二叉树的前、中、后遍历操作
7.1、前序遍历
前序遍历就是从根结点开始,然后访问左子树,最后访问右子树,若二叉树为空树,则直接返回NULL,遍历方式:根 -> 左子树-> 右子树,如图所示遍历结果为:1 2 3 N N 4 N N 5 N 6 N N(N则表示空,即NULL)。
代码:
//二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
return NULL;
//根 -> 左子树 -> 右子树
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
7.2、中序遍历
中序遍历是从根的左子树开始的,直到叶子结点后,回到根结点,然后再遍历右子树。所以它的遍历方式为:左子树 -> 根 -> 右子树。如上图所示遍历结果为:N 3 N 2 N 4 N 1 N 5 N 6 N(其中N代表NULL空)。
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
return NULL;
//左子树 -> 根 -> 右子树
BinaryTreeInOrder(root->left);
printf("%d", root->data);
BinaryTreeInOrder(root->right);
}
7.3、后序遍历
后续遍历的遍历方式:左子树 -> 左子树 -> 根。最后才访问根结点,遍历结果为:N N 3 N N 4 2 N N N 6 5 1。
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
return NULL;
//左子树 -> 右子树 -> 根
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%d", root->data);
}
8、层序遍历
层序遍历(Level Order Traversal)是一种针对树(Tree)或图(Graph)的遍历方式,它按照层次顺序逐层遍历结点。这种遍历方式通常利用队列来实现。
以下是层序遍历的基本思想:
- 从根节点开始,将根节点入队。
- 当队列不为空时,执行以下步骤
-出队列并访问当前结点。
-将当前节点的所有子节点(如果存在)依次入队。 - 重复步骤2,直到队列为空。
层序遍历保证了在同一层次的节点先于下一层次的节点被访问,因此可以方便地按照层次遍历树或图的节点。
这种遍历方式的一个应用是查找树的最小深度或最大宽度,以及图中的最短路径等。在树的层序遍历中,可以轻松地找到每一层的节点,而不需要对树进行深度优先搜索或广度优先搜索。
在实现层序遍历时,一般需要使用队列来保存待访问的节点,确保按照层次顺序访问节点。
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
int BinaryTreeLevelKSize= 1;
while (!QueueEmpty(&q))
{
// 一层一层出
while (BinaryTreeLevelKSize--)
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
BinaryTreeLevelKSize= QueueSize(&q);
}
printf("\n");
QueueDestroy(&q);
}
9、判断一颗树是否是二叉树
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
int BinaryTreeLevelKSize = 1;
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 前面遇到空以后,后面还有非空就不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
判断一颗树是否为二叉树还可以利用递归实现:
可以通过递归遍历树的节点,并在遍历的过程中判断每个节点是否满足二叉树的性质。其中,层序遍历是一种常用的方法之一。
以下是判断一棵树是否为二叉树的基本思路:
-
从根节点开始,对每个节点进行检查:
检查当前节点的左子节点是否存在,若存在则继续向左子节点递归检查。
检查当前节点的右子节点是否存在,若存在则继续向右子节点递归检查。 -
在递归的过程中,如果任何节点不满足以下条件之一,则树不是二叉树:
节点为空(树的叶子节点)。
节点存在左子节点,但左子节点的值大于等于当前节点的值。
节点存在右子节点,但右子节点的值小于等于当前节点的值。 -
如果在遍历过程中所有节点都满足以上条件,则该树为二叉树。
总结
二叉树整体使用递归思想,各位同学在学习过程中需要多画图,画递归展开图更有利于我们理解。