二叉树(链式结构)
前面的文章首先介绍了树的相关概念,阐述了树的存储结构是分为顺序结构和链式结构。其中顺序结构存储的方式叫做堆,并且对堆这个数据结构进行了模拟实现,并进行了相关拓展,接下来会针对链式结构的存储方式的树进行介绍。
1. 链式结构的二叉树
用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
1.1 链式二叉树的结构
其结构如下:
c
//定义二叉树链式结构
//二叉树结点的结构
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
BTNode* left;
BTNode* right;
}BTNode;
- 这里还是利用
typedef
对二叉树结构体进行重定义,定义为新的类型BTNode
,对于结构体中用于存储数据的变量data的类型int
再使用typedef
定义,便于后期的更改和维护。
1.2 创建一个链式二叉树
c
//创建一个二叉树节点并初始化
BTNode* buyNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
//开辟成功,初始化
newnode->data = x;
newnode->left = newnode->right = NULL;
//返回新节点
return newnode;
}
//手动创建一个二叉树
void createTree()
{
//创建数据
BTNode* node1 = buyNode(1);
BTNode* node2 = buyNode(2);
BTNode* node3 = buyNode(3);
BTNode* node4 = buyNode(4);
BTNode* node5 = buyNode(5);
BTNode* node6 = buyNode(6);
//连接数据
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
}
经过以上代码即可调整出如下图的二叉树。

总结:
- 回顾二叉树的概念,二叉树分为空树和非空二叉树
- 非空二叉树由根结点、根结点的左子树、根结点的右子树组成的
- 根结点的左子树和右子树分别又是由子树结点、子树结点的的左子树、子树结点的右子树组成的,因此二叉树定义是递归式的,后序链式二叉树的操作中基本都是按照该概念实现的。
2. 二叉树的前中后序遍历
2.1 遍历规则
按照规则,二叉树的遍历有:前序 / 中序 / 后序的递归结构遍历:
- 前序遍历 (Preorder Traversal 亦称先序遍历) :访问根结点的操作发生在遍历其左右子树之前
访问顺序为:根结点、左子树、右子树 - 中序遍历 (Inorder Traversal) :访问根结点的操作发生在遍历其左右子树之中(间)
访问顺序为:左子树、根结点、右子树 - 后序遍历 (Postorder Traversal) :访问根结点的操作发生在遍历其左右子树之后
访问顺序为:左子树、右子树、根结点
2.2 详解遍历
2.2.1 前序遍历
2.2.1.1 代码示例
c
//实现二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d", root->data);
//左右两个子树作为根节点继续遍历
//根据:根左右,所以先从左节点开始
PreOrder(root->left);
//左子树遍历完成,遍历右子树
PreOrder(root->right);
}
2.2.1.2 代码解释
这里使用递归的思想,因为为前序遍历是根左右 的思想,所以先打印根结点的数据;再按顺序依次将每个左结点 作为新的根结点 传给PreOrder函数,并每次将新的根结点数据进行打印 ,然后进行递归循环执行,直到遍历到二叉树最后一层的最左侧叶子结点 ,打印此数据并回退至其父结点;之后再检测此父节点有无右结点,若有则打印,没有则继续回退至此父结点的父结点 ;不断回退打印直到根结点的左子树遍历完成 ,之后再以相同的根左右的思路继续遍历右子树。
最终遍历一遍之后,就可以得到先序遍历的数据。
2.2.1.3 前序遍历的本质
-
根左右
-
先打印后遍历
-
对于前序遍历的本质可以理解为,一个小人开始绕着整棵树的外围转一圈,经过的结点顺序就是前序遍历的顺序。

2.2.2 中序遍历
2.2.2.1 代码示例
c
//实现二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
//不断递归找到左子树最左下角的数据
InOrder(root->left);
//找到之后打印数据
printf("%d ", root->data);
//左子树遍历完成,继续遍历右子树
InOrder(root->right);
}
2.2.2.2 代码解释
这里仍然使用函数递归的思想,根据中序遍历左根右 的思想,首先递归遍历二叉树的左子树,直到遍历至左子树中最后一层的最左侧叶子结点 ,开始打印此数据,并回退至最左侧叶子结点的父节点 并打印,之后再检测此父结点有无右结点,若有则打印,没有则继续回退至此父结点的父结点 ;不断回退打印直到根结点的左子树遍历完成 ,此时再打印根结点数据 ,然后继续以相同的左根右的思路遍历右子树。
最终遍历完所有数据之后,就可以得到中序遍历的数据。
2.2.2.3 中序遍历的本质
- 左根右
- 打印放遍历中间
- 中序遍历可以想象成,按树画好的左右位置直接投影下来即可

2.2.3 后序遍历
2.2.3.1 代码示例
c
//实现二叉树后序遍历--左右根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
2.2.3.2 代码解释
这里仍然借用递归的思想,根据后序遍历左右根 的思想,首先由根结点遍历左子树找到左子树中最后一层的最左侧叶子结点 ,开始打印此数据,并回退至最左侧叶子结点的父节点 ,进入其父结点的函数栈帧,执行代码PostOrder(root->right);
,检测此父结点 是否有有右结点 ,若有则打印,再返回回其父结点 并打印;若没有这直接回退至其父节点 并打印父节点中的数据,之后不断循环递归直到回到根结点,再根据上述思路,诋毁遍历根结点的右子树。
最终遍历完所有数据之后,就可以得到后序遍历的数据。
2.2.3.3 后序遍历的本质
- 左右根
- 先遍历,后打印
- 后序遍历就像剪葡萄,把一串葡萄剪成一颗一颗的,按照之前先序遍历的路径,围着树的外围绕一圈,如果发现一剪刀就可以剪下来的葡萄,就把他剪下来,这样就组成后序遍历了

3. 二叉树中结点个数以及高度的实现(递归)
3.1 求二叉树中结点个数
3.1.1 代码示例
c
// ⼆叉树结点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
3.1.2 代码解释
在面对求解二叉树总结点的问题时,可以将问题拆解成子问题:
- 若根结点为空则直接返回,结点个数为0
- 若根结点不为空,则先计算此时的结点本身,再继续遍历根结点的左右子树
二叉树结点总个数 = 根结点左子树个数 + 根结点右子树个数 + 1(根结点本身)
3.1.3 递归详解(return的讲解)
这里思考本质就把递归只放在一个随机结点处,此时这个结点接收到了其左子树 和右子树 返回给他的结点个数,此节点需要先**+1(加上自己本身的结点数),再将新的结点个数传递给其父结点**。
所以针对根节点的总结点数 ,直接递归遍历其根结点左子树 和右子树的结点数再**+1**相加即可。
**补充:**这里相当于每一个结点都会创建一个函数栈帧,每一个函数栈帧都会在返回值+1,记录结点数。
3.2 求二叉树中叶子结点个数
3.2.1 代码示例
c
// ⼆叉树叶⼦结点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//确认叶子结点,是叶子结点返回1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
//总叶子结点个数 = 左子树叶子结点个数 + 右子树叶子结点个数
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
3.2.2 代码解释
子问题拆解:
- 若根结点为空,则叶子结点个数为0
- 若结点的左指针和右指针均为空,则该节点为叶子结点返回1
- 除上述两种情况外,说明该树存在子树,继续向下遍历
二叉树叶子结点个数 = 左子树的叶子结点个数 + 右子树的叶子结点个数
3.2.3 递归详解(return的讲解)
同样这里从递归的本质进行思考,针对普通情况任何一点的叶子结点 ,都需要分别不断向下遍历,直到符合子问题2,找到叶子结点则返回1 (数据的调整实在函数回退的时候实现的 ),并开始回退至其父节点,再执行+号后面的代码,检测此父节点 的右子树是不是叶子结点,若是则继续返回1,再回退至其父节点若不是则直接回退。不断回退递归直到回到初始节点,此时此节点的左右子树的叶子结点相加即可。
所以针对根节点的叶子结点数 ,直接递归遍历其根结点的左子树 和右子树的叶子结点再相加即可。
3.3 求二叉树中第k层结点个数
3.3.1 代码示例
c
// ⼆叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
//判断结点是不是第k层,如果是则返回1
if (k == 1)
{
return 1;
}
//第k层结点个数 = 左子树的第k层结点个数 + 右子树的第k层结点个数
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
3.3.2 代码解释
子问题拆解:
- 若根节点为空,则二叉树第k层结点为0
- 设置标志位k用于表示是树的第几层,如果该节点的标志位层数为k,则返回1
- 若均不是以上两种情况表示,还不是第k层的结点,继续向下遍历
相对于根结点的第k层结点的个数 = 相对于以其左孩子为根的第k-1层结点的个数 + 相对于以其右孩子为根的第k-1层结点的个数
3.3.3 本质理解(return详解)
对任意一个二叉树 的第k层结点数量,都是由根节点的左子树和右子树 出发,先由左子树出发 直到符合子问题2,找到位于第k层的结点,并返回1(数据的调整实在函数回退的时候实现的 ),并开始回退至其父节点,再执行+号后面的代码,检测此父节点 的是否有右孩子结点,因为此时的k-1与+号前的函数统一 ,所以不需要再判断是不是第k层,直接判断此层有无数据即可。之后就不断回退递归直到回到根结点 ,此时该二叉树的第k层结点数量将左右子树的第k层结点数量相加即可。
对于结点第K层结点的个数由左子树的第k-1层结点个数 与右子树的第k-1层结点个数相加即可。
3.4 求二叉树深度/高度
3.4.1 代码示例
c
//⼆叉树的深度/⾼度
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftDep = BinaryTreeDepth(root->left);
int rightDep = BinaryTreeDepth(root->right);
//返回最终比较大的高度
return leftDep > rightDep ? leftDep + 1 : rightDep + 1;
}
3.4.2 代码解释
子问题拆解:
- 若为空,则深度为0
- 若不为空,则先对标志位 + 1
树的最大深度 = 左右子树中深度较大的值 + 1(根结点也代表一个深度)
3.4.3 本质理解(return详解)
对于求任意一个二叉树深度,都是由根结点的左右子树出发 ,先从左子树出发直到遍历至最左侧叶子结点 ,为左子树标志位leftDep + 1之后,回退至其父结点 ,再检测此父结点是否有右孩子结点若有则为右子树标志位rightDep + 1,再回退至父结点;若没有则直接回退至父结点。经过递归回退不断对leftDep和rightDep进行迭代,最后选择大的再加上根结点本身作为二叉树深度返回。
3.4 求二叉树深度/高度
3.4.1 代码示例
c
// ⼆叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
//判断是否找到x
if (root->data == x)
{
//找到直接返回数据所在的结点
return root;
}
//递归遍历左子树
BTNode* leftFind = BinaryTreeFind(root->left, x);
if (leftFind)
{
return leftFind;
}
//递归遍历右子树
BTNode* rightFind = BinaryTreeFind(root->right, x);
if (rightFind)
{
return rightFind;
}
return NULL;
}
3.4.2 代码解释
子问题拆解:
- 判断根结点是否是目标结点,若是则返回此结点
- 若不是,先在左子树寻找,再在右子树寻找
3.5 二叉树的销毁
3.5.1 代码示例
c
// ⼆叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
{
return;
}
//先释放左子树和右子树
BinaryTreeDestory(&((*root)->left));
BinaryTreeDestory(&((*root)->right));
//释放结点
free(*root);
*root == NULL;
}
3.5.2 代码解释
- 这里函数的参数传的是二级指针,因为销毁二叉树要操作的参数是结点的地址,然而结点的类型是结构体,所以结构体的地址用二级指针表示。
- 对于求任意一个二叉树的销毁,都是由根结点的左右子树出发 ,先从左子树出发直到遍历至最左侧叶子结点 ,销毁此节点后,回退至其父结点 ,再检测此父结点是否有右孩子结点若有则销毁此结点,再回退至父结点;若没有则直接回退至父结点。经过递归回退直到遍历至根节点释放掉即可实现二叉树的销毁。
3.6 总结
- 以上递归思路都是先遍历左子树在遍历右子树,采用的主要是左根右的中序遍历思想。
- 上述代码中对于左子树和右子树的遍历,并不是一次性将根结点的左子树遍历完成再遍历右子树,而是针对每一个节点都是先遍历左子树再遍历右子树 。(中序遍历的重要思想注意!!注意!!)
4. 层序遍历
4.1 概念介绍
除了先序遍历、中序遍历、后序遍历外,还可以对二叉树过进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第-一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问 树的结点的过程就是层序遍历。
实现层序遍历需要额外借助数据结构:队列

4.2 代码示例
c
//Queue.h
//定义队列结构
//通过修改重定义,将队列中的数据data改为结点
//typedef int QDataType;
typedef struct BinaryTreeNode* QDataType;
//typedef struct BTNode* QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
int size;//保存队列有效数据个数
}Queue;
//Tree.c
//层序遍历
//借助数据结构---队列
void LevelOrder(BTNode* root)
{
//创建队列,并初始化
Queue q;
QueueInit(&q);
//先将根结点放入队列
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
//取队头,并打印
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
//打印后出队列
QueuePop(&q);
//队头节点的左右孩子入队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
//队列为空,销毁队列
QueueDestroy(&q);
}
4.3 逻辑详解
-
要想实现层序遍历是没有办法通过想前面实现前中后序遍历那样通过递归实现,这里借助队列这一数据结构实现层序遍历。
-
用队列实现层序遍历详解:
-
首先根结点(1)进队列,再出队列,同时检查根结点有无左右孩子结点 ,若有则入队列。如上图,根结点有左右孩子结点,均入队列(2 3)。
-
之后左孩子结点(2)出队列,出队列之后检查根结点的左孩子结点是否有左右孩子结点 ,若有则也入队列(4 5),如上图,故有左右孩子结点,并入队列。
-
之后根结点的右孩子节点(3)出队列,同时执行上述逻辑,继续检查是否有左右孩子节点,但是上图中只有左孩子结点(6),故左孩子节点再入队列。
-
······
-
不断循环,直到队列为空,此时取出的数据,组成的就是层序遍历
-
**总结:**本质就是从根结点开始,每出一个结点就将其存在的左右孩子结点加入队列,并不断循环(借助队列先进先出的性质,上一层数据出队列的时候带入下一层的数据 ),即可实现层序遍历。
-
-
补充:由于每一个进入队列的数据都是一个结点,然后结点的数据结构类型是结构体类型
struct BinaryTreeNode*
,所以需要再队列中的typedef int QDataType;
改为typedef struct BinaryTreeNode* QDataType;
,这里就体现了前期使用重定义的方式定义队列,增加了后期代码的可维护性。
5. 判断二叉树是否为完全二叉树
5.1 代码示例
c
//判断二叉树是否为完全二叉树
//---队列
bool BinaryTreeComplete(BTNode* root)
{
//创建队列,并初始化
Queue q;
QueueInit(&q);
//将根节点插入队列
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
//取队头,并出队列
BTNode* front = QueueFront(&q);
QueuePop(&q);
// 检测到NULL则跳出循环,停止出队列
if (front == NULL)
{
break;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
//检查队列是否为空
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//队列中还有数据,说明不是完全二叉树
if (front != NULL)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
//是完全二叉树
return true;
}
5.2 逻辑详解
这里的基本逻辑和上面的层序遍历 基本一样,均是借用队列 这一数据结构,通过先进先出,一层一层检查结点,看结点是不是按照完全二叉树的顺序。
- 首先把根结点入队列,然后开始从队头出数据
- 出队头数据,若其有左右孩子结点,也将其依次如队列
- ······
- 不断循环,直到取队头数据为NULL时,停止入队列
- 此时只需要检查队列中的剩余数据,若均为NULL则是完全二叉树 ,如果还存在非空数据,则不是完全二叉树。

6. 二叉树的插入和删除
由于二叉树的定义比较广泛,其结构并不固定,比较多样化,所以模拟实现普通二叉树的增加和删除是没有意义的。
文章中部分图片来源于博主:2021dragon和一位前辈yuxi(抱歉个人主页已经找不到了,有好心人可以在评论区提醒一下)
最后谢谢大家看到这里,感谢!!