二叉树链式结构的实现

一.二叉树的概念:

在实现代码之前,先回顾一下二叉树的概念:

二叉树是:
1. 空树
2. 非空:根结点,根结点的左子树、根结点的右子树组成的。
递归问题就是将大问题转化成小问题,在这里看图右将一颗树分成了三颗树,其实还可以细分,所以我们解决二叉树的相关问题基本都是靠递归来实现的。

二.二叉树结构的代码实现:

      1.前序,中序,后序概念介绍:

       在实现代码之前还要介绍一个关于二叉树遍历的概念:

       按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历

        1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

        2. 中序遍历 (Inorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之中(间)。
        3. 后序遍历 (Postorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之后。
这里讲太多概念还是没用这里画图来解释:
  
                                      
那这颗二叉树为例子看看二叉树的遍历是如何进行的:
前序遍历:
1作为根节点,然后往左子树遍历,2也作为根节点,然后继续往左子树遍历,当3作为根节点后继续访问左子树和右子树,当3作为2的左子树访问完之后再去访问2的右子树,就这样继续访问下去。
看完前序遍历,现在来看一下中序遍历:

这就是中序遍历,先找到左子树,将左子树全部遍历完在去找到根节点,然后遍历右子树的过程。

看完中序遍历,再来看一下后序遍历:

跟前两个遍历顺序一样,先将左子树遍历,再去遍历右子树。图解画的非常详细可以参照。

在实现二叉树结构的代码前,这三种遍历顺序也就介绍完了。

还有一个层序遍历,遍历的结构就是1 2 4 3 5 6  这个概念非常好理解。

  2.前置声明:

定义一个二叉树的结构体,里面存着节点的数据,还有左子树的结构体和右子树的结构体。

3.创建树的节点的函数:

在前面链表中,我们在验证其功能是否成功实现前,我们自己手动创建了几个节点,然后将这几个节点连接在一起形成了链表,所以在这里我们也手动来创建一颗树,所以在创建一个树之前,要完成创建树的节点的函数:

4.手动创建一颗树:

在写完创建树的节点的函数之后,我们在手动创建一颗树就变得更简单了:

                          

上述代码按照这个二叉树来创建的,这个代码也非常的简单。

5.前序遍历代码实现:

前面介绍了前序遍历,现在来实现前序遍历的代码:

先运行一下看看这个程序是否正确:

运行结果跟我们上述的分析是一致的所以代码实现成功

这里就用到了递归,前序就是根 左子树 右子树,这里如果不为空就先打印根的节点,然后将那个根节点的左子树作为下一个根节点去进入这个递归,当这个递归走完之后就继续下一个递归,这里可以画图解释一下这个递归是怎么走的:

首先1作为第一个根节点传到函数中去,然后往下遍历,就先打印1,打印完之后走下一步就是再次创建一个新的函数,1的左节点(2)作为参数传给函数,2不为空就接着作为根节点打印。

然后走到下一步再次开辟一个新的函数,将2的左节点(3)作为根节点传过去,3这个节点又作为根节点先打印然后继续往下走,这回继续开辟一个新的函数

此时3的左节点为空,然后就要返回了:

返回后继续走3那个节点开辟没走完的函数,这时就是3的右节点作为函数参数传过去了:

往下走3的右节点也为空,但还是开辟一个新的函数,继续往下走,为空所以返回到上一个3的函数:

此时3作为根节点的这个函数已经走到尾了,所以继续回到2作为根节点的那个函数继续往下走,此时2的右节点为空,但还是要开一个新的函数将空作为参数传过去

判断完为空就返回到2的那个函数中去:

2作为根节点的那个函数走完了,也就是1作为根节点的那个左边节点都走完了,接下来走1的右边节点的函数:

4作为1的右节点,同理打印4,打印完之后继续往下走,这时4的左节点(5)为根节点往下走开辟一个新的函数:

5作为根节点打印,然后继续往下走,5的左节点为空然后继续创建一个函数:

然后5的左节点为空返回到原函数,作为根节点的函数左5子树走完了,再继续走5作为根节点的右子树,右子树为空然后继续开辟一个函数,5的右子树也为空然后继续返回

6这个节点跟前面一样的,这里就不画了,画不下了。

这就是整个前序遍历代码的实现加解析。

前序遍历代码实现完了,现在来实现中序遍历:

6.中序遍历代码实现:

中序就是左子树  根  右子树,按照前序遍历照葫芦画瓢换一下位置即可:

现在来测试一下运行结果是否与一开始分析的中序遍历结果一致:

                       

和上述分析的一致,所以代码实现正确,这里函数调用的解析跟前序遍历差不多,这里就不做过多的赘述了。

中序遍历代码也成功实现,现在来实现后序遍历

7.后序遍历代码实现:

后序就是 左子树  右子树  根,也是调换一下调用顺序即可实现:

         

实现成功,现在来看看打印结果是否正确:

                          

结果一致也是成功实现。

8.计算树节点(根节点)个数的代码实现:

   这里我们要想计算树节点个数,第一个想到的就是遍历,前面我们实现了三种遍历,但三种遍历都是利用递归来实现的,所以这里你定义一个size来记录树节点个数是加不上去的,所以这种思路直接pass,这里提到了递归,所以这里我们这里也用递归来实现,看看是否可行

递归思想跟实现遍历一样,先判断当前传过来的根节点是否为空,为空就返回0,不为空再次调用将跟根节点的左子树和右子树传过去,现在思路有了我们来实现一下看看是否可行:

        

这里我们还是拿的上面出现的六个节点的树为例子,但是这里打印发现结果为1,这时为什么呢,因为这里创建的size变量是动态的,你每调用1次他都是一个新的size去计算返回时就一直是1,所以打印结果为1,既然这里因为size是动态的导致计数计算不上去,所以我们这里换一种思路将size定义为静态局部变量试一下:

这里我们发现将size定义为静态局部变量即可

但是当我们调用过一次计算树节点的函数时,再去调用一次,你就会发现结果就会发生改变,不再是原来的6个而是12个:

因为这时静态局部变量,当年再次去调用的时候他的初值是不会置为0的,而是会往上叠加,所以这种方法也不可行。

再上面的错误示范的第一种你发现他每次调用都将size置为了0,导致树的节点加不上去,所以我们不定义局部变量,这里我们定义全局变量试一下:

这里也是成功计算出结果,但是这个代码太挫了,每次调用前都要将size手动置为0,非常麻烦所以这种方法也pass。

既然size怎么搞都十分麻烦索性我们不需要size直接利用三目运算符和递归思想一把将结果算出来然后返回:

 

这里验证一下看看结果是否正确:

结果正确,那么在这里,我们来分析一下,这个详细的过程:

这就是整个分析的过程,这样计算树节点个数的代码也就成功实现了。

9.计算叶子节点(终端节点)个数的代码实现:

就是计算终端节点的个数:

这里还是考虑递归思想,遍历还是实现不了。

如果根节点为空就返回0,如果根节点存在,左右节点都为空就返回1,因为只有根节点一个叶子节点,所以就返回1,如果左右子树有一个不为0,就去调用递归函数,利用递归还是跟上面思路一样,先计算左子树的终端节点然后再计算右子树的终端节点,两个相加即可。

这里来分析一下这个递归的过程:

这就是整个递归过程结果为3,看看运行结果是否为3:

结果正确,到这里计算叶子节点(终端节点)个数的代码也就实现成功了

10.求树的高度的代码实现:

 树的高度也就是树的层数,这种问题还是分成左子树和右子树两个部分来计算:

首先还是考虑作为根节点的那个节点如果为空,就返回0,求树的高度时。只要左右节点有一个节点就算1层,也就是算一个高度,所以这里我们要考虑是左子树有节点还是右子树有节点,怎么考虑是左子树还是右子树有节点呢,这里就用比大小来考虑:

这里是根据这个思路写的代码:

来看看运行结果:

运行结果也是正确的,但是这里存在一个问题,我们先看看递归计算的过程是什么样的吧:

当走到这由于可以发现到3这个结点,左右节点都是空,所以返回1给3那个作为根节点的函数,但是当真正返回的时候,我并没有定义一个变量来存贮刚刚获得的值,在这里如果是以一颗很大的树时,比大小时要递归一次,真正返回值时又要递归一次,所以这个是及其麻烦并且记不住值的方法。

所以想要优化的话就定义两个变量分别存左子树和右子树的高度:

来运行一下看看树的高度是不是3:

结果正确了,现在来分析一下整个递归过程吧

这就是整个递归过程,如果不利用变量存储的话,可想而知有多麻烦

11.求二叉树第k层结点个数:

这里还是考虑用递归的思想,还是分为左子树和右子树的两个小问题,比如这里要求第三层结点的个数:

                   

还是如果节点为空就返回0,如果根节点不为空,就继续往下访问,当k减减到1时,就不能继续往下访问了,因为求的就是求二叉树第k层结点个数,所以再往下访问就是违背了要求,所以这里直接返回1,也就是到达哪一层的那个节点,然后就是递归左子树和右子树,并且传k-1这个变量:

这里还是分析一下递归的过程,这里还是以求第三层节点个数为例子:

          

分析递归如下:

可以看出运行结果也是正确的。

12.二叉树查找值为x的节点:

在以前,查找都是用遍历来写,但是在这里我们还是用到递归来写

我一开始是这样写出代码的,也就相当于是前序遍历,但是这里函数的返回值是一个指针,而在这里,如果if的情况都进不去,这个函数就没有返回值了,就说明这个函数是错的。所以在这里优化一下并且解决这个问题:

        

这里返回空也就解决了,函数有可能没有返回值的可能了。

这里的递归展开图跟上面差不多,这里也就不做多赘述了。

13.二叉树的销毁:

                  

这里其实用到了后序遍历,如果先释放root的话就找不到root的左子树和右子树了,先找到左右子树,再去释放掉根节点。这样二叉树的销毁就完成了。

14.层序遍历:

前面讲的都是前序,中序,后序遍历,这里的层序遍历,就是一层一层来遍历:

                

先遍历第一层 1,再遍历第二层 2,4 ,再遍历最后一层 3,5,6。这里你可以发现使用递归无法实现,因为递归,你只能先把左子树或者右子树遍历完才能遍历其他的,但是这里层序遍历是一层一层来的。

因为树不是连在一起的蛮,根节点带左子树和右子树,所以这里想到用队列,一层带着一层入队列,队列不是先进先出蛮,这里用图来进行演示:

现在1是根节点那就入队列,入队列的时候就把2,4带着,正好是第一二层打印顺序就搞定了,此时1再出队列,我们在实现队列时写了一个函数是取队头数据,所以直接取即可,这时1取走了,这时2就是队头了,取出队头2的数据,然后再将2带的左右子树入队列,这里空就不算了:

此时2就是队头,将其取出,然后2的左子树和右子树入队列:

接下来步骤一样 取出4,然后 5,6 入队列:

此时,一个个出队列,直到队列为空,循环就结束了。

这样层序遍历也就完成了。

上述介绍到要用到队列,所以这时我们就要将我们之前写好的队列的头文件和源文件插入到现在实现二叉树的文件中去:

先在之前写的文件中找到写的队列的代码然后拷贝一份到当前写的文件夹中即可:

这里实现二叉树的文件中就要包含队列的头文件,才能实现关于队列的一系列函数:

但是这里队列存储的是树的一个个节点而不是一个个值,所以要将队列中原本定义的数据类型要改一下:

所要改成二叉树结构类型的结构体指针,因为是二叉树的文件包括队列的头文件所以在队列的头文件中找不到二叉树的文件,所以这里要用到前置声明:

这样在使用的时候就不会报错了,在前面利用顺序表实现通讯录的时候也用到了前置声明:

通讯录的实现(基于顺序表来实现)-CSDN博客

这时候前期准备工作就全部完成了,接下来就是代码的实现了:

这里写的时候有个问题这里传的就不是数据了,因为上面已经前置声明将队列的类型改了:

所以这里直接传指针root过去:

这样就对了,下面用到要用到直接传指针过去就行,因为类型已经更改了。

根据上述思路这就是整个代码的实现:

现在我们来测试一下,是否根据层序遍历来打印的:

也是成功一层层的打印出数据,这个函数比较麻烦的点就是把之前写的队列的代码搞进来,还有一步最重要的就是前置声明,这两步实现了,再加上上面的思路,这个代码的实现就比较简单了。

15.判断二叉树是否是完全二叉树:

我们在这里再次回忆一下什么是完全二叉树,完全二叉树就是假设有k层,k-1层都是满节点,而第k层的节点存在必须是连续的,中间不能有空节点,如果中间有空节点,然后又有节点的话这种就不是完全二叉树。

根据这个介绍,再根据上面队列的思想,入一个带左子树与右子树,如果我们遇到第一个空就开始判断如果接下来全是空即可说明是完全二叉树,如果空后又有节点就说明不是完全二叉树。

这里可能会有人担心如果前面全是空,后面其实还有一个节点但没有入队列怎么办:

                        

其实完全不用担心这种情况,1入队列带2,4            2出队列带3 ,6    4出队列带空,空   此时已经有空入队列了   3出队列带空,空   6出队列带空,6  可以分析出这个6是入队列了的所以到后面又有节点出来了所以其不是完全二叉树,

还有一种情况确实有数据没有入队列,这时因为在空空后已经有数据出现了,所以不用入数据了,已经不是完全二叉树了。

   

根据分析接下来就是代码的实现了:

以上就是关于二叉树实现的全部内容。

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值