实验报告:二叉树创建与遍历
一、问题描述
二叉树是一种实用范围很广的非线性结构,一棵非空二叉树有也只有一个根结点,每个结点最多有两个子树,我们称为左子树与右子树,当一个结点的左、右子树都是空的时,沃恩称此结点为叶子结点。
二叉树有一些很好的性质,这里不再赘述。考虑如何存储一棵树,本实验选择使用链式存储结构——二叉链表;如果事先知道需要存储的二叉树是满二叉树或者完全二叉树,则可以考虑使用顺序存储,否则将浪费大量的存储空间。
对于一棵既成的二叉树,有三种遍历方式——先序、中序与后序。可以证明,一棵形态固定的二叉树与先序遍历、后序遍历生成的序列是1-1对应的,基于这一点,我们可以从先序序列或后序序列出发得到唯一与之对应的二叉树。这是本实验创建二叉树的理论基础。
值得注意的是,一个中序遍历序列与二叉树并不是1-1对应的关系,也就是说一个中序序列对应的二叉树的形态并不固定,即使像本实验中这样添加了虚空结点也不是1-1对应的,所以中序列并不能够生成一棵二叉树。
本实验主要给出先序创建二叉树,以及中序遍历该树。
二、数据结构——二叉链表
二叉链表是二叉树的链式存储结构,其结构与二叉树的性质有天然的契合。
我们知道,二叉树的结点需要保存的信息有:当前结点的信息、左子树、右子树。基于如此的结构,我们在一个结点处分为数据域和指针域:数据域保存该结点自身的信息;指针域分为左、右指针域,分别指向该结点的左、右子树;有必要时可以加上一个链域指向该结点的父亲结点(双亲结点),当然这样就成了三叉链表。三叉链表与二叉链表没有太大的差别,只是三叉链表在具体操作涉及到寻找结点的祖先时会有优势,比如寻找最近公共祖先(lca),这与本实验无关,不再赘述。
值得注意的是,当结点的某个子树不存在时,指向该结点的指针域应该是NULL的。有个小技巧是,可以人为定义一个“空指针域”,应该指向NULL的时候都指向这个所谓的“空结点”,可以一定程度上避免指针访问越界的错误。
三、算法的设计和实现
这个算法的思想非常简单。本实验可以分为两部分——创建二叉树与遍历二叉树。
1、创建二叉树(以先序序列为例)
树的序列生成是一个递归的过程,所以以遍历序列创建树的时候也需要递归生成这棵树。
以先序序列为例,如果当前标号非空,那么我们得到的就是当前结点的数据,保存该数据后,下一个数据——无论是虚空结点还是真实的数据——是左子树的数据,所以我们将递归到左子树中;如果当前标号是空的,即虚空结点,表示此处不存在结点,返回NULL即可;当前结点的左子树递归返回后进入右子树递归;返回该结点的地址,结束。
可以看出这就是一个模拟生成先序序列的过程,着眼于一个结点来说,就是先访问当前子树的根节点,然后左子树,再右子树,最后返回。
后序序列也可以类似地生成,具体细节将在下面提到。
2、遍历二叉树(以中序遍历为例)
与树的创建相似,二叉树的遍历同样是一个递归的过程。
以中序序遍历为例,如果左子树非空,则递归进入左子树;输出当前结点的标号;如果右子树非空,则递归进入右子树;结束。如果不是需要输出序列,而是想得到这个序列,那么很自然地可以想到栈,将输出标号的操作改为将标号压栈即可,别的操作不改动。
遍历二叉树的意义重大,比如中序遍历一棵排序二叉树,得到的就是保存的有序的序列。
四、预期结果和实验中的问题
1、预期结果:
程序能够根据输入的先序序列(带有虚空结点),正确地生成对应的二叉树,并正确地输出中序遍历序列。下图是一个生成三序的例子。
2、实验中的问题及一些说明:
(1)如果没有虚空结点,能否通过三序得到唯一对应的二叉树?
答案是肯定的。下面将简单地说明如何利用先序序列和中序序列得到对应的二叉树。
A)分析:
对于一个先序序列来说,第一个结点一定是根结点,它的右边是左子树的先序序列,然后是右子树的先序序列。这里的问题在于,无法找到左、右子树序列的分界点,换句话说,仅靠一个先序序列我们无法得到对应的二叉树。
这时我们再对中序序列的构成分析,我们可以在中序序列中找到根结点(根结点的标号已经在先序序列中得到了),它的左边是左子树的中序序列,右边是右子树的中序序列。
这时候我们发现,这里出现了一个子结构。因为一棵树(子树)的三序序列长度是相等的,所以我们可以得到左、右子树的先序序列,从而对于任一子树来说,我们得到了它的先序序列和中序序列,问题转换成了相似的子问题。
B)实现:
a)先序序列第一个结点找到根结点;
b)在中序序列中找到该结点(此处可以顺序查找,找到结束即可;当数据量比较大时推荐使用二分法查找),得到左、右子树的结点数,从而分别得到左、右子树对应的先序序列和中序序列;
c)分别递归进入左、右子树,建树。递归的边界是叶子结点,它的先序和中序都是长度为1的自己的标号。
(2)包含虚空结点的后序序列如何生成对应的二叉树?
有一个简单的办法,从后往前扫描后序序列,相当于是顺序为“根结点-右子树-左子树”生成的序列。这个与先序序列生成对应二叉树是一个镜面的过程,不再赘述。
(3)中序序列能够生成唯一对应的二叉树吗?
答案是否定的。中序序列与二叉树并不是1-1对应的,下图是一个例子:
这两个都是合法的二叉树,两者的中序序列都是CBA,即使是加上了虚空结点,得到的中序序列也都是$C$B$A$($表示空格,也就是虚空结点),所以中序序列与二叉树不是1-1对应的,当然不能通过中序序列生成一棵唯一的二叉树。
(2)能否不使用递归操作?
这个问题可以归结为研究递归操作的本质。
递归操作实际上是一个对栈的操作,以先序遍历二叉树为例:将当前结点压栈;如果左子树非空,将左子树压栈;如果右子树非空,将右子树压栈;如果左、右子树都是空的,即当前结点为叶子结点,弹栈;当结点的左右子树都完成了操作,弹栈。
于是我们得到了一个有意思的结果:所有的递归算法是可以有非递归调用的实现方式的(这里的非递归调用实现方式指的是递归调用函数,其本质应该还是递归)。当然,也不排除一些递归解决的问题同样可以用递推解决,比如汉诺塔问题,这里不赘述。
附:c++源代码:
1 /* 2 项目:创建二叉树,三序遍历 3 作者:张译尹 4 */ 5 #include <iostream> 6 #include <cstdio> 7 8 using namespace std; 9 10 template <class T> class BiTree 11 { 12 private: 13 T data; //根结点的标志 14 BiTree *lch, *rch; //左右儿子 15 public: 16 void InitBiTree() //初始化二叉链表 17 { 18 data = 0; 19 lch = rch = NULL; 20 } 21 BiTree* CreatBiTree() //先序(带虚空结点)递归建立二叉链表 22 { 23 BiTree <char> *rt = new BiTree; 24 rt -> InitBiTree(); 25 char ch; 26 scanf("%c", &ch); 27 if(ch != ' ') //非空指针,递归建树 28 { 29 rt -> data = ch; 30 rt -> lch = CreatBiTree(); 31 rt -> rch = CreatBiTree(); 32 } 33 else 34 { 35 delete rt; 36 rt = NULL; 37 } 38 return rt; 39 } 40 void PreOrderTraverse() //递归先序遍历 41 { 42 printf("%c", data); 43 if(lch) 44 lch -> PreOrderTraverse(); 45 if(rch) 46 rch -> PreOrderTraverse(); 47 } 48 void InOrderTraverse() //递归中序遍历 49 { 50 if(lch) 51 lch -> InOrderTraverse(); 52 printf("%c", data); 53 if(rch) 54 rch -> InOrderTraverse(); 55 } 56 void PostOrderTraverse() //递归后序遍历 57 { 58 if(lch) 59 lch -> PostOrderTraverse(); 60 if(rch) 61 rch -> PostOrderTraverse(); 62 printf("%c", data); 63 } 64 }; 65 66 int main() 67 { 68 BiTree <char> *Tree; 69 //Tree -> InitBiTree(); 70 71 printf("请输入二叉树的先序列,结点用字母表示,空域用空格表示。 "); 72 Tree = Tree -> CreatBiTree(); 73 74 printf("先序序列: "); 75 Tree -> PreOrderTraverse(); 76 printf(" "); 77 78 printf("中序序列: "); 79 Tree -> InOrderTraverse(); 80 printf(" "); 81 82 printf("后序序列: "); 83 Tree -> PostOrderTraverse(); 84 printf(" "); 85 86 return 0; 87 }