C语言实现二叉树的链式结构
一、二叉树的链式存储
1.1 二叉树存储结构实现
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data; // 当前节点值域
struct BinaryTreeNode* _left; // 指向当前节点左孩子
struct BinaryTreeNode* _right; // 指向当前节点右孩子
}BTNode;
1.2 创建二叉树结点
我们使用一个函数来每次创建一个二叉树的结点,并返回结点的指针
//创建二叉树的节点
BTNode* CreateNode(int x)
{
BTNode* node = (BTNode *)malloc(sizeof(BTNode));
node->_data = x;
node->_left = NULL;
node->_right = NULL;
return node;
}
1.3 构建二叉树
如何构建上面这颗二叉树呢?我们只需要创建出每一个结点,然后根据上图所示来构建二叉树就可以了
#include"BinaryTree.h"
int main()
{
BTNode* A = CreateNode('A');
BTNode* B = CreateNode('B');
BTNode* C = CreateNode('C');
BTNode* D = CreateNode('D');
BTNode* E = CreateNode('E');
A->_left = B;
A->_right = C;
B->_left = D;
B->_right = E;
InOrder(A);
system("pause");
return 0;
}
二、二叉树链式结构的遍历
所谓遍历(Traversal)是指:
沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。
访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。
2.1 前序/中序/后序的递归结构遍历
前序/中序/后序的递归结构遍历:根据访问结点操作发生位置命名
- NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- LNR:中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- LRN:后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
2.1.1 前序遍历
二叉树前序遍历的访问顺序为:
- 访问当前节点
- 遍历当前节点的左子树
- 左子树遍历完毕后,遍历当前节点的右子树
如果我们直接写出遍历过程和结果,难以让人理解,因此我们先写出递归代码再看过程(代码其实很简单)
假设我们的前序遍历函数已经写完,并且定义为:
void PrevOrder(BTNode* root) //二叉树的前序遍历
我们参照下图来表示一颗二叉树
- root :表示二叉树的根节点(A为二叉树的根节点)
- root->_left: 表示二叉树的左子树(B为根节点A的左子树)
- root->_right:表示二叉树的右子树(C为根节点A的右子树)
根据前序遍历的顺序,我们可以写出代码:
- 访问当前节点;就是打印当前节点,代码很简单直接打印就可:
printf("%c ", root->_data); //打印当前节点
- 遍历当前节点的左子树;刚才已经写出前序遍历的函数,现在直接把左子树传递给函数即可:
PrevOrder(root->_left); //遍历当前节点左子树
- 左子树遍历完比后,遍历当前节点的右子树;同理,把右子树传递给前序遍历函数即可:
PrevOrder(root->_right); //遍历当前节点右子树
这样,二叉树的前序遍历就写完了,但是下面这个代码会发现一个问题:递归的出口在哪里?
// 二叉树前序遍历
void PrevOrder(BTNode* root)
{
//递归的结束条件是什么呢?
printf("%c ", root->_data); //打印当前节点
PrevOrder(root->_left); //遍历当前节点左子树
PrevOrder(root->_right); //遍历当前节点右子树
}
我们知道无论哪种遍历,只要当前节点为NULL时,我们就结束遍历,那么递归的结束条件(出口):
if (root == NULL) //当前节点为空时打印NULL并返回
{
printf("NULL ");
return;
}
如果上述条件你不理解,我们可以把这句代码看成该函数的一种特殊情况:即当前二叉树为空树时直接返回。
// 二叉树前序遍历
void PrevOrder(BTNode* root)
{
if (root == NULL) //当前节点为空时打印NULL并返回
{
printf("NULL ");
return;
}
printf("%c ", root->_data); //打印当前节点
PrevOrder(root->_left); //遍历当前节点左子树
PrevOrder(root->_right); //遍历当前节点右子树
}
上述代码就是二叉树的前序递归遍历,我们参照这个代码来画一下前序遍历的过程和最终结果
- 访问节点A
- 访问A的左子树B
- 访问B的左子树D
- 访问D的左子树NULL返回栈帧D
- 访问D的右子树NULL返回栈帧D
- D左右子树遍历完返回栈帧B ,访问B的右子树E
- 访问E的左子树NULL返回栈帧E
- 返回E的右子树NULL返回栈帧E
- E左右子树遍历完,返回到栈帧B,B左右子树也遍历完返回栈帧A,访问A的右子树C
- 访问C的左子树NULL返回栈帧C
- 访问C的右子树NULL也返回栈帧C
- C左右子树遍历完返回栈帧A,A左右子树也遍历完,二叉树前序遍历递归结束
二叉树的前序遍历结果为:
A B D NULL NULL E NULL NULL C NULL NULL
我们来实现以下这个代码是不是和我们分析的结果一样
#include"BinaryTree.h"
int main()
{
BTNode* A = CreateNode('A');
BTNode* B = CreateNode('B');
BTNode* C = CreateNode('C');
BTNode* D = CreateNode('D');
BTNode* E = CreateNode('E');
A->_left = B;
A->_right = C;
B->_left = D;
B->_right = E;
PrevOrder(A); //前序遍历
system("pause");
return 0;
}
结果
2.1.2 中序遍历
二叉树中序遍历的访问顺序为:
- 遍历当前节点的左子树
- 左子树遍历完毕后,访问根节点
- 遍历当前节点的右子树
中序遍历的结果为:NULL D NULL B NULL E NULL A NULL C NULL
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->_left); //遍历左子树
printf("%c ", root->_data); //打印当前节点
InOrder(root->_right); //遍历右子树
}
测试代码同上,只需改变函数就可以,结果
2.1.3 后序遍历
二叉树后序遍历的访问顺序为:
- 遍历当前节点的左子树
- 左子树遍历完毕后,遍历当前节点的右子树
- 最后访问根节点
结果为
NULL NULL D NULL NULL E B NULL NULL C A
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if(root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->_left);
PostOrder(root->_right);
printf("%c ", root->_data);
}
结果
2.2 二叉树的层序遍历
2.2.1 层序遍历基本思想
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
层序遍历过程很好理解,其访问顺序为A、B、C、D、E
2.2.2 层序遍历代码实现
层序遍历的访问顺序和可以使用我们之前学过的队列来实现,队列的实现原理参看这篇博客
数据结构——队列(C语言)
我们来画图理解层序遍历是怎样使用队列实现的
- 节点A入队
- 节点A出队,左节点B入队。右结点C入队
- 节点B出队,左节点D入队。右结点E入队
- 节点C出队,左右节点为空,不执行入队操作
- 节点D出队,左右节点为空,不执行入队操作
- 节点E出队,左右节点为空,不执行入队操作
注意:队列中data的数据类型不再是int或者char等类型,而是root指针
typedef BTNode* QDataType;
// 层序遍历
void LevelOrder(BTNode* root)
{
Queue q; //创建队列
QueueInit(&q); //初始化队列
QueuePush(&q, root); //根节点入队
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q); //获取队头结点
QueuePop(&q); //队头出队
printf("%c ", front->_data);
//节点front的左节点和右节点依次入队
if (front->_left)
{
QueuePush(&q, front->_left); //左节点入队
}
if (front->_right)
{
QueuePush(&q, front->_right); //右节点入队
}
}
printf("\n");
}
2.3 完全二叉树的判断
判断二叉树是否是完全二叉树,在掌握了层序遍历的实现过程后,如果我们每次都让叶子节点左右空子树也入对的话,可以发现一个规律
可以看到完全二叉树的最后一个叶子节点出队后,队列中全部为NULL元素
非完全二叉树的最后一个叶子节点前会出现NULL元素,利用这个性质,我们就可以判断一棵树是否是完全二叉树
// 判断二叉树是否是完全二叉树
//是返回1,不是返回0
int TreeComplete(BTNode* root)
{
Queue q;
if (root == NULL)
return 1;
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);
}
//将队列中所有元素出队,此时完全二叉树队列都为NULL,非完全二叉树队列中存在其他数值元素
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//队列中只要有不为NULL的元素说明不是完全二叉树
if (front)
{
QueueDestroy(&q);
return 0;
}
}
QueueDestroy(&q);
return 1;
}
三、二叉树的节点个数
3.1 二叉树节点个数
//二叉树结点个数
int TreeSize(BTNode* root)
{
if (root == NULL)
return 0;
return 1 + TreeSize(root->_left) + TreeSize(root->_right);
}
3.2 二叉树叶子节点个数
// 二叉树叶子节点个数
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//只有叶子节点的左右子树为空
if (root->_left == NULL && root->_right == NULL)
{
return 1;
}
return TreeLeafSize(root->_left) + TreeLeafSize(root->_right);
}
3.3 二叉树第k层节点个数
可以用分治算法:大问题分解为小问题,小问题再进一步分解,直到不可分解的子问题。
第k层节点的个数可以转为第k层节点左右子树的k-1层节点个数
第k-1层节点的个数又可以转为第k-1层节点左右子树的k-2层节点个数
……
当k=1时,表示根节点,问题不再分解,直接返回1即可。
// 二叉树第k层节点个数
//第k层叶子节点的个数就是当前层左右子树的第k-1层节点个数,
//当k==1时就不再分解
int TreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return TreeLevelKSize(root->_left,k-1)+ TreeLevelKSize(root->_right, k - 1);
}
3.4 二叉树查找值为x的节点
// 二叉树查找值为x的节点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
//当前节点值等于x返回当前节点值
if (root->_data == x)
return root;
//未找到就遍历左子树,找到了则返回
BTNode* findNode = TreeFind(root->_left, x);
if (findNode)
return findNode;
//左子树未找到,遍历右子树
findNode = TreeFind(root->_right, x);
if (findNode)
return findNode;
return NULL;
}
四、代码清单
GitHub下载链接
https://github.com/Kyrie-leon/Data_Structures/tree/main/BinaryTree