目录
一、 树的概念与结构
树是一种非线性结构,数据结构中所说的树,是现实生活中“倒过来的树”,以根为起始,开始衍生
- 有个特殊的节点:根节点,没有前驱节点
- 除根节点外,其余结点被分成m(m>0)个互不相交的集合,T1、T2、……、Tm其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继节点
- 所以,树是递归定义的
注意:在书的结构中,子树间不能有交集
树的常见用语:
- 节点的度:一个节点含有的子树的个数称为该节点的度,如下图:A的度为6,E的度为2
- 叶节点:度为0的节点称为叶节点,如下图:B C H I P Q K L M N无子节点,即为叶节点
- 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点,如下图:D为H的父节点
- 子节点:一个节点含有的子树的根节点称为该节点的子节点,如下图:J的子节点有P Q
- 兄弟节点:具有相同父节点的节点互称为兄弟节点,如下图:P Q互为兄弟节点
- 树的度:一棵树中,最大的节点的度称为树的度,如下图,树的度为6
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推,如下图:树有4层
- 树的高度或深度:树中节点的最大层次,如下图:最大深度为4
二、二叉树的概念与结构
1.概念
一棵二叉树是结点的一个有限集合,该集合:
- 可能为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
- 二叉树度的取值只可能是0、1、2
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
这是二叉树的几种情况:
2.特殊的二叉树
- 满二叉树:所有层的节点都被完全填满。在满二叉树中,叶节点的度为0,其余所有节点的度都为2,假设有h层,那么满二叉树共有(2^h - 1)个节点
- 完全二叉树:只有最底层的节点未被填满,且最底层节点尽量靠左填
3. 二叉树的性质
-
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点
-
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1
-
对任何一棵二叉树, 如果度为0的结点个数为n0,度为2的结点个数为n2,则有n0 = n2 + 1
-
若规定根节点的层数为1,具有n个结点的满二叉树的深度 h = log2(𝑛+1)
三、二叉树基本操作
//定义一个二叉树的结构
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
1.手搓一颗二叉树
二叉树是种复杂的非线性结构,增删查改效率太低,我们学习二叉树是为了学习二叉树递归和分治的思想
//接口实现
//创建新的节点
BTNode* BuyNode(BTDataType val);
//手搓一颗树
BTNode* TreeCreate(BTNode* root);
//代码实现
//创建新的节点
BTNode* BuyNode(BTDataType val) {
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL) {
perror("malloc() err!");
return;
}
newnode->data = val;
newnode->left = newnode->right;
return newnode;
}
//手搓一颗树 层序:1 2 4 3 N 5 6
BTNode* TreeCreate(BTNode* root) {
BTNode* n1 = BuyNode(1);
BTNode* n2 = BuyNode(2);
BTNode* n3 = BuyNode(3);
BTNode* n4 = BuyNode(4);
BTNode* n5 = BuyNode(5);
BTNode* n6 = BuyNode(6);
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
return n1;
}
这是我们手搓出来的二叉树示意图:
2.二叉树的遍历
2.1. 前序遍历
前序遍历按照 根 左子树 右子树的顺序
如图:
开始根1完成,往下走,
走到以2为根的树,根2,完成,往下走
走到以3为根的树,根3,完成,往下走
遇到NULL,3的左子树为空,回溯,回到3,开始找3的右子树,往下走
遇到NULL,3的右子树为空,回溯,回到3,以3为根的树遍历完成,而3是2的左孩子,回溯,回到2,开始找2的右孩子,往下走
遇到NULL,2的右子树为空,回溯,回到2,以w为根的树遍历完成,而2是1的左孩子,回溯,回到1,开始找1的右孩子,往下走
…………………………
这体现了“先走到尽头,再回溯继续”的思想,前中后序都是这种思想
将复杂的一颗树不断细分 分成 根 左子树 右子树,直到这个树不能再分,这便是递归的思想
- “递”表示开启新方法,程序在此过程中访问下一个节点。
- “归”表示函数返回,代表当前节点已经访问完毕。
代码实现:
// 二叉树前序遍历 根 左 右
void BinaryTreePrevOrder(BTNode* root) {
//N代表空
if (root == NULL) {
printf("N ");
return;
}
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
2.2. 中序遍历
中序遍历按照 左子树 根 右子树的顺序
开始遇到根1,找根1的左子树,往下走
遇到根2,找根2的左子树,往下走
遇到根3,找根3的左子树,往下走
遇到NULL,根3的左子树找到,回溯,回到根3,开始找根3的右子树,往下走
遇到NULL,根3的右子树找到,回溯,回到根3,以3为根的树结束,回溯,回到根2,开始找根2的右子树
………………………………
代码实现:
// 二叉树中序遍历 左 根 右
void BinaryTreeInOrder(BTNode* root) {
//N代表空
if (root == NULL) {
printf("N ");
return;
}
BinaryTreeInOrder(root->left);
printf("%d ", root->data);
BinaryTreeInOrder(root->right);
}
2.3. 后序遍历
后序遍历按照 左子树 右子树 根的顺序
代码实现:
// 二叉树后序遍历 左 右 根
void BinaryTreePostOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%d ", root->data);
}
2.4. 层序遍历
顾名思义,按照一层一层遍历,我们需要借助队列先进先出的性质实现
代码实现:
// 层序遍历 利用队列性质
void BinaryTreeLevelOrder(BTNode* root) {
//创建一个队列来存储二叉树的数据
Queue helpQueue;
QueueInit(&helpQueue);
//根不为空,入队列
if (root != NULL)
QueuePush(&helpQueue, root);
//不断地入队列 出队列 一层出 一层进
while (!QueueEmpty(&helpQueue)) {
BTNode* front = QueueFrontVal(&helpQueue);
QueuePop(&helpQueue);
if (front != NULL) {
//front不为空,打印下来
printf("%d ", front->data);
//带入下一层
QueuePush(&helpQueue, front->left);
QueuePush(&helpQueue, front->right);
}
else {
printf("N ");
}
}
QueueDestory(&helpQueue);
}
3.求一棵二叉树的最大深度
代码实现:
//二叉树的高度
int BinaryTreeHeight(BTNode* root) {
//空树
if (root == NULL) {
return 0;
}
//分治 分别求解左右子树高度
int leftHeight = BinaryTreeHeight(root->left);
int rightHeight = BinaryTreeHeight(root->right);
//大的加根节点高度 即是二叉树的高度
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
4.求解一颗二叉树的节点个数
利用分治的思想,举个例子
一个学院一个院长,两个专业,一个专业四个班,统计这个学院总共有多少人
院长将任务分发给两个专业负责人,两个专业负责人继续分发任务,给自己管理的班级再次下发任务,任务分配给四个班长,班长统计好之后,回溯给两个专业负责人,两个负责人再回溯给院长,院长根据回溯回来的数据即可得到统计
将一个大问题不断细化,分成若干个子问题,直到子问题不可再分,解决最小子问题之后,不断回溯
代码实现:
// 二叉树节点个数
int BinaryTreeSize(BTNode* root) {
//分治的思想 分别统计左子树节点个数和右子树节点个数 最后加上根节点自己
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
5.求解一颗二叉树第k层的节点个数
代码实现:
// 二叉树第k层节点个数
//左子树第k-1层节点数 + 右子树第k-1层节点数
int BinaryTreeLevelKSize(BTNode* root, int k) {
//空树 无节点
if (root == NULL) {
return 0;
}
//k == 1 指向根节点
if (root != NULL && k == 1) {
return 1;
}
//开始找子树
if (k > 1) {
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
}
6.查找值为x的节点
检查根值是否==x,之后先找左子树,如果左子树找到直接返回,若未找到,则继续在右子树中找
代码实现:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
//空树
if (root == NULL) {
return NULL;
}
//分治找左子树 右子树节点
// 这个方法
if (root->data == x) {
return root;
}
//分治 先找左子树 找到了直接返回 没找到在找右子树
BTNode* findLeft = BinaryTreeFind(root->left, x);
if (findLeft != NULL) {
return findLeft;
}
BTNode* findRight = BinaryTreeFind(root->right, x);
if (findRight != NULL) {
return findRight;
}
return NULL;
}
7.判断一棵二叉树是否为完全二叉树
非完全二叉树的层序遍历:
完全二叉树的层序遍历:
代码实现:
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root) {
Queue helpQueue;
QueueInit(&helpQueue);
// 空树视为完全二叉树
if (root == NULL) {
QueueDestroy(&helpQueue);
return true;
}
QueuePush(&helpQueue, root);
bool reachNull = false; // 标记是否遇到空节点
while (!QueueEmpty(&helpQueue)) {
BTNode* front = QueueFrontVal(&helpQueue);
QueuePop(&helpQueue);
if (front) {
// 如果之前已遇到空节点,但当前节点非空 → 不完全
if (reachNull) {
QueueDestroy(&helpQueue);
return false;
}
// 无论子节点是否为空都入队
QueuePush(&helpQueue, front->left);
QueuePush(&helpQueue, front->right);
}
else {
// 首次遇到空节点时设置标记
reachNull = true;
}
}
QueueDestroy(&helpQueue);
return true;
}
结语
二叉树初阶和中阶问题基本讲清楚了,还剩下高阶知识等我去学,诸君共勉!