📖 前言:让节点不再孤单,链条连接一切!在这里,数据结构玩起了“链式反应”!本期我们将深入探讨其结构特点及实现。
🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:
目录📌
🕒 1. 二叉树的链式存储结构
二叉树的链式结构可以存储任意树(包括:普通二叉树,满二叉树,完全二叉树等),而二叉树的顺序结构(堆)存储普通二叉树会造成空间浪费。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
二叉树可分两种:
- 空树
- 非空树:根节点、根节点的左子树、根节点的右子树组成
以上图为例,将其用代码实现:
// 二叉树的创建
BTNode* CreateTree()
{
BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
assert(n1);
BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
assert(n2);
BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
assert(n3);
BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
assert(n4);
BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
assert(n5);
BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
assert(n6);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n5->data = 5;
n6->data = 6;
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = NULL;
n3->left = NULL;
n3->right = NULL;
n4->left = n5;
n4->right = n6;
n5->left = NULL;
n5->right = NULL;
n6->left = NULL;
n6->right = NULL;
return n1;
}
综上:二叉树定义是递归式的,我们可以通过递归的方式去遍历整个二叉树。
🕒 2. 二叉树的遍历
🕘 2.1 前序遍历
前序遍历(Preorder Traversal,亦称先序遍历)——访问根节点的操作发生在遍历其左右子树之前。
访问顺序为:根 → 左子树 → 右子树
⚡ 注意:上述所说的根节点不仅仅指的是整棵树的根节点,也指每一个节点(因为每一个节点都可以当作一个根节点来看待)。即一棵完整的树,被拆分成多个子树看待。
ps:对函数递归展开不熟悉的同学可以像上图这样多画画递归展开图哦~
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
学会前序遍历后,我们可以通过给定一个字符串进行创建二叉树,该字符串中含有需要我们构建的二叉树的所有节点。
注意:字符串中的 # 表示空树,即上一个节点没有左子树或右子树。
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* CreateTree(BTDataType* a, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
// 创建根节点
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc fail");
exit(-1);
}
root->data = a[*pi];
(*pi)++;
// 创建左右子树
root->left = CreateTree(a, pi);
root->right = CreateTree(a, pi);
return root;
}
🕘 2.2 中序遍历
中序遍历(Inorder Traversal)——访问根节点的操作发生在遍历其左右子树之中(间)。
访问顺序为:左子树 → 根 → 右子树
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
🕘 2.3 后序遍历
后序遍历(Postorder Traversal)——访问根节点的操作发生在遍历其左右子树之后。
访问顺序为:左子树 → 右子树 → 根
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
🕘 2.4 层序遍历
区别其他三种遍历方式,层序遍历采用的是非递归。
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的节点的过程就是层序遍历。
访问顺序为:一层一层,从上到下,每层从左到右
💡 具体思路:可以使用一个队列来存储二叉树节点的地址。首先,将根节点加入队列,然后将根节点从队列中移除,同时将根节点的左右子节点(如果有的话)加入队列。这样操作可以确保当一层的所有节点都被访问后,它们的子节点会在队列中准备好被访问。当队列为空时,表示二叉树的所有节点都已经被访问过了。
// 二叉树层序遍历
void LevelOrder(BTNode* root)
{
if (root == NULL)
return;
// 将节点的地址存储在队列中,取出一个节点的同时将该节点的子节点入栈
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
// 取出队顶的元素
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->data);
// 将队顶元素的左右子节点入队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
QueueDestory(&q);
}
注意:因为需要使用队列来存储二叉树节点的地址,所以必须将之前编写的队列文件Queue.h和Queue.c加入到当前项目中;还需要在队列结构体定义之前定义二叉树节点的结构体。
🕒 3. 二叉树节点个数
💡 思路1:静态变量法
int TreeSize(BTNode* root)
{
static int count = 0; //局部变量记得加static
if (root == NULL)
return count;
count++; //前序
TreeSize(root->left);
TreeSize(root->right);
return count;
}
⚡ 注意:不可取,因为如果再次调用就会出现问题
💡 思路2:遍历计数法
// 遍历计数
int count = 0; //全局变量
void TreeSize2(BTNode* root)
{
if (root == NULL)
return;
count++;
TreeSize2(root->left);
TreeSize2(root->right);
return;
}
⚡ 注意:每次调用前都要置为0,比较麻烦
💡 思路3:划分子问题
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left) + TreeSize(root->right) + 1;//左+右+自己
}
🕒 4. 二叉树叶子节点个数
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);
}
🕒 5. 二叉树高度/深度
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int lh = TreeHeight(root->left);
int rh = TreeHeight(root->right);
return lh > rh ? lh + 1 : rh + 1;
}
🕒 6. 二叉树第K层节点个数
// 第K层节点个数
int TreeKLevelSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
// 转换成求子树第k-1层
return TreeKLevelSize(root->left, k - 1)
+ TreeKLevelSize(root->right, k - 1);
}
🕒 7. 二叉树查找值为x的节点
思考一下,下面的写法对吗❓
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TreeFind(root->left, x);
TreeFind(root->right, x);
}
这种是不对的,详见图解:
正确方法:
// 返回x所在的节点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
// 先去左树找
BTNode* lret = TreeFind(root->left, x);
if (lret)
return lret;
// 左树没有找到,再去右树找
BTNode* rret = TreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}
🕒 8. 修改二叉树节点
💡 思路:配合查找函数使用
BTNode* ret = TreeFind(root, 5);
ret->data *= 10;
🕒 9. 判断二叉树是否是完全二叉树
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q); // 初始化队列
// 如果根节点不为空,将其入队
if (root)
QueuePush(&q, root);
// 使用层序遍历二叉树
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 != NULL)
{
QueueDestory(&q); // 销毁队列
return false; // 返回 false 表示不是完全二叉树
}
}
QueueDestory(&q); // 销毁队列
return true; // 返回 true 表示是完全二叉树
}
🕒 10. 销毁二叉树
💡 思路:找到左右子树之后再销毁本身,否则找不到子树。
// 销毁二叉树
void TreeDestroy(BTNode* root)
{
if (root == NULL)
{
return;
}
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
}
OK,以上就是本期知识点“链式二叉树”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~
❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页