携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
前言
在讲完线性表后,接下来要介绍树结构中的二叉树,本文是基于C语言实现的。
本文就来分享一波作者对数据结构二叉树的学习心得与见解。本篇属于第十篇,介绍二叉树链式结构的相关内容,建议阅读本文之前先把前面的文章看看。
笔者水平有限,难免存在纰漏,欢迎指正交流。
二叉树的链式结构
概念与认知
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链接关系来表示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,以后学到高阶数据结构如红黑树等会用到三叉链。
正确认知二叉树
- 空树
- 非空树:根节点,根节点的左子树、根节点的右子树组成的。
二叉树定义是递归式的,因此后面的基本操作中基本都是按照该概念实现的 。
简单创建二叉树
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在我对二叉树结构掌握还不够深入,为了降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,反过头再来研究二叉树真正的创建方式 。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* CreateBinaryTree()
{
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;
}
注意:上述代码并不是真正创建二叉树的方式,真正创建二叉树方式以后再重点讲解 。
这里的代码建出来的二叉树是这样的:
二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。 这里主要运用的是递归方法和分治思想。
前序、中序以及后序遍历
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历
- 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
三种遍历的顺序:
- 前序遍历:根->左子树->右子树
- 中序遍历:左子树->根->右子树
- 后序遍历:左子树->右子树->根
举个例子:
三种遍历的过程(把空树NULL也算进去):
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
遍历时逻辑结构图解
前序遍历打印和图解分析
这里写的是用前序遍历打印每一个结点(包括空树结点)。函数一开始判断当前根结点是否为空,一是应对二叉树为空的情况,二是作为递归的终止条件,也就是遍历到空结点就该往回退。
如果操作代码在递归代码前面那就是前序遍历,比如这里是把打印操作放在了左右子树递归操作之前,也就相当于先访问根结点。那不难发现,要递归实现前中后序遍历主要看操作代码和递归代码的位置关系(代码逻辑执行的顺序),看看是先操作还是先递归。
代码实现
void PreOrderPrint(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrderPrint(root->left);
PreOrderPrint(root->right);
}
函数调用过程图解
中序遍历打印
相比于前序遍历的代码,逻辑稍作改动即可:之前是先访问根结点,这里是先访问左子树,所以操作代码在左子树递归操作之后、右子树递归操作之前。
void InOrderPrint(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrderPrint(root->left);
printf("%d ", root->data);
InOrderPrint(root->right);
}
后序遍历打印
相比于中序遍历的代码,逻辑稍作改动即可:这里是先访问左右子树再考虑根结点,所以操作代码在左右子树递归操作之后。
void PostOrderPrint(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrderPrint(root->left);
PostOrderPrint(root->right);
printf("%d ", root->data);
}
遍历结果推导题
先上三个结论:
- 已知前序遍历结果和中序遍历结果,可以唯一确定一棵二叉树。
- 已知中序遍历结果和后序遍历结果,可以唯一确定一棵二叉树。
- 已知前序和后序遍历结果,不能唯一确定一棵二叉树。
为什么第三个就不能唯一确定呢?
举个简单的例子(这里不考虑NULL):
比如前序遍历结果为ABC,后序遍历结果是CBA。我们可以确定A一定是根结点,但我们无法知道,哪一个结点是左子树,哪一个结点是右子树,实际上这棵树有四种可能。
从这里我们可以引申出一个结论:一棵非空的二叉树的先序遍历结果和后序遍历结果正好相反,则该二叉树一定满足只有一个叶子结点。
例题:
已知某二叉树的中序遍历序列为JGDHKBAELIMCF,后序遍历序列为JGKHDBLMIEFCA,则其前序遍历序列为( )
A.ABDGHJKCEFILM
B.ABDGJHKCEILMF
C.ABDHKGJCEILMF
D.ABDGJHKCEIMLF
由后序遍历确定子树的根,后序遍历从后向前看,最后一个元素为根,和前序遍历刚好相反,从后向前看后序遍历,应该是根,右,左,根据中序遍历确定子树的左右区间
故根:A
A的左子树:JGDHKB A的右子树:ELIMCF
A的左子树的根:B A的右子树的根:C
B的左子树:JGDHK B的右子树:空
C的左子树:ELIM C的右子树:F
B的左子树的根:D C的左子树根:E
D的左子树的根:G D的右子树的根:H E的右子树的根:I
故树的结构为:
故前序遍历结果:
A B D G J H K C E I L M F
所以选择B
以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~