实验三 二叉树及其应用
0: 背景
这是一次数据结构的实验报告。源代码不另附,将文中散乱代码块合在一起即可。
1:二叉树的创建和遍历
需求分析
通过添加虚结点,为二叉树的每一实结点补足其孩子,再对补足虚结点后的二叉树按层次遍历的次序输入。构建二叉树(不包含虚结点),并增加左右标志域,将二叉树后序线索化。完成后序线索化树上的遍历算法,依次输出该二叉树先序遍历、中序遍历和后序遍历的结果。
输入:以#
代表虚结点,输入字符,层序创建。如:ABC#DEFG##H######
输出:先中后序遍历结果,以字符串表示。如:
ABDHCEFH
, BGDAEHCF
, GDBHEFCA
概要设计
程序中使用了二叉树和队列的数据结构,以及字符串数组结构。涉及到出队列,入队列操作,对字符串数组赋值和取值操作。
主程序:接受输入,层序创建二叉树,先序、中序遍历,之后将其后序线索化,并按照后序线索后序遍历。
子函数中后序遍历用到了另一个子函数:求该结点的父结点。
详细设计
数据类型:二叉树和线索
//定义线索
typedef enum{
link, thread
}ptrtag;
//定义线索二叉树结点
typedef struct node{
char data;
ptrtag ltag;
ptrtag rtag;
struct node *lchild;
struct node *rchild;
}node,*Btree;
主程序:
int main(void){
Btree T;
T = CreatBtree();
PreOrder(T);
puts("\n");
InOrder(T);
puts("\n");
PosTreeLink(T); //线索化
PostThreadOrder(T); //后序遍历
printf("\n");
return 0;
}
模块:
-
二叉树创建
/层序创建二叉树 Btree CreatBtree(){ Btree T,que[N],new; int front = 0, rear = 0; char ch = getchar(); if(ch == '#') T = NULL; else{ T = (Btree)malloc(sizeof(node)); T->data = ch; T->lchild = NULL; T->rchild = NULL; T->ltag = link; T->rtag = link; que[rear++] = T; } while (front != rear) { ch = getchar(); if(ch == '\n') break; if(ch == '#'){ que[front]->lchild = NULL; } else{ new = (Btree)malloc(sizeof(node)); new->data = ch; new->ltag = link; new->rtag = link; new->lchild = NULL; new->rchild = NULL; que[front]->lchild = new; que[rear++] = new; } ch = getchar(); if(ch == '#'){ que[front]->rchild = NULL; } else{ new = (Btree)malloc(sizeof(node)); new->data = ch; new->ltag = link; new->rtag = link; new->lchild = NULL; new->rchild = NULL; que[front]->rchild = new; que[rear++] = new; } front++; } return T; }
-
后序线索化
//全局变量,指向前一个结点 Btree Prev = NULL; //后序线索化 void PosTreeLink(Btree Root) { if (Root == NULL) { return; } PosTreeLink(Root->lchild); PosTreeLink(Root->rchild); if (Root->lchild == NULL) { Root->lchild = Prev; Root->ltag = thread; } if (Prev != NULL && Prev->rchild == NULL) { Prev->rchild = Root; Prev->rtag = thread; } Prev = Root; }
-
先序和中序遍历(递归)
//先序遍历 void PreOrder(Btree T){ if(T){ printf("%c ", T->data); PreOrder(T->lchild); PreOrder(T->rchild); } } //中序遍历 void InOrder(Btree T){ if(T){ InOrder(T->lchild); printf("%c ", T->data); InOrder(T->rchild); } }
-
寻找双亲
//找双亲 void Parent(Btree root,Btree *child) { Btree temp = *child; if (*child == root) { return; } if (root) { if (root->lchild == *child || root->rchild == *child) { *child = root; return; } if (root->ltag == link) { Parent(root->lchild, child); } if (temp == *child && root->rtag == link) { Parent(root->rchild, child); } } }
-
后序遍历
//后序遍历 void PostThreadOrder(Btree T) { Btree cur = T; Btree lastvisited = NULL; while (cur) { //找最左边结点 while (cur&&cur->ltag != thread && cur->lchild != lastvisited) { cur = cur->lchild; }//当cur是通过其左孩子找到的即没有右孩子,不对cur进行重复找最左边的叶子节点 if (cur && (cur->rtag == thread))//当cur无右孩子时,输出cur { printf("%c ", cur->data); lastvisited = cur; } if (cur == T&&cur->rchild== NULL)//当根节点被访问时,遍历完成 { printf("%c ", cur->data); return; } cur = cur->rchild; //前往后继结点 while (cur && cur->rchild == lastvisited) //cur前驱是自己右孩子 { printf("%c ", cur->data); lastvisited = cur; Parent(T , &cur); //寻找cur的根节点 if (cur == lastvisited) { return; } } } }
主程序调用CreateBtree,PostThreadOrder,preorder,inorder,postreelink。
PostThreadOrder调用parent。
调试分析
-
编写程序前,思考前和中序遍历,均可以使用简单的递归完成。后序线索化可以在后序遍历基础上修改而成。在得到后序线索化树以后,思考如何处理前驱和后继关系。除了使用三叉链表外,还可以使用寻找父结点的函数来完成。当一个结点两个孩子都存在时,应该找它的父结点。
-
调试过程中,试图使用中-右-左的模式来完成倒序后序遍历,但由于各种原因停止,改为找父结点。同时在使用过程中发现结束条件可以改成前驱结点是树根。
-
写完程序以后尝试使用各种极端情况来测试,发现在全部为左子树和交替为左子树时出现了bug。原因是没有判断右孩子是否存在和回溯条件不清楚。只输入一个结点时也会出现问题。多次完善以后才完成后序线索化的模块。
-
遍历时递归调用自身不能写错,否则会产生不理想的结果。
-
本算法创建二叉树时遇到了困难。找到新方法是将前一层结点存入队列,每次往后编入两个结点作为左右孩子,编组完成后将前一层结点出队列,从而完成结点的创建。
-
本算法创建二叉树均适用单层结构,没有高次方复杂度的操作。前中序遍历操作也是如此。空间复杂度为o(n)级别。后序遍历使用找父结点的操作,有一个o(n^2/2)的操作,时间复杂度为多项式级别,比较简单。如果可以使用三叉链表,时间复杂度将降低至o(n)级别。
用户使用说明
- 本程序为命令行操作,可以使用支持C语言的设备运行。
- 进入程序后可以输入字符串,以#作为空结点标志,注意输入的结果字符串必须合法而且正确,否则无法得出正确结果。
- 输入后回车,程序将分行输出前、中、后序遍历的结果。行与行之间空一行,输出完成后自动退出。
测试结果
Input:
ABC#DEFG##H######
Output:
A B D H C E F H
B G D A E H C F
G D B H E F C A
Input:
AB#C#D##E##
Output:
A B C D E
D E C B A
E D C B A
Input:
A#BC##DE##F##
Output:
A B C D E F
A C E F D B
F E D C B A
Input:
ABCD##E##F###
Output:
A B D C E F
D B A C F E
D B F E C A
测试结果满足预期,可以认为程序运行正常。
2:表达式树
###需求分析
输入合法的波兰式(仅考虑运算符为双目运算符的情况),构建表达式树,分别输出对应的中缀表达式 (可含有多余的括号)、逆波兰式和表达式的值,输入的运算符与操作数之间会用空格隔开。
输入输出样例:
Input:
-+231 //波兰式
Output:
(2+3)-1 //中缀表达式 23+1- //逆波兰式
4 //求值
选做要求(二选一即可):
-
输出的中缀表达式中不含有多余的括号。 例如在上面的样例中,期望的输出结果应该是
2=3-1
。 -
输入逆波兰式,输出波兰式、中缀表达式(可含有多余的括号)和表达式的值。
本人选了第二个要求。
概要设计
本实验中,使用了二叉树和栈的数据结构。涉及到初始化栈,出栈和入栈操作。二叉树则涉及到创建二叉树结点,修改二叉树的指针域,二叉树的三种遍历等。
主程序:判断用户需求。按照输入逆波兰式或者波兰式来进行创建树,输出表达式和计算结果操作。调用了树的创建,树的遍历,树的求值函数。
子函数:创建树使用了子函数judge,判断字符是否为操作符。遍历树则使用了自身递归。
详细设计
-
定义树结点
typedef struct tree{ char data; tag tag; struct tree *lchild; struct tree *rchild; }treenode,*btree;
-
定义tag(标记结点储存的数据是什么类型)
typedef enum tag{ num,op,mult }tag;
-
定义栈
typedef struct stac{ int bottom,top; btree data[N]; }stac;
-
树的操作:
-
创建结点
btree CreNode(char data){ btree T = (btree)malloc(sizeof(treenode)); T->data = data; T->rchild = NULL; T->lchild = NULL; return T; }
-
创建树
btree CreateBtree() { btree root; stac s; InitStac(s); char *ch[N], c; int i = 0; c = ' '; while (c == ' ') { ch[i] = (char *) malloc(N * sizeof(char)); scanf("%s", ch[i]); i++; c = getchar(); } while (i--) { int len = (int) strlen(ch[i]) - 1; if (len == 0) { char cur = ch[i][0]; if (!judge(cur)) { btree t = CreNode(cur); t->tag = num; Push(&s, t); } else { btree lc, rc; Pop(&s, &lc); Pop(&s, &rc); btree t = CreNode(cur); t->tag = op; t->lchild = lc; t->rchild = rc; Push(&s, t); } } else { char cu[N] = {0}; for (int j = 0; j <= len; j++) { cu[j] = ch[i][j]; } char cur = (char)atoi(cu); btree t = CreNode(cur); t->tag = mult; Push(&s, t); } } Pop(&s, &root); return root; }
-
逆波兰式创建树
btree ExtraCreate(){ btree root; stac s; InitStac(s); char *ch[N], c; int i = 0; c = ' '; while (c == ' ') { ch[i] = (char *) malloc(N * sizeof(char)); scanf("%s", ch[i]); i++; c = getchar(); } int n = i; for ( i = 0; i < n; i++) { int len = (int) strlen(ch[i]) - 1; if (len == 0) { char cur = ch[i][0]; if (!judge(cur)) { btree t = CreNode(cur); t->tag = num; Push(&s, t); } else { btree lc, rc; Pop(&s, &rc); Pop(&s, &lc); btree t = CreNode(cur); t->tag = op; t->lchild = lc; t->rchild = rc; Push(&s, t); } } else { char cu[N] = {0}; for (int j = 0; j <= len; j++) { cu[j] = ch[i][j]; } char cur = (char)atoi(cu); btree t = CreNode(cur); t->tag = mult; Push(&s, t); } } Pop(&s, &root); return root; }
-
前缀表达式
void PrintPreTree(btree T){ if(T == NULL) return; else{ if ( T->tag == mult ) { printf("%d ",T->data); } else printf("%c ", T->data); PrintPreTree(T->lchild); PrintPreTree(T->rchild); } }
-
后缀表达式
void PrintPosTree(btree T){ if(T == NULL) return; else{ PrintPosTree(T->lchild); PrintPosTree(T->rchild); if ( T->tag == mult ) { printf("%d ",T->data); } else printf("%c ", T->data); } }
-
中缀表达式(没有删除多余括号)
void PrInTree(btree T){ if(T == NULL) return; else { if (T->tag == op) { printf("("); PrInTree(T->lchild); if (T->tag == mult) { printf("%d", T->data); } else { printf("%c", T->data); } PrInTree(T->rchild); printf(")"); } else { if (T->tag == mult) { printf("%d", T->data); } else { printf("%c", T->data); } } } }
-
计算树的值
int value(btree t){ int a,b; if (t ->tag == op){ a = value(t->lchild); b = value(t->rchild); return calc(a,t->data,b); } else{ if(t->tag == num){ return atoi(&t->data); } else { return (int) t->data; } } }
-
-
栈的操作
-
初始化栈
stac InitStac(stac S){ S.bottom = S.top = 0; S.data[S.top] = NULL; return S; }
-
压栈
void Push(stac *s, btree T){ s->top++; s->data[s->top] = T; }
-
出栈
void Pop(stac *s, btree *T){ if (s->top == s->bottom) return; *T = s->data[s->top]; s->top--; }
-
-
判断操作(判断是否为操作符号)
int judge(char x){ if (x == '+'||x == '-'||x == '*' || x == '/') return 1; else return 0; }
调试分析
- 实现过程中,最先考虑的是如何输入带空格的字符串。再经过大量尝试和搜索之后,发现可以使用字符串数组,以空格作为分隔符号,从而达到输入字符串的作用。在输入字符串以后,还需要考虑数字的位数。因此在树上加入tag结构域,用于区分是多位数字还是单位数字还是操作符号。否则会因为分不清是否为ascii码还是数值而输出计算错误。
- 可以用栈先入后出的特点,实现前后缀表达式的计算。这对计算机来说是非常方便的。而且表达式树是一个非常好的方式,因为可以从表达式树中获取前中后缀表达式,也可以用遍历的方法来创建和计算表达式值。
- 由于没有使用一个字符一个字符的储存方式,导致每次都要判断是否为字符。导致程序较长。使用一个字符一个字符的处理方法可能会更好。
- 指针在这个过程中起到了非常大的作用,使用指针可以简化程序,对指针进行加减就可以指向不同的结果。相比多维数组更加方便。
- 判断递归结束的条件一直是难点,同时加不加括号也比较难想清楚。
- 本算法创建只涉及出入栈,遍历等操作也只与输入个数有关。因此时间和空间复杂度为o(n)级别,复杂度为多项式级别,可以接受。
用户使用说明
- 本程序为命令行操作,可以使用支持C语言的设备运行。
- 进入程序后在提示下输入A和B选择输入模式。A为前缀表达式,B为后缀表达式。选择之后可以输入字符串,以空格作为分隔标志,注意输入的结果字符串必须合法而且正确,否则无法得出正确结果。
- 如果选择A,输入后回车,程序将分行输出中缀表达式,后缀表达式和表达式计算的结果。如果选择B,输入后回车,程序将分行输出中缀表达式,前缀表达式和表达式计算的结果。输出完成后自动退出。输出每行间空一行。
测试结果
Input:
A
/ + 15 * 5 + 2 18 5
Output:
((15+(5*(2+18)))/5)
15 5 2 18 + * + 5 /
23
Input:
A
- / 15 - 2 7 10
Output:
((15/(2-7))-10)
15 2 7 - / 10 -
-13
Input:
A
- 20 + 11 13
Output:
(20-(11+13))
20 11 13 + -
-4
Input:
A
/ 32 * 8 2
Output:
(32/(8*2))
32 8 2 * /
2
测试结果与预期相符,可以认为程序设计基本完善。
思考:
-
分别给定先序序列和中序序列、中序序列和后序序列、先序序列和后序序列,是否能够唯一确定一颗二叉树?
答:先序和后序序列是不能确定二叉树的,因为后序和先序缺少根结点信息,无法判断结点之间的关系。中序可以提供根的关系。
-
**表达式树的先序序列、中序序列和后序序列与波兰式、中缀表达式和逆波兰式之间有什么联系? **
先序序列其实就是波兰式,后序就是逆波兰式。中序遍历加上括号之后就是中缀表达式。
-
给定波兰式、中缀表达式或逆波兰式中的任意一种,是否能够唯一确定一颗表达式树?是什么造成了表达式树与二叉树之间的这种区别?
都可以确定一颗二叉树。原因是,表达式中的操作符和操作数是有区别的,这可以作为区别根结点和叶结点的关键信息,而普通的二叉树本身并不含有这样的信息。因此给定表达式是可以创建树的。