文章目录
- 🎯引言
- 👓链式二叉树的实现
- 1.链式二叉树的结构
- 2.链式二叉树相关操作实现
- 2.1源码展示
- 2.2函数实现详解
- 2.2.1前中后序遍历
- 2.2.2二叉树的其他方法实现
- 2.2.3二叉树的层序遍历和判断是否是完全二叉树
- 🥇结语
🎯引言
欢迎来到HanLop博客的C语言数据结构初阶系列。在这个系列中,我们将深入探讨各种基本的数据结构和算法,帮助您打下坚实的编程基础。在本篇文章中,我们将探讨另一种重要的二叉树数据结构——链式二叉树(Linked Binary Tree)。不同于顺序结构的二叉树,链式二叉树使用链表节点来存储树中的元素,使其在内存分配和动态调整上具有更大的灵活性。链式二叉树在许多编程问题中都有广泛应用,特别是在递归算法和树遍历等方面。本篇文章将介绍链式二叉树的基本概念、创建方法、基本操作以及其在实际编程中的应用。通过代码示例,您将学会如何在C语言中实现链式二叉树,并理解其优缺点,为进一步学习更复杂的数据结构做好准备。
👓链式二叉树的实现
链式二叉树(Linked Binary Tree)是一种数据结构,它使用链表节点来表示树中的每个元素。每个节点包含一个数据域和两个指针域,分别指向该节点的左子节点和右子节点。这种结构与顺序结构的二叉树不同,它不需要连续的内存空间,因此在动态变化的数据量下具有更好的灵活性和效率。
1.链式二叉树的结构
每个节点通常包括以下部分:
- 数据域(Data):存储节点的值或数据。
- 左指针域(Left Child):指向左子节点的指针。
- 右指针域(Right Child):指向右子节点的指针。
typedef int BTNDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTNDataType x;
}BTNode;
在上面的C语言结构体定义中,TreeNode
是一个二叉树节点,包含一个整数数据域x
,以及两个指针left
和right
,分别指向左子节点和右子节点
2.链式二叉树相关操作实现
2.1源码展示
BinaryTree.h源码
//BinaryTree.h文件中
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
//包含队列的头文件为了实现层序遍历,
//队列需要自己去实现
//队列的实现在之前文章中有讲过
#include "Queue.h"
typedef int BTNDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTNDataType x;
}BTNode;
//前序遍历
void PerOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//前序遍历
void PostOrder(BTNode* root);
//二叉树的节点个数
int BinaryTreeSize(BTNode* root);
//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
// ⼆叉树第k层结点个数
int BinaryTreeLeavelKSize(BTNode* root, int k);
//二叉树的深度
int BinaryTreeDepth(BTNode* root);
//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTNDataType x);
// ⼆叉树销毁
void BinaryTreeDestory(BTNode** root);
//二叉树的层序遍历
void LevelOrder(BTNode* root);
//判断二叉树是否为完全二叉树
bool BinaryTreeComplete(BTNode* root);
BinaryTree.c源码
//BinaryTree.c文件中
#include "BinaryTree.h"
//前序遍历 根左右
void PerOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->x);
PerOrder(root->left);
PerOrder(root->right);
}
//中序遍历 左根右
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d ", root->x);
InOrder(root->right);
}
//前序遍历 左右根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->x);
}
//二叉树的节点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
//二叉树叶子结点个数
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);
}
// ⼆叉树第k层结点个数
int BinaryTreeLeavelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLeavelKSize(root->left, k - 1) + BinaryTreeLeavelKSize(root->right, k - 1);
}
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//if (root->left == NULL && root->right == NULL)
//{
// return 1;
//}
int numLeft = BinaryTreeDepth(root->left);
int numRight = BinaryTreeDepth(root->right);
return 1 + (numLeft > numRight ? numLeft : numRight);
}
//找到结点为x的值
BTNode* BinaryTreeFind(BTNode* root, BTNDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->x == x)
{
return root;
}
BTNode* retLeft = BinaryTreeFind(root->left, x);
if (retLeft)
return retLeft;
BTNode* retRight = BinaryTreeFind(root->right, x);
if (retRight)
return retRight;
return NULL;
}
//二叉树的销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
{
return;
}
BTNode* leftTree = (*root)->left;
BTNode* rightTree = (*root)->right;
free(*root);
*root = NULL;
BinaryTreeDestory(&leftTree);
BinaryTreeDestory(&rightTree);
}
void LevelOrder(BTNode* root)
{
Queue pq;
QueueInit(&pq);
//将根结点放入队列中
QueuePush(&pq, root);
while (!QueueEmpty(&pq))
{
if (QueueFront(&pq) != NULL)
printf("%d ", QueueFront(&pq)->x);
else
printf("NULL ");
if (QueueFront(&pq) != NULL)
{
QueuePush(&pq, QueueFront(&pq)->left);
QueuePush(&pq, QueueFront(&pq)->right);
}
QueuePop(&pq);
}
QueueDestory(&pq);
}
bool BinaryTreeComplete(BTNode* root)
{
Queue pq;
QueueInit(&pq);
QueuePush(&pq, root);
while (QueueFront(&pq)!=NULL)
{
QueuePush(&pq, QueueFront(&pq)->left);
QueuePush(&pq, QueueFront(&pq)->right);
QueuePop(&pq);
}
while (!QueueEmpty(&pq))
{
if (QueueFront(&pq) != NULL)
{
QueueDestory(&pq);
return false;
}
QueuePop(&pq);
}
QueueDestory(&pq);
return true;
}
2.2函数实现详解
2.2.1前中后序遍历
我们下面讲解前中后序遍历都通过下面该树来讲解
(只详细讲解前序遍历,其他遍历类似,只讲解代码)
1. 前序遍历 (Pre-order Traversal)
定义: 前序遍历按照 “根 -> 左 -> 右” 的顺序访问节点。
访问顺序:
- 访问根节点。
- 前序遍历左子树。
- 前序遍历右子树。
特点: 前序遍历首先访问当前节点,然后依次访问左子树和右子树。这种遍历方式可以用于表达式树的拷贝或克隆,因为它首先处理的是每个节点的值
动图讲解:
前序遍历 (Pre-order Traversal)
void PerOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->x);
PerOrder(root->left);
PerOrder(root->right);
}
详细解析:
- 功能: 前序遍历按照"根-左-右"的顺序访问节点。
- 过程:
- 根节点: 首先访问当前节点,如果节点不为空,则打印节点的值
root->x
。 - 左子树: 递归调用
PerOrder(root->left)
访问左子树。 - 右子树: 递归调用
PerOrder(root->right)
访问右子树。
- 根节点: 首先访问当前节点,如果节点不为空,则打印节点的值
递归思路: 递归地访问左子树和右子树,直到访问到叶子节点为止。每次递归调用时,将当前节点的子节点作为新的根节点。
中序遍历 (In-order Traversal)
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d ", root->x);
InOrder(root->right);
}
详细解析:
- 功能: 中序遍历按照"左-根-右"的顺序访问节点。
- 过程:
- 左子树: 递归调用
InOrder(root->left)
访问左子树。 - 根节点: 访问当前节点,打印节点的值
root->x
。 - 右子树: 递归调用
InOrder(root->right)
访问右子树。
- 左子树: 递归调用
用途: 在二叉搜索树中,中序遍历会按升序输出节点的值。
后序遍历 (Post-order Traversal)
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->x);
}
详细解析:
- 功能: 后序遍历按照"左-右-根"的顺序访问节点。
- 过程:
- 左子树: 递归调用
PostOrder(root->left)
访问左子树。 - 右子树: 递归调用
PostOrder(root->right)
访问右子树。 - 根节点: 访问当前节点,打印节点的值
root->x
。
- 左子树: 递归调用
用途: 通常用于删除或释放树中的节点,因为在访问根节点之前,确保了其子节点已经被处理。
2.2.2二叉树的其他方法实现
二叉树的节点个数 (BinaryTreeSize)
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
详细解析:
- 功能: 计算二叉树的总节点数。
- 过程:
- 空节点: 如果当前节点为空,返回 0。
- 递归计数: 返回值为 1(当前节点)加上左子树和右子树的节点数总和。
递归思路: 每次调用都会返回当前节点加上左右子树的节点数,总体上遍历了整个树。
二叉树叶子结点个数 (BinaryTreeLeafSize)
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);
}
详细解析:
- 功能: 计算二叉树中叶子节点的数量。叶子节点是没有子节点的节点。
- 过程:
- 空节点: 如果当前节点为空,返回 0。
- 叶子节点: 如果节点的左右子树均为空,返回 1。
- 递归计数: 返回左子树和右子树的叶子节点数之和。
二叉树第 k 层结点个数 (BinaryTreeLeavelKSize)
int BinaryTreeLeavelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLeavelKSize(root->left, k - 1) + BinaryTreeLeavelKSize(root->right, k - 1);
}
详细解析:
- 功能: 计算二叉树第
k
层的节点数。 - 过程:
- 空节点: 如果当前节点为空,返回 0。
- 当前层: 如果
k
为 1,表示当前层即为第k
层,返回 1。 - 递归计数: 递归调用函数,计算左右子树中第
k-1
层的节点数之和。
递归思路: 每递归一次,层数减一,直到 k
为 1 时,表示到达目标层。
二叉树的深度 (BinaryTreeDepth)
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int numLeft = BinaryTreeDepth(root->left);
int numRight = BinaryTreeDepth(root->right);
return 1 + (numLeft > numRight ? numLeft : numRight);
}
详细解析:
- 功能: 计算二叉树的深度(从根节点到最远叶子节点的最长路径)。
- 过程:
- 空节点: 如果当前节点为空,返回 0。
- 递归计算: 递归地计算左子树和右子树的深度,取其中较大值加 1(当前节点深度)。
递归思路: 计算每个节点的深度时,考虑左右子树的最大深度,加上当前节点所在的层数。
查找值为 x 的节点 (BinaryTreeFind)
BTNode* BinaryTreeFind(BTNode* root, BTNDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->x == x)
{
return root;
}
BTNode* retLeft = BinaryTreeFind(root->left, x);
if (retLeft)
return retLeft;
BTNode* retRight = BinaryTreeFind(root->right, x);
if (retRight)
return retRight;
return NULL;
}
详细解析:
- 功能: 查找二叉树中值为
x
的节点。 - 过程:
- 空节点: 如果当前节点为空,返回
NULL
。 - 匹配节点: 如果当前节点的值为
x
,返回当前节点。 - 递归查找: 先在左子树中查找,如果找到,返回该节点;否则在右子树中查找。
- 空节点: 如果当前节点为空,返回
递归思路: 通过递归遍历树的每一个节点,查找指定值的节点。
二叉树的销毁 (BinaryTreeDestory)
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
{
return;
}
BTNode* leftTree = (*root)->left;
BTNode* rightTree = (*root)->right;
free(*root);
*root = NULL;
BinaryTreeDestory(&leftTree);
BinaryTreeDestory(&rightTree);
}
详细解析:
- 功能: 销毁二叉树,释放所有节点的内存。
- 过程:
- 空节点: 如果当前节点为空,返回。
- 递归销毁: 递归地销毁左子树和右子树,然后释放当前节点的内存。
递归思路: 先销毁子节点,再销毁当前节点,确保不会丢失任何内存。
2.2.3二叉树的层序遍历和判断是否是完全二叉树
实现这两个方法,需要借助到前面我们所实现的队列的数据结构
Queue.h中需要更改的地方:
typedef struct BinaryTreeNode* QueueDataType;
//我们所需要存储的数据类型是二叉树的结点 所以将int更改为struct BinaryTreeNode*
//注:struct BinaryTreeNode* 中的struct的关键字一定要带着
typedef struct QueueNode
{
QueueDataType x;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
int size;
}Queue;
上面就是Queue.h
所需更改的地方,其他的代码直接拷贝复制即可使用
层序遍历 (LevelOrder)
void LevelOrder(BTNode* root)
{
Queue pq;
QueueInit(&pq);
QueuePush(&pq, root);
while (!QueueEmpty(&pq))
{
if (QueueFront(&pq) != NULL)
printf("%d ", QueueFront(&pq)->x);
else
printf("NULL ");
if (QueueFront(&pq) != NULL)
{
QueuePush(&pq, QueueFront(&pq)->left);
QueuePush(&pq, QueueFront(&pq)->right);
}
QueuePop(&pq);
}
QueueDestory(&pq);
}
详细解析:
- 功能: 按层次从上到下、从左到右遍历二叉树。
- 过程:
- 队列初始化: 使用队列来保存每层的节点。将根节点入队。
- 遍历: 当队列不为空时,取出队首节点并访问(打印)它的值。若该节点不为空,则将其左右子节点依次入队。
- 销毁队列: 最后销毁队列,释放内存。
队列的应用: 利用队列的 FIFO 特性实现层序遍历。
动图图解
判断是否为完全二叉树 (BinaryTreeComplete)
bool BinaryTreeComplete(BTNode* root)
{
Queue pq;
QueueInit(&pq);
QueuePush(&pq, root);
while (QueueFront(&pq) != NULL)
{
QueuePush(&pq, QueueFront(&pq)->left);
QueuePush(&pq, QueueFront(&pq)->right);
QueuePop(&pq);
}
while (!QueueEmpty(&pq))
{
if (QueueFront(&pq) != NULL)
{
QueueDestory(&pq);
return false;
}
QueuePop(&pq);
}
QueueDestory(&pq);
return true;
}
详细解析:
- 功能: 判断一个二叉树是否为完全二叉树。
- 完全二叉树定义: 除了最后一层外,每层的节点都是满的,并且最后一层的节点从左到右是连续的。
- 过程:
- 队列初始化: 将根节点入队。
- 遍历队列: 对于每个节点,如果队列中的节点为非空,则将其左右子节点入队。直到遇到第一个空节点为止。
- 验证完全性: 遇到第一个空节点后,检查队列中是否还有非空节点。如果有,则该树不是完全二叉树;如果没有,则是完全二叉树。
关键点: 空节点后不应再出现非空节点,否则树不符合完全二叉树的定义。
🥇结语
在本篇文章中,我们深入探讨了链式二叉树这一重要的数据结构。我们介绍了它的基本概念和结构,讲解了如何创建和操作链式二叉树,并通过实际代码示例展示了其在C语言中的实现。链式二叉树因其灵活性和高效性,在处理许多实际问题时具有重要意义。希望通过本文的学习,您能够更好地理解链式二叉树的工作原理,并能在实际编程中有效地应用这一数据结构。随着对链式二叉树的掌握,您将在数据结构和算法的学习道路上迈出重要一步,为后续的更复杂的树结构和高级算法打下坚实的基础。感谢您阅读本篇文章,我们期待在下一篇文章中继续与您探讨更多有趣的编程知识。