二叉树遍历操作的实现以及鲁棒性的完善

Posted nufe_wwt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树遍历操作的实现以及鲁棒性的完善相关的知识,希望对你有一定的参考价值。

二叉树遍历操作的实现以及鲁棒性的完善

一、实验内容

1.实验目的

能够使用先序序列构建一棵基于二叉链表存储结构的二叉树;能够对该二叉树实现先序、中序以及后序遍历;完善鲁棒性,使得尽可能多的输入都有与之对应的输出。

2.实验内容

1)使用先序序列建一棵基于二叉链表存储结构的二叉树
使用CreatBiTree()函数构造二叉树;

2)对该二叉树实现实现先序、中序以及后序遍历
使用PreOrderTraverse()函数实现先序遍历;
使用InOrderTraverse()函数实现中序遍历;
使用PostOrderTraverse()函数实现后序遍历;

3)完善代码健壮性
使用whetherCan()函数判断所输入序列是否可以构建一棵二叉树;
whetherCan()函数流程图如下图所示:

4)实现代码并输出相关数据
多次输入以获得不同的输出,检验鲁棒性;

一.先给出一个例子,要构建一棵如下图的树,则需要输入的先序序列为ABC##DE#G##F###(#表示空结点,CSDN’星号‘是文本样式的一种,在代码中笔者用的是‘星号’):

其先序遍历结果为:A B C D E G F
其中序遍历结果为:C B E G D F A
其后序遍历结果为:C G E F D B A

二.先、中以及后序遍历的函数实现机制都大差不差,但是下面的内容中我将都做介绍;

三.鲁棒性的完善在我打草稿时候写了这么两条:
1.直接无根节点(直接回车)
2.少了必要的空节点
第一点很好判断,但在本次实验没有对其完善鲁棒性;我将在下文(二、实验过程)中的whetherCan()函数分析中着重分析第二条。

四.实验输出见下文(三、实验结果)。

二、实现过程

本次实验环境:macOS Mojave, Xcode

根据实验要求,本次实验着重分析了鲁棒性的完善,并给与较为清晰的输出。下面是实验过程:


头文件以及使用结构体以及类别名

1.头文件以及使用结构体以及类别名,stdlib头文件用于分配动态存储空间,string头文件用于调用相关函数strlen(),一个二叉树节点拥有data域以及左孩子、右孩子的指针域,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct BiTNode 
   char data;
   struct BiTNode *lchild, *rchild;
 BiTNode, *BiTree;

whetherCan()函数

2.判断序列是否可以构成树的whetherCan()函数,(该函数及是用来完善鲁棒性所写)以及其在main函数中的实现,如下所示:
在whetherCan()函数返回值为int类型,为的是能够在主函数中实现自身循环(当输入结果不符合要求时),传入的参数为用户输入的一段字符串。在函数体重定义并初始化了countNode以及countNull,这两个值分别表示字符串中非空(星号)符号的个数以及空符号的个数减1,为什么要减1呢?
这正是本条算法的精彩之处:通过不断地列举先序序列,笔者得出两个,假设一棵树有n个节点。那么有:
1.该树有n+1个空节点,也就是说真正的countNull(也就是空结点)的个数应当为countNode(节点个数)+1。(当然这个结论书上也有)
2.在先序序列的最后一个符号之前(易知最后一个符号一定代表空结点),从第一个字符到倒数第二个字符,countNode一定一直保持 >= countNull,该结论便是笔者多次试验得出的;
基于以上两条结论,便有了以下的判断语句,也解释了为什么countNull在最后出来只能是空结点数-1,而不能是空结点数。而whetherCan()函数在主函数中的实现也满足了要求用户输入正确序列的要求。
另外:还判断了输入字符串的长度是不是1,因为在for循环中,条件i<lens-1,如果lens==1将不会进行for循环,这种情况对应直接输入一个*或者非空字符,而这两种输入都应该判0.

//whetherCan()函数
int whetherCan(char str[]) 
   int countNode=0, countNull=0;
   int i;
   int lens=strlen(str);
   if(lens==1) return 0;
   for(i=0; i<lens-1; i++) 
      if(str[i] == '*')
         countNull += 1;
      else
         countNode += 1;
      if(countNode<countNull) return 0;
   
   if(countNode != countNull) return 0;
   return 1;
   
//其在main函数中的实现
   char str[21];
   scanf("%s", str);
   while (!whetherCan(str)) 
      printf("先序序列%s无法构建一个二叉树,请重新输入:\\n", str);
      scanf("%s", str);
   
   printf("您输入的先序序列为:%s,请再次输入一遍来进行遍历:", str);


CreateBiTree()函数

3.创建树的CreateBiTree()函数及实现,如下所示:
可以先看下文中主函数的第一部分,可以清楚的知道:只有当用户输入正确的能够构建一棵二叉树的先序序列时,才会继续运行下面的代码,也就是说运行到CreatBiTree()函数时,用户自己已经输入了一串准确的字符串,而在主函数中要做的,就是提醒用户再对照着输入一遍,便可以准确无误地构建一棵二叉树。

//CreateBiTree()函数
int CreatBiTree(BiTree &T) 
   char ch;
   scanf("%c", &ch);
   if(ch=='*') T = NULL;
   else 
      if(!(T=(BiTNode*)malloc(sizeof(BiTNode)))) exit(-2);
      T->data = ch;
      CreatBiTree(T->lchild);
      CreatBiTree(T->rchild);
   
   return 1;


//实现
   BiTree T;
   CreatBiTree(T);

Visit()函数

4.Visit()函数,如下所示:
Visit()函数用于对已经读到的结点做一个输出,充当一个功能函数。

int Visit(char  e) 
   printf("%c ", e);
   return 1;


三序算法

5.先序遍历PreOrderTraverse()函数、中序遍历InOrderTraverse()函数以及后序遍历PostOrderTraverse()函数及其实现如下所示:
PreOrderTraverse()函数:先序遍历先中后左再右,先访问“根”,再访问左子树,最后访问右子树,依次遍历;
InOrderTraverse()函数:中序遍历先左后中再右,先访问左子树,再访问“根”,最后访问右子树,依次遍历;
PostOrderTraverse()函数:后序遍历先左后右再中,先访问左子树,再访问右子树,最后访问“根”,依次遍历。
值得注意的时候在主函数用到的函数指针,这一块笔者并不是十分熟悉。

//先序
int PreOrderTraverse(BiTree T, int(* Visit)(char e)) 
   if(T) 
      if(Visit(T->data))
         if(PreOrderTraverse(T->lchild, Visit))
            if(PreOrderTraverse(T->rchild, Visit)) return 1;
      return 0;
   
   else return 1;


//中序
int InOrderTraverse(BiTree T, int(*Visit)(char e)) 
   if(T) 
      if(InOrderTraverse(T->lchild, Visit))
         if(Visit(T->data))
            if(InOrderTraverse(T->rchild, Visit)) return 1;
      return 0;
   
   else return 1;


//后序
int PostOrderTraverse(BiTree T, int(*Visit)(char e)) 
   if(T) 
      if(PostOrderTraverse(T->lchild, Visit))
         if(PostOrderTraverse(T->rchild, Visit))
            if(Visit(T->data)) return 1;
      return 0;
   
   else return 1;


//实现
   BiTree T;
   CreatBiTree(T);
   int (*p)(char);
   p = Visit;
   printf("先序遍历序列是: ");
   PreOrderTraverse(T, p);
   printf("\\n");
   printf("中序遍历序列式: ");
   InOrderTraverse(T, p);
   printf("\\n");
   printf("后序遍历序列是: ");
   PostOrderTraverse(T, p);

主函数

6.主函数main(),分为两部分main() (1)以及main() (2),如下所示:
主函数的第一部分功能为:提示用户输入,并确保用户的输入是一个二叉树准确的先序序列,并提示用户再次进行输入,这一部分主要依赖于whetherCan()函数完成;
主函数第二部分的功能为:依照用户输入的正确先序序列构建一棵二叉树,完成三序遍历并输出相应结果。

int main() 
//main(1)
   printf("\\n请输入不超过20位的先序序列,“*”表示空节点:\\n");
   char str[21];
   scanf("%s", str);
   while (!whetherCan(str)) 
      printf("先序序列%s无法构建一个二叉树,请重新输入:\\n", str);
      scanf("%s", str);
   
   printf("您输入的先序序列为:%s,请再次输入一遍来进行遍历:", str);
   char c = getchar();
   printf("\\n");
   
//main(2)
   BiTree T;
   CreatBiTree(T);
   int (*p)(char);
   p = Visit;
   printf("先序遍历序列是: ");
   PreOrderTraverse(T, p);
   printf("\\n");
   printf("中序遍历序列式: ");
   InOrderTraverse(T, p);
   printf("\\n");
   printf("后序遍历序列是: ");
   PostOrderTraverse(T, p);
   printf("\\n");
      

三、实验结果

为了证明此次试验的鲁棒性被完善了些许,笔者将提供6种输入以及其对应的输出并分析其中的问题,输入结果如下所示:(#即意味着是星号)

1.第一种输入:简单的且准确的先序序列AB##CD###,两次准确输入后出现的结果也是准确无误的:

>>>output1
请输入不超过20位的先序序列,*”表示空节点:
AB**CD***
您输入的先序序列为:AB**CD***,请再次输入一遍来进行遍历:
AB**CD***
先序遍历序列是: A B C D 
中序遍历序列式: B A D C 
后序遍历序列是: B D C A 
Program ended with exit code: 0

2.第二种输入:较为复杂的先序序列(因为本次代码设计的是20位以内的先序序列,但其实只要改动一处代码便可实现任意多位,但是笔者想这19位的先序序列也足以证明代码本身的准确性了)先序序列为:AB##CDE##F##GHI####,可以看到,输出无误:

>>>output2
请输入不超过20位的先序序列,*”表示空节点:
AB**CDE**F**GHI****
您输入的先序序列为:AB**CDE**F**GHI****,请再次输入一遍来进行遍历:
AB**CDE**F**GHI****
先序遍历序列是: A B C D E F G H I 
中序遍历序列式: B A E D F C I H G 
后序遍历序列是: B E F D I H G C A 
Program ended with exit code: 0

3.第三种输入:调试鲁棒性,从上文介绍的whetherCan()函数中可以知道:
(1)#的数量在总体上等于除#字符以外的字符数+1.
(2)除去最后一个字符,#又在整体上保持着<=其他字符数量

>>>output3
请输入不超过20位的先序序列,*”表示空节点:
AB**
先序序列AB**无法构建一个二叉树,请重新输入:
AB*
先序序列AB*无法构建一个二叉树,请重新输入:
AB
先序序列AB无法构建一个二叉树,请重新输入:
AB***
您输入的先序序列为:AB***,请再次输入一遍来进行遍历:
AB***
先序遍历序列是: A B 
中序遍历序列式: B A 
后序遍历序列是: B A 
Program ended with exit code: 0

第四种输入:同3,也是在验证鲁棒性,就不再赘述。先序序列:ABC##DE#G##F###

>>>output4
请输入不超过20位的先序序列,*”表示空节点:
ABC**DE*G**F***
您输入的先序序列为:ABC**DE*G**F***,请再次输入一遍来进行遍历:
ABC**DE*G**F***
先序遍历序列是: A B C D E G F 
中序遍历序列式: C B E G D F A 
后序遍历序列是: C G E F D B A 
Program ended with exit code: 0

5.第五种输入:在whetherCan()函数成功运行之后,在CreatBiTree()函数中用户输入错误。这里是笔者本条代码的一个败笔,这里本想用strcmp函数进行一个再次确认的,但是想不到什么好方法将主函数中的字符串放置在CreatBiTree()函数中进行比较,所以这一鲁棒性没有完善:

>>>output5
请输入不超过20位的先序序列,*”表示空节点:
AB**CD***
您输入的先序序列为:AB**CD***,请再次输入一遍来进行遍历:
AB**CD**
**
先序遍历序列是: A B C D 
 
中序遍历序列式: B A D C 
 
后序遍历序列是: B D 
 C A 
Program ended with exit code: 0

6.第六种输入:原正确的先序序列应为:AB##CD###
在第一次的输入中故意多输入一个*,这个是用户再输入时常犯的错误,可以看到,whetherC()函数几乎已经完善了。

>>>output6
请输入不超过20位的先序序列,*”表示空节点:
AB**CD****
先序序列AB**CD****无法构建一个二叉树,请重新输入:
AB**CD***
您输入的先序序列为:AB**CD***,请再次输入一遍来进行遍历:
AB**CD***
先序遍历序列是: A B C D 
中序遍历序列式: B A D C 
后序遍历序列是: B D C A 
Program ended with exit code: 0

四、编程时遇到的问题

鲁棒性怎么完善?多余字符怎么处理?函数总是不正常运行怎么办?
刚刚拿到这个实验题目,我想的就是三序遍历都和简单啊,那鲁棒性应该问题不大吧!可是真当我开始下手去实现,三序遍历是很快,毕竟都讲过,可是这个鲁棒性真的让人头大啊。别的不说,但是在哪一块代码区实现鲁棒性的完善,就让我折腾半天了,一开始想的是在CreatBiTree()函数中给判断,但是CreatBiTree()代码块还有子代码块啊(左右孩子的赋值),这我一下就头大了,于是我关了电脑,在草稿本上列出下面的分析图(图4-1):
这下才终于想通,便有了上面的两点结论以及whetherCan()函数。但是whetherCan()虽然是想到了,但是完善的过程真的是艰辛,因为whetherCan()函数最后需要回车才能给出结果,就是因为这个回车,以及CreatBiTree()中一开始的字符读入,才出现了主函数中夹在上面两个函数中间的字符读入,目的就是把这个多余的回车“吃掉”。所以真正的代码其实是有很多的注释,一下的代码分别是是whetherCan()的真容,以及CreatBiTree()里不断地调试,还有夹在这两个函数和中间的多余回车的处理:

图4-1

//whetherCan()的真容
int whetherCan(char str[]) 
//   printf("%d", strlen(str));
   int countNode=0, countNull=0;
   int i;
   int lens=strlen(str);
//   printf("%d", lens);
   if(lens==1) return 0;
   for(i=0; i<lens-1; i++) 
      if(str[i] == '*')
         countNull += 1;
      else
         countNode += 1;
//      printf("countNode:%d\\n", countNode);
//      printf("countNull:%d\\n", countNull);
      if(countNode<countNull) return 0;
//      countNull += 1;
   
   if(countNode != countNull) return 0;
   return 1;


//CreatBiTree()里不断地调试
int CreatBiTree(BiTree &T) 
//   printf("进来Tree了\\n");
   char ch;
   scanf("%c", &ch);
//   scanf("%c", &ch);
//   printf("你不要跳过我啊\\n");
//   char ch = getchar();
   if(ch=='*') T = NULL;
   else 
      if(!(T=(BiTNode*)malloc(sizeof(BiTNode)))) exit(-2);
      T->data = ch;
//      printf("还没输入就进到if里了");
      CreatBiTree(T->lchild);
      CreatBiTree(T->rchild);
   
//   scanf("%c", &ch);
   return 1;


//夹在这两个函数和中间的多余回车的处理
   while (!whetherCan(str)) 
      printf("先序序列%s无法构建一个二叉树,请重新输入:\\n", str);
      scanf("%s", str);
   
   printf("您输入的先序序列为:%s,请再次输入一遍来进行遍历:", str);
   char c = getchar();
//   printf("%c", c);
   printf("\\n");
   
   BiTree T;
   CreatBiTree(T);

五、总结

鲁棒性的完善过程有意思又有难度,本次实验做完,我想在以后的写代码路上笔者应该会对鲁棒性有更深的印象以及实现的渴望。本次实验的一个缺陷在于:在运行到CreatBiTree()函数时,如果用户没有准确输入,也不会跳出来提示,这一块的完善方法在上文有提及,但是真正想做可能需要等笔者复习复习以前的知识,这个漏洞记着了!

清新脱俗的源代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct BiTNode 
   char data;
   struct BiTNode *lchild, *rchild;
 BiTNode, *BiTree;

int whetherCan(char str[]) 
   int countNode=0, countNull=0;
   int i;
   int lens=strlen(str);
   if(lens==1) return 0;
   for(i=0; i<lens-1; i++) 
      if(str[i] == '*')
         countNull += 1;
      else
         countNode += 1;
      if(countNode<countNull) return 0;
   
   if(countNode != countNull) return 0;
   return 1;


int CreatBiTree(BiTree &T) 
   char ch;
   scanf("%c", &ch);
   if(ch=='*') T = NULL;
   else 
      if(!(T=(BiTNode*)malloc(sizeof(BiTNode)))) exit(-2);
      T->data = ch;
      CreatBiTree(T->lchild);
      CreatBiTree(T->rchild);
   
   return 1;


int Visit(char  e) 
   printf("%c ", e);
   return 1;


int PreOrderTraverse二叉树的前序中序后序层次遍历的原理及C++代码实现

设计鲁棒性的方法:输入一个链表的头结点,逆序遍历打印该链表出来

二叉树的前中后遍历以及层序遍历操作(C语言)

二叉树的三种遍历非递归实现

第2节 二叉树的基本操作(递归实现)

Python 二叉树的创建和遍历、重建