实验五二叉树的递归及非递归的遍历及其应用(必做,设计性实验)
- 实验目的
熟练掌握二叉树的二叉链表存储结构的C语言实现。掌握二叉树的基本操作-前序、中序、后序遍历二叉树的三种方法。了解非递归遍历过程中“栈”的作用和状态,而且能灵活运用遍历算法实现二叉树的其它操作。
- 实验内容
(1)二叉树的二叉链表的创建
(2)二叉树的前、中、后序遍历的递归算法和非递归算法的实现
(3)求二叉树中叶子结点数目的递归算法。
(4)编写求二叉树深度的递归算法。
(5)编写判断二叉树是否相似的递归算法
(6)编写求二叉树左右子树互换的递归算法
(7)编写二叉树层序遍历的算法
(8)编写判断二叉树是否是完全二叉树的算法
创建二叉树的二叉链表结构、进行先、中、后序的递归函数显示不同遍历序列,结合习题了解二叉树遍历的应用实例。编写前、中、后、层序遍历的非遍历函数,注意用以前做过的栈和队列作辅助存储结构。掌握用遍历算法求解其它问题的方法。
- 问题描述
(说明你选做的题目及要求)
二叉树的二叉链表的创建,二叉树前、中、后序遍历的递归算法和非递归算法,求叶子结点数目的递归算法,求二叉树深度的递归算法,判断二叉树是否相似的递归算法,求二叉树左右子树互换的递归算法,二叉树层序遍历的算法,判断二叉树是否是完全二叉树,要求用C语言实现,熟练掌握二叉链表存储结构,谅解非递归遍历过程的栈的应用,灵活运用二叉树的其他操作。
- 数据结构定义
(说明你算法中用到的数据结构、数据类型的定义)
(1)数据结构:
①二叉树:树是由n个结点组成的有限集合。其中,如果n=0,他是一棵空树,这是树的特例。如果n>=0,这n个结点中存在一个结点作为树的根节点,简称为根(root),其余结点可分为m个互不相交的有限集,其中每一棵子集本身又是一棵符合本定义的树,称为根root的子树。二叉树是另一种树形结构,它的特点是每个结点至多只有两棵子树,并且二叉树的子树有左右之分,其次序不能任意颠倒。采用的是链式存储结构。
②栈:一种只能在一端进行插入或删除操作的线性表—栈。表中允许插入、删除操作的一端称为栈顶。栈顶的当前位置是动态的,栈顶的当前位置由一个称为栈顶指针的位置指示器指示,栈的主要特点是后进先出。采用的存储结构是链式存储。
③循环队列:循环队列是只允许一端进行插入,另一端进行删除元素的线性表,具有先进先出的特点。采用的存储结构是顺序存储。
(2)数据类型定义:
①二叉树:*lchild是左孩子指针,*rchild是右孩子指针,data用来储存结点的值,二叉树结点数据类型名为BiTNode
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode ,*BiTree;
②栈:stacksize表示栈当前可使用的最大容量。Base是栈底指针,top作为栈顶指针。
typedef struct SqStack{
BiTree base;
BiTree top;
int stacksize;
}SqStack;
③循环队列:base用来存储初始化的动态分配的存储空间的地址,front指向队列的第一个元素,rear指向队列的最后一个元素的下一个元素,队列类型名定义为SqQue
typedef struct SqQue{
BiTree base;
int front,rear;
}SqQue;
- 算法思想及算法设计
(先文字说明算法的思想,然后给出类C语言算法)
(1)二叉树的二叉链表的创建
①算法思想:采用递归的思想前序创建二叉树。首先输入根节点,如果根节点为空,则树为空;否则调用函数自身创建以根节点左孩子为根节点的子树,再调用函数自身创建以根节点右孩子为根节点的子树,直到所有左右孩子均为空。
②类C语言算法:
Status CreatBiTree(BiTree &T){
scanf("%c",&ch);
if(ch==' ') T=NULL; //根节点为空则树空
else{
if(!(T = (BiTree)malloc(sizeof(LEN)))) exit(OVERFLOW);
T->data = ch;
CreatBiTree(T->lchild); //创建以根节点左孩子为根节点的子树
CreatBiTree(T->rchild); //创建以根节点右孩子为根节点的子树
}
return OK;
}
(2)二叉树的前、中、后序遍历的递归算法和非递归算法的实现,此处以中序遍历的递归算法和非递归算法为例,前序和后序遍历思想类似
①算法思想:中序递归遍历:若树非空,调用函数自身中序遍历左子树,直到左子树遍历完成,再输出根节点,最后再次调用函数自身中序遍历右子树,直到右子树遍历完成。
中序非递归遍历:借助栈来实现非递归中序遍历。首先初始化一个栈;其次,如果二叉树的根节点不是空,根节点数据入栈,遍历左子树,直到此时的左孩子为空,然后根结点退栈,访问根节点;然后再遍历右子树,直到栈为空且结点为空时,遍历完成。
②类C语言算法:
Status InOrderTraverse(BiTree T,Status(*Visit)(TElemType e)){ //递归中序遍历
if(T){ //非空
if(InOrderTraverse(T->lchild,Visit))
if(Visit(T->data))
if(InOrderTraverse(T->rchild,Visit))
return OK;
return ERROR;
}
else return OK;
}
Status InOrderTraverse2(BiTree T,Status(*Visit)(TElemType)){ //非递归中序遍历
if(T){
p = T; InitStack(s); //初始化一个栈用来存放树结点
while(p||s.base!=s.top){//当结点不为空或者栈不空的时候=不是空树
if(p){ Push(s,*p); p = p->lchild; }//树的根结点数据入栈,遍历左子树
else{ //根指针退栈,访问根节点,遍历右子树
Pop(s,p); if(!Visit((*p).data)) return ERROR;
p = p->rchild;
}
}
return OK;
}
return ERROR;
}
(3)求二叉树中叶子结点数目的递归算法。
①算法思想:采用递归的思想,用初值为0的num来记录叶子结点的数目,首先,如果二叉树非空且没有左孩子和有孩子,则叶子节点数为1;然后调用函数自身计算以根节点左孩子为根节点的子树的叶子节点数和以根节点右孩子为根节点的子树的叶子节点数,直到左右子树为空,调用结束,依层返回;最后返回num的值即为二叉树叶子结点的个数。
②类C语言算法:
int LBNode(BiTree T){
num = 0;
if(T){
if(T->lchild==NULL&&T->rchild==NULL) num++;
else{ LBNode(T->lchild); LBNode(T->rchild); }
}
return num;
}
(4)编写求二叉树深度的递归算法。
①算法思想:采用递归的思想,从根节点开始,最大深度为 Math.max(左子树的最大深度,右子树的最大深度比较)+ 1;左子树的最大深度是调用函数自身,即相对于左子树的根节点来说Math.max(左子树的最大深度,和右子树的最大深度比较)+ 1,右子树最大深度同理,直到左右子树为空,调用函数结束,依层返回,最后即可得出二叉树深度。
②类C语言算法:
int DepthBiTree(BiTree T){
if(T){
ldepth = DepthBiTree(T->lchild);
rdepth = DepthBiTree(T->rchild);
return (ldepth>rdepth?ldepth:rdepth)+1;
}
return 0;//如果是空树,结点数是0
}
(5)编写判断二叉树是否相似的递归算法
①算法思想:采用递归的思想,首先如果两个树都是空树则相似返回1;如果两棵树一棵为空树,一棵不是空树,那么不想似返回0;如果两棵树都不是空树,那么调用函数本身判断两棵树的左子树和右子树是否相似,直到左右子树为空,调用结束,依层返回,如果左右子树均相似,那么这两棵树相似返回1,否则不相似返回0。
②类C语言算法:
Status SimBiTree(BiTree T1,BiTree T2){
if(T1==NULL&&T2==NULL) return TRUE; //二者都是空树
else if(T1==NULL||T2==NULL) return FALSE; //其中一个为空 另一个不为空
else{
int l1 = SimBiTree(T1->lchild,T2->lchild);//左支树是否相似
int l2 = SimBiTree(T1->rchild,T2->rchild);//右支树是否相似
return (l1&&l2);
}
}
(6)编写求二叉树左右子树互换的递归算法
①算法思想:采用递归的思想,若二叉树不是空树,借用中间变量temp将左孩子和右孩子互换,再调用函数本身将以根节点左右孩子的为根节点的子树的左右子树互换,子树为空,递归完成,二叉树左右子树互换完成。
②类C语言算法:
Status change(BiTree &T){
if(T){
temp = T->lchild; T->lchild = T->rchild; T->rchild = temp;
change(T->lchild); change(T->rchild);
return OK;
}
}
(7)编写二叉树层序遍历的算法
①算法思想:借助队列实现,首先创建一个循环队列并将根节点如队列;用while循环控制二叉树结点入队列,当队列不空时,队列头的结点出队列,并访问该结点;如果该结点的左孩子非空,则入队列,如果该结点的右孩子非空,则入队列;直到队列为空,二叉树的层序遍历结束。
②类C语言算法:
Status printNodeByLevel(BiTree T,Status (*Visit)(TElemType e)){
if(T){
p = T;
InitSqQue(q); EnSqQue(q,T);
while(q.front!=q.rear){//当队列不为空的时候
DeSqQue(q,p);
Visit(p->data);
if(p->lchild) EnSqQue(q,p->lchild);
if(p->rchild) EnSqQue(q,p->rchild);
}
}
}
(8)编写判断二叉树是否是完全二叉树的算法
①算法思想:叶子节点只能出现在最后一层和倒数第二层,按照层序遍历的方式来判断。首先判断树是否是空树,若是则是完全二叉树;其次判断树是否只有一个根节点,若是则是完全二叉树;若以上情况均不符合,则先让根节点入队列,如果队列非空,则循环执行下列操作,队列头的结点出队列,若该结点为空,则跳出循环,否则不管它有没有左右孩子都让他们入队列;跳出循环后判断遇到空之后队列里还有没有非空的元素,若有则不是完全二叉树,否则是。
②类C语言算法:
Status IsAbsoBiTree(BiTree T){
if(!T) return TRUE; //空树
else if(T->lchild==NULL&&T->rchild==NULL) return TRUE;
else{//有一个或两个孩子
p = T; InitSqQue(q); EnSqQue(q,T);//先将根结点入队列
while(q.front!=q.rear){
DeSqQue(q,p);
if(p->data=' ') break;//如果出队列的数为空就跳出这个出队入队的循环
EnSqQue(q,p->lchild); EnSqQue(q,p->rchild);//不管有没有左右孩子都让他们入队
}
while(q.front!=q.rear){//判断遇到空之后队列里还有没有非空的元素
DeSqQue(q,p);//让队列里的元素一个一个出队列判断
if(p->data) return FALSE;//当出来的元素不为空的时候 说明不是完全二叉树
}
return TRUE;//如果出来的元素全是空说明是完全二叉树
}
}
- 实验代码
(即C语言程序)
详见电子版
- 算法测试结果
(说明测试数据,粘贴实验结果图)
|
①测试用例:abc de g f (不是完全二叉树)
|
②测试用例:124 5 36 (是完全二叉树)
- 分析与总结
(1)算法复杂度分析及优、缺点分析
(说明你编写算法的复杂度,算法的优点和缺点有哪些)
算法复杂度分析:
CreatBiTree(BiTree &T), PreOrderTraverse(BiTree T,Status(*Visit)(TElemType e)),
InOrderTraverse(BiTree T,Status(*Visit)(TElemType e)),
PostOrderTraverse(BiTree T,Status(*Visit)(TElemType e)), LBNode(BiTree T)
DepthBiTree(BiTree T), SimBiTree(BiTree T1,BiTree T2), change(BiTree &T),
InOrderTraverse2(BiTree T,Status(*Visit)(TElemType)),
printNodeByLevel(BiTree T,Status (*Visit)(TElemType e)), IsAbsoBiTree(BiTree T),
main()
算法时间复杂度为O(n)
Visit(TElemType e), InitStack(SqStack &s), Push(SqStack &s,BiTNode e),
Pop(SqStack &s,BiTree &p), InitSqQue(SqQue &q), EnSqQue(SqQue &q,BiTree e),
DeSqQue(SqQue &q,BiTree &p)
算法时间复杂度为O(1)
优点:
①对于二叉树的前、中、后序遍历,二叉树二叉链表的创建,求叶子节点数目,求二叉树的深度,将二叉树左右子树互换,判断两个二叉树是否相似这些操作来说,递归算法是最简单直观、易于理解和实现的方法;
②二叉树的二叉链表灵活性较高,可以动态地进行插入和删除操作;
③使用非递归的方式实现二叉树的遍历,可以借助辅助数据结构(如栈)来保存需要遍历的结点,通过迭代的方式按照定义好的遍历顺序访问二叉树的结点。这种方法消除了函数调用的开销,提高了效率;
④对于二叉树的层序遍历,可以借助队列来实现,层序遍历的算法利用了队列的先进先出特性,具有时间复杂度较低且易于实现的优点。
缺点:
①在二叉树结点数较多时,递归算法可能会导致函数调用堆栈溢出,造成内存消耗较大,针对递归算法可能存在的效率问题;
②对二叉树非递归遍历算法的缺点是代码相对复杂,不如递归算法直观易懂;
③二叉树的二叉链表的存储结构访问节点的效率稍低。
(2)实验总结
(说明你怎么解决实验中遇到的问题,有什么收获)
问题:
①如何实现二叉树的中序非递归遍历
解决办法:中序遍历是遍历左子树、根结点、右子树的顺序,当某一个结点的左子树均访问完成时,需要回到该结点,然后访问该结点的右子树,这意味这需要借助一个工具来记录一路访问的结点,这个问题和迷宫问题有点类似,所以可以借助栈来实现。具体实现是先将根节点入栈,然后循环逐个访问左子树,执行根结点入栈的步骤;当访问到没有左子树的结点时,跳出循环,栈不为空,根结点出栈,访问根结点,再访问右子树,这样就可以实现二叉树的中序非递归遍历。
②如何实现二叉树的层序遍历
解决办法: 所谓二叉树的层次遍历,是指从二叉树的第一层(根节点开始)自上而下逐层遍历,同层内按照从左至右的顺序逐个结点访问。由二叉树层次遍历的要求可知,当一层访问完之后,按该层结点访问的次序,再对各结点的左、右孩子进行访问(即对下一层从左到右进行访问),这一访问的特点是:先访问的结点其孩子也将先访问,后访问的结点其孩子也将后访问,这与队列的操作控制特点吻合,因此在层次遍历的算法中,将应用队列进行结点访问次序的控制。具体实现如下,首先根节点入队,当队列非空时,重复如下两步操作;对头结点出队,并访问出队结点;出队结点的左、右孩子依次入队。
③如何让判断二叉树是否为完全二叉树
解决办法:由于完全二叉树在每一层非空节点都是一个接一个连续分布的,不可能出现两个非空节点之间交叉一个空节点,因此我们可以借助层序遍历判断是否为完全二叉树。具体实现如下,首先通过层序遍历从上往下,从左往右将节点(包括非空节点)放到队列里,在出队列的过程中,如果遇到空节点,则跳出循环;跳出循环后,然后再判断队列中剩下的元素是否有非空节点,有非空节点,非完全二叉树;没有非空节点(全是空节点),完全二叉树。
收获:
①我学会了如何使用二叉链表来创建二叉树。通过定义每个节点包含左子树指针、右子树指针和数据的结构,可以逐个节点地创建二叉树,按照指定的顺序连接节点之间的关系,从而构建出完整的二叉树结构。
②我掌握了前序、中序和后序遍历二叉树的递归算法。通过递归地访问二叉树的根节点、左子树和右子树,可以按照不同的遍历顺序输出二叉树中的节点值。这样的遍历方式对于分析和处理二叉树的结构和特性非常有帮助。
③我还学会了使用非递归的算法来实现前序、中序和后序遍历。通过利用栈这种数据结构,可以模拟递归过程中的栈帧,将遍历的节点依次入栈并弹出,实现非递归方式的遍历。这种方法可以节省递归调用的开销,提高遍历效率。
④在实践过程中,我也学会了如何使用遍历算法解决其他问题。例如,通过修改遍历过程中的访问操作,可以统计出二叉树中叶子节点的数量;通过记录遍历中经过的层数,可以计算出二叉树的深度;通过对比两棵树的遍历序列,可以判断它们是否相似等等。
⑤我还学会了进行二叉树的层序遍历。通过借助队列这种数据结构,可以按照层次的顺序来遍历二叉树的节点,输出每个节点的值。这种遍历方式常用于按层统计树的节点数目、在二叉树中查找某个节点等应用场景。