文章目录
- 关于二叉树的创建
- 如何创建二叉树
- 实现二叉树的前、中、后序遍历
- 层序遍历
关于二叉树的创建
在笔者的上一篇文章中堆进行了一个详细介绍,而二叉树是以堆为基础进行创建,它与堆的显著不同是
堆像是一个线性结构,堆的结构往往是一个数组,通过对父子索引的查找进行大多数功能的实现
而二叉树是一个逻辑结构,通过结构体实现二叉树的每一个节点,然后再通过指针将各个节点给联系起来
这里放一下两者的结构体对比,更加明显些
- 堆
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
- 二叉树
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyNode(int x);
如何创建二叉树
如果需要创建一个二叉树,我们往往需要一个能够提供二叉树元素根前序逻辑的数组,比如这个
char a[17] = { ‘A’,‘B’,‘D’,‘#’,‘#’,‘E’,‘#’,‘H’,‘#’,‘#’,‘C’,‘F’,‘#’,‘#’,‘G’,‘#’,‘#’ };
这里补充一下前序、中序、后序的概念
- 前序遍历(Preorder Traversal 亦称先序遍历)–访问根结点的操作发生在遍历其左右子树之前。
- 中序遍历(Inorder Traversal)–访问根结点的操作发生在遍历其左右子树之中(间)
- 后序遍历(Postorder Traversal)–访问根结点的操作发生在遍历其左右子树之后。
比如说前序:即根-左孩子-右孩子的顺序呈现二叉树的逻辑
既然能理解前序的概念我们就可以发现如果暗战数组元素顺序,那么第一个进来的就是根,通过递归本函数,我们可以实现先将根创建完后再创建左子树,最后创建右子树
一旦遇到 # 我们就退出递归,回到上一级
还需要注意的是,我们用来创建二叉树的往往是一个堆逻辑的数组,所以为了获取下一个元素,我们需要一个能够在递归时确定当前元素下标的变量,因此我们可以这样子做
int b = 0;
int* pi = &b;
由此一来pi对应是b的指针,即使在递归途中,我们不用改变指针pi直接通过指针改变b的值,即可以实现定位元素下标了
实现代码如下
BTNode* BuyNode(int x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
return;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
//a为外界传进的数组
//n为最大长度
//pi为我们遍历数组的指针
//使用‘#’表示NULL
//(*pi)++ 意味着pi所指向的那个数加一,所以pi作为指针,它所指向的数的地址不会发生变化,但它所指向的那个数会加一
if (a[*pi] == '#' || *pi >= n)
{
printf("N ");
(*pi)++;
//因为是二叉树,所以遇到 '#' 意味着后面很有可能还有,所以pi所指向的那个数,即要查看现在查看的数组元素的下一个元素
return NULL;
}
//若不为#就要创建一个新的节点
BTNode* dst = BuyNode(a[(*pi)]);
printf("%c ", dst->data);
//递归数组的下一个元素
(*pi)++;
//赋值左右节点元素给当前节点
dst->left = BinaryTreeCreate(a, n, pi);
dst->right = BinaryTreeCreate(a, n, pi);
return dst;
}
另外加一嘴,因为我们创建的二叉树是一个一个节点创建的,所以我们为了避免内存泄漏,最后也是需要通过递归一个一个释放,这里我们可以通过函数递归一直找到叶子节点,往上一个一个释放,即
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
//利用二叉树节点的特点,递归到最底层的结点,并释放
//再一层层返回调用,自下而上逐渐销毁
if (*root == NULL)
{
return;
}
BinaryTreeDestory(&((*root)->left));
BinaryTreeDestory(&((*root)->right));
free(*root);
root = NULL;
return;
}
实现二叉树的前、中、后序遍历
刚刚我们提到了前、中、后序的概念,所以当我们需要通过这三种形式提取二叉树中的元素,通过递归左右节点来获取根节点,就可以通过改变三者的输出顺序即可实现,还是比较简单的
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%c ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeInOrder(root->left);
printf("%c ", root->data);
BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->data);
}
层序遍历
层序遍历就与之前的遍历不同了,因为父子节点中往往可以通过指针直接获取对应的额位置和值,但是同一层中的节点却无法通过这种方法实现。
因此,我们需要用到之前学的一个数组结构 - 队列,即先进先出的数据结构
通过这种数据结构,我们将每次提取出来的节点放到队列的末尾,这样最后输出的队列,从头往后就是二叉树的层序遍历。
需要注意的是,如果大家在看别的博客的时候可能会遇到,他们直接使用队列的尾插功能,但其实这病不行,因为队列我们在创建时它的尾插功能的对象往往是队列的结构体,如果直接将其用来放入二叉树的层序遍历功能中,会出现bug
因此我们在二叉树中新建一个队列尾插功能,并将其的形参设为二叉树的结构体
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
if (root == NULL) {
return;
}
// 使用队列实现层序遍历
int front = 0, rear = 0;
BTNode** queue = (BTNode**)malloc(sizeof(BTNode*) * 1000); // 假设节点数不超过1000
queue[rear++] = root;
while (front < rear) {
BTNode* current = queue[front++]; // 取出队列前端节点
printf("%c ", current->data);
if (current->left != NULL) {
queue[rear++] = current->left; // 左子节点入队
}
if (current->right != NULL) {
queue[rear++] = current->right; // 右子节点入队
}
}
free(queue); // 释放队列内存
}
不仅如此,最后为了防止内存泄漏,我们需要把这个新建立的队列给释放,注意不能全部释放二叉树,要不然后面就没得用了