当链表学习结束,也就可以向更加重要的对内存的分配操作前进。那么这一篇是对二叉树的个人理解和思考。如果有不当的地方欢迎在评论区进行指导
首先是介绍二叉树
二叉排序树是一种比较有用的折衷方案。 数组的搜索比较方便,可以直接用下标, 链表与之相反,删除和插入元素很快,但查找很慢。 二叉排序树就既有链表的好处,也有数组的好处。 在处理大批量的动态的数据是比较有用。
我认为可以把它理解成为链表的一种进步,那么重点就是如何对这个进步的建设
首先是先对其创建结构代码。
这里的代码和链表很像,都是先是定义一个数据域和两个指针域
typedef struct node{
char data;
struct node*l,*r;
}btnode,*btree;
接下来是创建二叉树表,二叉树和链表很像,其中很重要的地方就是在这个地方。在这里同样输入要生长的节点数量同时用不同指针域内的指针由相同的初始位置指向不同的两个内存。因为这里是要有返回值的函数,所以这里使用的函数应该使用int而不是void其中要求的形参应该是其双亲结点。
int Createbtree(btree* T)
{
char ch;
scanf("%c", &ch);
*T = (btree)malloc(sizeof(btnode)); //动态申请内存
if (*T == NULL)
{
printf("内存申请失败\n");
return 0;
}
(*T)->data = ch; //生成根结点
CreateBTree(&(*T)->Lchild); //构造左子树
CreateBTree(&(*T)->Rchild); //构造右子树
return 1;
}
其中也有使用了一个特定值创建出来虚节点。这样可以让节点确定它是否存在左右孩子。
比如说在树上使用的是‘#’
学完了生成一个二叉树,那么就是要学习如何销毁一个二叉树。首先是要判断,这个二叉树的两亲节点是否为空,当为空时,说明这个二叉树并不需要销毁的多么复杂。但如果存在,那么我只能只能判断左边和右边有没有存在的部分,然后对他进行一个销毁的操作。举一个很简单的例子,和书上的例子很像
void Destroybtree(btree* T)
{
if (*T) //双亲结点不为空
{
if ((*T)->Lchild) //左孩子不为空
DestroyBTree(&(*T)->Lchild); //销毁左孩子
if ((*T)->Rchild) //右孩子不为空
DestroyBTree(&(*T)->Rchild); //销毁右孩子
free(*T); //销毁双亲结点
*T = NULL; //双亲结点指空
}
}
链表的销毁比这个更加简单,因为其中只存在一条线路,所以其删除的时候并不需要对左右进行判断等。
接下来是二叉树的遍历,如果用习惯的从左到右的办法来遍历这个二叉树的话,那么最经常使用到的可能是中序遍历。这种方法是从最左边树分支的子树开始。从下往上如果有节点的话会从节点哪里读靠里面的部分最后读到根节点,从根节点再开始读右子树。来保证其读到的顺序的一致性。
这个是我从网上找到的样图,中序读图四的话会得到#bd##ac##
值得注意的是,在二叉树遍历当中,每一个节点都应该被访问一次,而且只会被访问一次,毕竟如果一个节点被访问多次,谁也说不清楚这个节点到底出现过几次。
但就算是从左到右也可以有更多的办法来遍历这个二叉树。
比如说前序遍历
这个遍历方法是先从根节点开始遍历这个左子树,当左子树被遍历一遍结束之后,会从左子树的最后一个跳到右子树的第一个。
那么同样是刚才的那个例子,如果是前序ab#d##c##
后序遍历更简单了,他是从左边的最下面开始,一层一层的遍历然后得到输出同样是要保证是从左子树开始的,不然不能得到。
好了,在这个情况下我们再一次对刚刚的例子树进行一次遍历。那么我们可以得到一个简单的结论。在得到这个#b##d##ca
在很多书上都存在层序遍历,这个我个人认为和后序遍历更像,其中都是从极端的地方开始遍历,只不过后序遍历是从最下面进行遍历,但层序遍历是从最上面开始,而且不会存在只有在遍历完一个左子树之后才可以进行遍历右子树。
这里有一个需要注意的地方,只有同时知道前序和中序或者后序和中序的才可以得到一个确定的二叉树。当只知道前序和后序,那么可以判断出来根节点,但是不能判断是不是一个二叉树,他可能是一种链表,只不过排列的顺序并不是如人所愿的那种。
那么在存在了遍历方法的情况下,我便开始不再考虑费劲把自己的时间花在这种事情上。。。虽然是的确有一些懒惰。但事实可以通过一个简单的程序来结束这个复杂的问题
在前序遍历当中,其输出的方法如下
void PreOrderTraverse(BTree t)
{
if (t)
{
printf("%-5c", t->data); //输出双亲结点
PreOrderTraverse(t->Lchild); //遍历左孩子
PreOrderTraverse(t->Rchild); //遍历右孩子
}
}
这样可以在不所以循环的情况下,对这个二叉树进行遍历, 那么中序和后序通过分析机会发现,其中的差别主要还是在顺序上
void InOrderTraverse(BTree T)
{
if (T)
{
InOrderTraverse(T->Lchild); //遍历左孩子
printf("%-5c", T->data); //输出双亲结点
InOrderTraverse(T->Rchild); //遍历右孩子
}
}
void PostOrderTraverse(BTree T)
{
if (T)
{
PostOrderTraverse(T->Lchild); //遍历左孩子
PostOrderTraverse(T->Rchild); //遍历右孩子
printf("%-5c", T->data); //输出双亲结点
}
}
虽然可能顺序有点变化,但重要的是先遍历左子树是必须的。
那么接下来便是对这个二叉树进行更多的处理。
比如说,我现在需要对这个二叉树的一个部分进行修改,更准确来说是要删除一个部分
比如说我觉得左子树出现了很大的问题,而且是不可逆的那种,或者我单纯看左子树不爽,那么可以直接对其中的左子树直接进行删除,那么该如何才可以呢?观察前面删除整个二叉树的代码,可以发现二叉树的删除中也可以使用的部分,如果我要删除左子树,那么关右子树和根节点什么事情呢,好像并没有什么关系。那么就简单了
if (*T) //双亲结点不为空
{
if ((*T)->Lchild) //左孩子不为空
DestroyBTree(&(*T)->Lchild); //销毁左孩子
相同的对于右子树也可以使用相同的办法来解决
解决了删除相关的问题,那么就可以考虑一下生成过的东西,假如说别人给我看他的代码,在他写的树中我感觉他生成的二叉树的层数太多了,但我不确定或者其他原因,那么有没有办法来解决这个问题呢?重点是在每一层生成的时候不能再还没有生成结束便退出这个二叉树。那么就很简单了,在没有生成的二叉树中直接输出一,在已经有孩子的二叉树中进行遍历,左右子树都要完整的遍历,以避免出现出现层数少了的情况,毕竟有可能会有在输入和生成结束将左子树删除,那么对左子树的遍历并不可能得到正确的结果,要是在左右子树中得到较大的结果进行输出。那么这里还是用代码来说
int MaxDepth(BTree T) {
if (T == NULL) {
return 0; //返回值传给maxLeft或者maxRight
}
else {
int maxLeft = MaxDepth(T->Lchild); //maxLeft最终值是0
int maxRight = MaxDepth(T->Rchild); //maxRight最终值也是0
if (maxLeft > maxRight) {
return 1 + maxLeft; //返回双亲结点加上最深子树的深度
}
else {
return 1 + maxRight;
}
}
}
这里使用的是 cdhuangjin- 的源码可以发现他是对这个是对这个进行了一次判断树的是否存在,然后在最后直接进行return相加,那么可能会有问如果这个树只有一个根节点该怎么办的话,可以很容易发现如果没有左右子树,即左右子树都是0。会直接竟然最下面的else输出是左边的0加上1即深度为一(可以说是比我的想法好很多的思路)
简单来说对树的遍历也可以帮助到很多涉及数数的问题,毕竟他虽然是树,但也的确不能直接表现出来他的生长情况,当存在一个树,不清楚的情况下直接对其输入的确不够礼貌。。吧
遍历了这个树之后,也就可以再对其中的节点进行输出
也许有人好奇,这不是很简单的吗,将所有的层数进行一次乘二累加在一起就好了,但需要注意到是在删除中可以存在删除左子树的左子树,那么的话,可能这棵树也就已经被修改了很多次了,那么这个方法并不适用,已经知道了,我们在创建一个新的树的时候,已经将每一个确定下来节点给指向了NULL这样子较好解决了很多。首先可以通过对每一个节点的指向进行判断。判断出来再进行指向的不是NULL累加
int NodeCount(BTree T)
{
if (T == NULL)
return 0;
else
return NodeCount(T->Lchild) + NodeCount(T->Rchild) + 1; //返回双亲结点加上左子树和右子树的结点
}