零、读前说明
- 本文中所有设计的代码均通过测试,并且在功能性方面均实现应有的功能。
- 设计的代码并非全部公开,部分无关紧要代码并没有贴出来。
- 如果你也对此感兴趣、也想测试源码的话,可以私聊我,非常欢迎一起探讨学习。
- 由于时间、水平、精力有限,文中难免会出现不准确、甚至错误的地方,也很欢迎大佬看见的话批评指正。
- 嘻嘻。。。。 。。。。。。。。收!
一、概述
从前面的几个基础知识中已经知道,树是一个递归的模型,从树的定义到遍历,包括创建都将是在递归的基础上进行的。
在前面简单说明了二叉树的遍历的相关知识,在二叉树的世界中,二叉树的遍历是树的各种操作的基础,我们可以基于二叉树的遍历过程来做很多的事情。
那么接下来所有关于二叉树的数据结构定义,均采用的是二叉链表的形式来定义。所以,二叉树中的数据结构定义如下:
/* 定义二叉树的节点类型 */
typedef int TElemType;
/* 二叉链表表示法的结构模型 */
typedef struct BiTNode
{
TElemType data; /* 节点数据 */
struct BiTNode *lchild; /* 左孩子 */
struct BiTNode *rchild; /* 右孩子 */
} BiTNode;
在本文中,关于二叉树的所有的操作函数,均采用下面这种定义。所以,二叉树操作函数定义如下:
/* 接口申明 */
typedef struct func_BiTree
{
BiTNode *(*create)(unsigned char *, unsigned char *, int);
void (*prevOrder)(BiTNode *);
void (*inOrder)(BiTNode *);
void (*postOrder)(BiTNode *);
void (*levelOrder)(BiTNode *);
void (*release)(BiTNode *);
} func_BiTree;
首先,最主要的是关于二叉树的创建、遍历、释放等等,那么本文章就首先详细来说明一下关于二叉树的创建的内容。关于二叉树的释放,在文件最后一章将详细说明。
二、先序创建(#法)
为了建立一个二叉树,将二叉树中的每个节点的空指针(没有左孩子或者右孩子)引出一个 虚节点 ,将其值设定为一个特定的 “#” ,用来标识此节点为空,把这样处理后的二叉树称为原来二叉树的扩展二叉树。如下图所示。
在上图所示的树中,可以看见,E、K、I、J 节点为叶子节点,既不存在左孩子也不存在右孩子,所以将其左孩子和右孩子位置的节点全部设置为“#”,H 节点不存在左孩子,F 节点不存在右孩子,G 节点不存在左孩子,所以将不存在的节点同样也设置为“#”,这样就可以得出右边的扩展二叉树。
所以对于这个扩展二叉树的的先序遍历的结果为:
由上面的二叉树中,设定二叉树中为节点的内容为一个字符(A、B、C…),那么二叉树的创建的过程为:
1、首先输入根节点,如果输入为一个#字符,则表明该二叉树为空数,即 root 为 NULL;
2、否则输入的字符应该赋给 root->data,之后依次递归建立他的左子树和右子树;
3、并且在递归创建的过程中, 将返回值分别赋值于当前节点的左子树或者右子树。
那么,综合所述,其遍历的代码可以这样编了。
/**
* 功 能:
* 创建并且初始二叉树 - 按照先序遍历建立二叉树
* 参 数:
* 无
* 返回值:
* 成功:创建完成的树的根节点
* 失败:NULL
**/
BiTNode *BiTree_Create(void)
{
BiTNode *root = NULL;
char ch;
scanf("%c", &ch);
// 如果字符值不为 # ,则说明节点存在
if (ch != '#')
{
// 为节点申请空间
root = (BiTNode *)malloc(sizeof(BiTNode));
if (root == NULL)
return NULL;
memset(root, 0, sizeof(BiTNode));
// 将字符值赋值给节点
root->data = ch;
// 递归创建左孩子,并将返回值赋值给左孩子
root->lchild = BiTree_Create();
// 递归创建右孩子,并将返回值赋值给右孩子
root->rchild = BiTree_Create();
// 返回根节点
}
// else // 符值为 #, 节点存在为空,返回为空
// {
// root = NULL;
// }
return root;
}
上面已经完成二叉树的先序创建,下面简单的编写一个测试demo,测试一下创建过程,并使用四种遍历方式分别打印输出。
下面为 main.c 文件中的内容。
#include "../src/biTree/biTree.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern func_BiTree fBiTree;
//ABDH#K###E##CFI###G#J##
int main(int argc, const char *argv[])
{
int ret = 0;
BiTNode *tree = NULL;
// 创建一个二叉树
printf("请按照先序输入二叉树:");
tree = fBiTree.create();
if (tree != NULL)
{
printf("二叉树创建成功!\n");
}
else
{
printf("二叉树创建出现异常!\n");
ret = -1;
goto ERROR_END;
}
printf("先序遍历输出:");
fBiTree.prevOrder(tree);
putchar(10);
printf("中序遍历输出:");
fBiTree.inOrder(tree);
putchar(10);
printf("后序遍历输出:");
fBiTree.postOrder(tree);
putchar(10);
printf("层序遍历输出:");
fBiTree.levelOrder(tree);
putchar(10);
ERROR_END:
// 释放二叉树
fBiTree.release(tree);
printf("system exit!\n");
return ret;
}
下面为工程文件的结构,使用cmake进行工程管理与编译。
$ tree biTree-create-prev/
biTree-create-prev/
├── CMakeLists.txt
├── README.md
├── image
│ └── image.jpg
├── main
│ └── main.c
├── runtime
└── src
└── biTree
├── biTree.c
└── biTree.h
5 directories, 6 files
然后创建并编译工程,详细效果如下图所示。
至此,二叉树的先序创建已经全部测试完成!!!
二、先序中序创建
首先看一个示例,说已知一个二叉树的先序遍历顺序和中序遍历的顺序,那么能不能确定一个唯一的二叉树的形状?
那么下面就简单的进行分析看看。
已知:
先序遍历:ABDHKECFIGJ
中序遍历:HKDBEAIFCGJ
那么应该怎么来进分析并确定二叉树呢?
1、 由先序遍历可以得知,第一个节点 A 为根节点
再由中序遍历可得知,根节点 A 的左部分为左子树(紫色),右部分为右子树(绿色)
所以,此时可以得出这样的形状。
2、 已经确定左子树的内容,那么用同样的方法在确定左子树的根以及节点,为了方便查看,我们暂时去掉右子树部分的节点
同样由先序遍历可以得知, B 为左子树的根节点
再由中序遍历可得知,根节点 B 的左部分为左子树(紫色),右部分为右子树(绿色)
所以,此时可以得出这样的形状。
3、 和步骤2相同,由先序遍历可以得知, D 为左子树的根节点
再由中序遍历可得知,根节点 D 的左部分为左子树(紫色),不存在右子树
所以,此时可以得出这样的形状。
4、 和步骤2相同,由先序遍历可以得知, H 为左子树的根节点,此时K可能为左子树也有可能为右子树
但是由中序遍历可得知,根节点 H 的左部分为空,右部分为 K ,所以, K 为 H 的右子树
所以,此时可以得出这样的形状。
至此,左子树已经全部确定完成。下面开始右子树的判断。
5、 同样还是在重复进行判断
由先序遍历可以得知, C 为右子树的根节点
由中序遍历可得知,根据根节点 C 的位置确定其左部分为左子树,右部分为右子树
所以,此时可以得出这样的形状。
6、 由先序遍历可以得知, F 为右子树的根节点,但是不能确定 I 是左子树还是右子树
由中序遍历可得知,根据根节点 F 的位置可以确定 I 为左子树( I 在 F 的左边)
所以,此时可以得出这样的形状。
7、 用同样的方式
由先序遍历可以确定 G 为根节点,但是不能确定 J 是左子树还是右子树
由中序遍历可以确定,根据 J 在根节点 G 的右边框可以确定为其右子树
所以,此时可以得出这样的形状。
至此,树已经全部确定完成。
综上所述,先序遍历结果和中序遍历结果可以唯一确定一个二叉树。
从上面的分析并确定的过程中,最主要的 从先序遍历顺序确定根节点,然后在中序遍历顺序中找到根节点的位置,然后将结果分为左子树和右子树,之后开始递归确定根节点,递归确定左子树,递归确定右子树。 就是这么简单,那么,根据这个特例我们呢可以写出对应的代码。
/**
* 功 能:
* 创建并且初始二叉树 - 按照先序中序遍历建立二叉树
* 参 数:
* preStr: 先序遍历的顺序字符串
* inStr : 中序遍历的顺序字符串
* num : 树的节点的个数
* 返回值:
* 成功:创建完成的树的根节点
* 失败:NULL
**/
BiTNode *BiTree_Create(unsigned char *preStr, unsigned char *inStr, int num)
{
int pos = 0;
BiTNode *root = NULL;
// 减产参数的合法性
if (preStr == NULL || inStr == NULL || num < 0)
{
goto ERROR;
}
// 如果 num 大于 0,意味着还有节点没有创建
if (num > 0)
{
// 申请空间
root = (BiTNode *)malloc(sizeof(BiTNode));
// 将先序遍历中当前的字符赋值给当前节点
root->data = *preStr;
// 从中序遍历中查找当前节点的位置
while (pos < num)
{
if (inStr[pos] == *preStr)
break;
pos++;
}
/**
* 递归创建左孩子节点,先序遍历的字符串后移一个位置,
* 中序遍历根节点前面的为左子树,节点数目为pos一样
**/
root->lchild = PreInCreate(preStr + 1, inStr, pos);
/**
* 递归创建右孩子节点,从先序遍历的字符串后移pos + 1个位置,对应图中紫色部分
* 中序遍历字符串同理,节点数目为总数据减去pos在减去1个(0开始的)
**/
root->rchild = PreInCreate(preStr + pos + 1, inStr + pos + 1, num - pos - 1);
}
ERROR:
return root;
}
上面已经完成二叉树的先序和中序创建,下面简单的编写一个测试demo,测试一下创建过程,并使用四种遍历方式分别打印输出。
如果为了增加灵活性,可以将先序遍历和中序遍历的字符串通过终端输入
本测试中为了方便测试,直接采用写入的方法测试
如果您有兴趣,可以自己进行尝试
下面为 main.c 文件中的内容。
#include "../src/biTree/biTree.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern func_BiTree fBiTree;
int main(int argc, const char *argv[])
{
int ret = 0;
BiTNode *tree = NULL;
/**
* 创建一个二叉树
* 如果为了增加灵活性,可以将先序遍历和中序遍历的字符串通过终端输入
* 本测试中为了方便测试,直接采用写入的方法测试
* 如果您有兴趣,可以自己进行尝试
**/
tree = fBiTree.create("ABDHKECFIGJ", "HKDBEAIFCGJ", 11);
if (tree != NULL)
{
printf("二叉树创建成功!\n");
}
else
{
printf("二叉树创建出现异常!\n");
ret = -1;
goto ERROR_END;
}
printf("先序遍历输出:");
fBiTree.prevOrder(tree);
putchar(10);
printf("中序遍历输出:");
fBiTree.inOrder(tree);
putchar(10);
printf("后序遍历输出:");
fBiTree.postOrder(tree);
putchar(10);
printf("层序遍历输出:");
fBiTree.levelOrder(tree);
putchar(10);
ERROR_END:
// 释放二叉树
fBiTree.release(tree);
printf("system exit!\n");
return ret;
}
下面为工程文件的结构,使用cmake进行工程管理与编译。
$ tree biTree-create-prev-in/
biTree-create-prev-in/
├── CMakeLists.txt
├── README.md
├── image
│ └── image.jpg
├── main
│ └── main.c
├── runtime
└── src
└── biTree
├── biTree.c
└── biTree.h
5 directories, 6 files
然后创建并编译工程,详细效果如下图所示。
至此,二叉树的先序中序创建已经全部测试完成!!!
三、中序后序创建
中序后序创建二叉树与前面的先序中序创建的过程、分析方法是一样的,只不过对于根节点、左子树、右子树的分析的位置不同,下面用一个简单的例子来说明一下中序后序创建二叉树的过程与方法吧。
那么下面就简单的进行分析看看。
已知:
中序遍历:BDAEFC
后序遍历:DBFECA
那么应该怎么来进分析并确定二叉树呢?
1、 由后序遍历可知,最后一个节点为根节点,所以 A 为根节点
再由中序遍历可知,根节点左边的为左子树,右边为左右子树,所以, BD 为左子树, EFC 为右子树
所以,可以确定如下图这样的结构。
2、 对于D和B的关系,同样从后序遍历可知, B 为根节点,所以, E 为 B 的子树,但是需要确定是左子树还是右子树
在中序遍历中, D 在 B 的右边,由中序遍历根在前,右(子树)在右(边)可以确定, D 为 B 的右子树
所以,可以确定如下图这样的结构。
至此,左子树部分已经确定完成,开始确定右子树部分。
3、 将右子树 EFC 单独出来继续分析
由后序遍历可以得知 C 为根节点,但是不能确定 EF 为左子树还是有子树
由中序遍历,EF 在根节点 C 的左边,所以 EF 为 C 的左子树
所以,可以确定如下图这样的结构。
4、 在后序遍历中,F 在 E 的左边,所以 E 为根节点
在中序遍历中,F 出在 E 的右边,所以 F 为 E 的右子树
所以,可以确定如下图这样的结构。
至此,整个遍历创建的过程均已经完成
综上所述,在整个分析过程中,与先序中序的分析过程如出一辙,最主要的 从后序遍历顺序确定根节点,然后在中序遍历顺序中找到根节点的位置,然后将结果分为左子树和右子树,之后开始递归确定根节点,递归确定左子树,递归确定右子树。 就是这么简单,那么,根据这个特例我们呢可以写出对应的代码。
/**
* 功 能:
* 创建并且初始二叉树 - 按照中后序创建二叉树
* 参 数:
* inStr :中序遍历的顺序字符串
* postStr :后序遍历的顺序字符串
* num :树的节点的个数
* 返回值:
* 成功:创建完成的树的根节点
* 失败:NULL
**/
BiTNode *BiTree_Create(unsigned char *inStr, unsigned char *postStr, int num)
{
int pos = 0;
BiTNode *root = NULL;
if (NULL == inStr || NULL == postStr || num < 1)
{
goto ERROR;
}
if (num > 0)
{
root = (BiTNode *)malloc(sizeof(BiTNode));
/**
* 将后序遍历中最后一个字符赋值给当前节点
* 因为后序遍历中,做末尾的为根节点
**/
root->data = *(postStr + num - 1);
// 从中序遍历中查找当前节点的位置
while (pos < num)
{
if (*(inStr + pos) == root->data)
break;
pos++;
}
/**
* 递归创建左孩子节点,中序遍历在此节点前面的字符串均为左子树
* 后序遍历根节点在最末尾,前面的开始的为左子树,总计 pos 个
* 节点数目为 pos
**/
root->lchild = BiTree_Create(inStr, postStr, pos);
/**
* 递归创建右孩子节点,中序遍历在此节点后面的字符串为右子树,需要将字符串移动 pos+1 个位置
* 后序遍历根节点在最末尾,前面的pos个为左子树,后面为右子树,移动pos个
* 节点数目为 总结点数目减去 pos 再减去 1 (从0开始的)
**/
root->rchild = BiTree_Create(inStr + pos + 1, postStr + pos, num - pos - 1);
}
ERROR:
return root;
}
上面代码已经完成二叉树的中序和后序创建,下面简单的编写一个测试demo,测试一下创建过程,并使用四种遍历方式分别打印输出。
如果为了增加灵活性,可以将先序遍历和中序遍历的字符串通过终端输入
也可以为了方便测试,直接采用写入的方法测试
本测试采用分别输入中序遍历顺序和后序遍历顺序。
下面为 main.c 文件中的内容。
#include "../src/biTree/biTree.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern func_BiTree fBiTree;
#define BiTNodeNUMMAX 16
int main(int argc, const char *argv[])
{
int ret = 0;
BiTNode *tree = NULL;
unsigned char inOdBuf[BiTNodeNUMMAX] = {0}, postOdBuf[BiTNodeNUMMAX] = {0};
printf("请输入中序遍历字符串:");
fgets(inOdBuf, sizeof(inOdBuf), stdin);
printf("请输入后序遍历字符串:");
fgets(postOdBuf, sizeof(postOdBuf), stdin);
if (strlen(inOdBuf) != strlen(postOdBuf) ||
strlen(inOdBuf) > BiTNodeNUMMAX ||
strlen(postOdBuf) > BiTNodeNUMMAX)
{
printf("输入字符串信息异常或者不匹配,请检查并重新运行!\n");
ret = -1;
goto ERROR_END;
}
// 创建一个二叉树
tree = fBiTree.create(inOdBuf, postOdBuf, strlen(inOdBuf));
if (tree != NULL)
{
printf("二叉树创建成功!\n");
}
else
{
printf("二叉树创建出现异常!\n");
ret = -1;
goto ERROR_END;
}
printf("先序遍历输出:");
fBiTree.prevOrder(tree);
putchar(10);
printf("中序遍历输出:");
fBiTree.inOrder(tree);
putchar(10);
printf("后序遍历输出:");
fBiTree.postOrder(tree);
putchar(10);
printf("层序遍历输出:");
fBiTree.levelOrder(tree);
putchar(10);
ERROR_END:
// 释放二叉树
fBiTree.release(tree);
printf("system exit with return code %d\n", ret);
return ret;
}
下面为工程文件的结构,使用cmake进行工程管理与编译。
$ tree biTree-create-in-post/
biTree-create-in-post/
├── CMakeLists.txt
├── README.md
├── image
│ └── image.jpg
├── main
│ └── main.c
├── runtime
└── src
└── biTree
├── biTree.c
└── biTree.h
5 directories, 6 files
然后创建并编译工程,详细效果如下图所示。
至此,二叉树的中序后序创建已经全部测试完成!!!
四、手动创建
好啦,上面提到的各种创建方法,均需要提供先序遍历、中序遍历、后序遍历的相关遍历信息,不然的话也是巧妇难为无米之炊,但是现在我还想比较非主流化,我想用自己勤劳致富的双手亲自创建一个属于我自己的独特的树,那么下面的这个代码可以满足你的想法。
同样,本创建方式还是采用先序创建的方式,在创建过程中还是使用递归创建,代码如下。
/**
* 功 能:
* 创建并且初始二叉树 - 按照先序创建二叉树
* 参 数:
* 无
* 返回值:
* 成功:创建完成的树的根节点
* 失败:NULL
**/
BiTNode *BiTree_Create()
{
char ch;
BiTNode *tree = NULL;
ch = getchar();
// 如果是回车符,不需要再去获取回收垃圾字符了
if (ch != '\n')
getchar();
// 输入'#'或者'*'或者回车, 表示该节点为空
if (ch == '#' || ch == '*' || ch == '\n')
{
goto RET;
}
else
{
tree = malloc(sizeof(BiTNode));
tree->data = ch;
printf("%c的左孩子为: ", tree->data);
tree->lchild = BiTree_Create(tree->lchild);
printf("%c的右孩子为: ", tree->data);
tree->rchild = BiTree_Create(tree->rchild);
}
RET:
return tree;
}
下面为 main.c 文件中的内容。
#include "../src/biTree/biTree.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern func_BiTree fBiTree;
int main(int argc, const char *argv[])
{
int ret = 0;
BiTNode *tree = NULL;
printf("请输入树的根节点:");
// 创建一个二叉树
tree = fBiTree.create();
if (tree != NULL)
{
printf("二叉树创建成功!\n");
}
else
{
ret = -1;
printf("二叉树创建出现异常!\n");
goto ERROR_END;
}
printf("先序遍历输出:");
fBiTree.prevOrder(tree);
putchar(10);
printf("中序遍历输出:");
fBiTree.inOrder(tree);
putchar(10);
printf("后序遍历输出:");
fBiTree.postOrder(tree);
putchar(10);
printf("层序遍历输出:");
fBiTree.levelOrder(tree);
putchar(10);
ERROR_END:
// 释放二叉树
fBiTree.release(tree);
printf("system exit with return code %d\n", ret);
return ret;
}
然后创建并编译工程,详细效果如下图所示。
为了方便对比上面在测试过程中输入的二叉树的遍历的结果,下面根据输入过程复原二叉树的结构模型,如下图所示。
至此,二叉树的手动先序创建已经全部测试完成!!!
五、二叉树的释放
二叉树本来就是一个递归的矛盾体,所以二叉树的创建、遍历、释放等等都将继承这个特性,当然可以用非递归的方法去实现各类操作,但是本质上来说,使用递归能够非常明显的、快速的说明其特征。那么下面所说的二叉树的释放也不落俗套的使用递归去实现。
但是需要注意的就是,在二叉树的释放过程中,我们要使用类似于后序遍历的方式去释放二叉树的所有节点,因为不能一开始就将根节点释放,那么后面的节点将不再受到控制了,至于先释放左子树还是先右子树就看个人喜好了,实际上并不会影响整个二叉树的释放。
所以,本文中使用的二叉树的释放的实现代码可以这样写。
/**
* 功 能:
* 二叉树节点释放
* 参 数:
* tree:要释放的二叉树
* 返回值:
* 无
**/
void freeTree(BiTNode *tree)
{
if (tree == NULL)
return;
// 递归释放左孩子节点
if (tree->lchild != NULL)
{
freeTree(tree->lchild);
tree->lchild = NULL;
}
// 递归释放右孩子节点
if (tree->rchild != NULL)
{
freeTree(tree->rchild);
tree->rchild = NULL;
}
// 释放根节点
if (tree != NULL)
{
free(tree);
tree = NULL;
}
}
好啦,不知道经历了多少个日夜这个简单的文章才算完成,主要是因为只能在工作之余才能总结总结,时间实在是宝贵的一P,也让这片文章以一天几乎10个字的速度在龟速前行,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然关注一波那就更好了,好啦,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。
上一篇:数据结构(十一) – C语言版 – 树 - 二叉树基本概念
下一篇:数据结构(十三) – C语言版 – 树 - 二叉树的遍历(递归、非递归)