一、实验目的
使用二叉树完成表达式的存储和计算,要求
* 能够含有( )、+、-、*、/等运算符的实数表达式计算功能
* 能够处理小数和负数
* 能够处理求余运算符(%)
* 能够处理乘方(^)
* 能够处理exp、sin、cos、tan、ctan等常见函数
* 能够打印包含括号的中缀表达式
二、实验内容
二叉树是计算机中的重要数据结构,通过树,可以完成许多复杂的工作,而且由于树具有递归的结构,适合递归的算法。
二叉树实现计算器本来是一个比较复杂的过程,之前我们就用过栈来实现计算器,总体来说比较简单,老师为了考察我们对树这个结构的理解,就给我们提出了这个难题,总体来说还是很有难度的。
三、实验思路
首先,整个代码分成了五个文件, StrProcess.c是对字符串的处理代码,Stack.c是实现了简单栈的数据结构,因为括号的处理需要用到栈的数据结构,Tree.c则是定义了树的节点的类型文件,display.c则是最后整个树的表达式输出和结果输出,main.c则是表达了整个代码的测试文件。实验的思路则是先用自己写的my_getline获取一行的代码,利用StrProcess函数进行字符串处理,当遇到左括号的时候把左括号入栈,之后跳过所有括号内的符号或者数字,直到遇到第一个右括号,在扫描字符,如果遇到第一个加号或者减号,把该符号作为头结点。
然后再把整个字符串以第一个加号或者减号作为中间点,分裂到左右子节点,再重复以上的过程,不断分裂,直到递归的字符串中所有的字符都为数字或者小数点,再通过numProcess函数判断字符串为int类型还是float类型,进行转换,放入子节点中。
对于sin、cos、tan等函数,需要通过字符匹配的函数,找到这个函数,在调用math.h库里面对应的函数,将结果存到子节点中。
对于括号的问题,就是当整个字符串的两端是括号的时候,在头结点处设置一个flag,设为1,如果到时候中缀遍历的时候遇到了节点里的flag是1,在左节点输出之前先要输出一个左括号,然后再右节点输出之后,要输出右括号,这样就做到打印包含括号的中缀表达式。
对于中缀表达式的输出,只要对整棵树进行中序遍历,就可以得到完整的中缀表达式
四、实现代码与测试结果
采用Visual Studio 2015
树节点的结构
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifndef TREE_H
#define TREE_H
#define MAXSIZE 40
#define MIDDLESIZE 20
typedef struct Node Node;
typedef enum{
true,false
}bool;
struct Node{
Node *leftPoint;
Node *rightPoint;
int intNum;
double doubleNum;
char ope;
char expArray[10];
int flag; //toindicate the expression contain bracket if flag==2 sin flag==3 cos flag==4 tan flag==5 exp flag==6 ctan
int intDoubleOpe; //1means int 2 means double 3 means Ope
};
void nodeIntInit(Node *node,int num);
void nodeDoubleInit(Node *node,double num);
#endif
#include "Tree.h"
/**
*initializationof each node
*paramintNum indicates the int number stored in the node
*paramdoubleNum indicates the float number stored in the node
*paramif flag == 1 means all the characters under this node is included in thebracket
*paramintDoubleOpe decides whether the type of number stored in the node is a int ora float
*paramleftPoint and rightPoint is the binary-branch of the parent node
**/
void nodeIntInit(Node *node,int num){
node->intNum=num;
node->flag=0;
node->doubleNum=0;
node->intDoubleOpe=1;
node->leftPoint=NULL;
node->rightPoint=NULL;
}
void nodeDoubleInit(Node *node,double num){
node->intNum=0;
node->flag=0;
node->doubleNum=num;
node->intDoubleOpe=2;
node->leftPoint=NULL;
node->rightPoint=NULL;
}
#include "StrProcess.h"
/**
*thisfunction can get all the characters which i put in the computer
*paramhead the pointer point to the beginingsection of the array
*parammaxsize means the limit of the array
**/
void my_getline(char *head,int maxsize){
char c;
int len=0;
while(len<MAXSIZE-1&&(c=getchar())!='\n'){
head[len]=c;
len++;
}
//head[len]='\n';
}
/**
*STRProcesscan process a string to make it stored in the node of binary tree and spilitinto two part
*and call the function itself in a recursive way
*stris the original input string and the root is the overall root of tree
**/
void STRProcess(char *str,Node *root){
char front[MIDDLESIZE]={'\0'}; //设置一个前字符串数组用于存储分裂字符串的时候前半段字符串所存储的内容
char back[MIDDLESIZE]={'\0'}; //设置一个后字符串数组用于存储分裂字符串的时候后半段字符串所存储的内容
int min=10,temp=0,position=0,type=0,jump=0;
int length=strlen(str);
//int tempFlag=0;
double tempDouble=0;
int tempInt=0;
SC *p=(SC*)malloc(sizeof(SC)); //初始化一个栈用于后面对括号的处理
init_Stack(p);
assert(assertLine(str)); //判断整个表达式是否有输入错误
if((type=isNumber(str))>0){ //isNumber函数针对的是 判断该字符串是否只剩下数字字符以及小数点
if(type==6){ //返回值为6的话 说明表达式是含有sin cos tan exp等符号的函数,所以需要进行处理
char tempArray[10]={'\0'};
int jump=0;
int j=0;
// doubledoublenumber=0;
for(inti=0;i<length;i++){
if(str[i]=='('){ //如果遇到了左括号,在sin(x)这样的函数当中,如果遇到了左括号就要把内容存储数组当中
jump=1;
continue;
}
if(str[i]==')'){
jump=1;
continue;
}
if(jump){ //将sin函数 cos函数括号内的字符存储到数组当中
tempArray[j]=str[i];
j++;
}
}
tempDouble=numProcess(tempArray); //将字符串进行数字处理,转化为int或者float类型
switch(str[0]){
case 's': //如果首字母为s 说明是sin函数 调用math.h的sin函数
tempDouble=sin(tempDouble);
//root->flag=2;
break;
case 'c': //如果首字母为c第二个字母为o 说明是cos函数调用math.h的cos函数
if(str[1]=='o'){
tempDouble=cos(tempDouble);
// root->flag=3;
}
else{
tempDouble=atan(tempDouble); //如果首字母为c第二个字母为t 说明是ctan函数调用math.h的atan函数
//root->flag=6;
}
break;
case 't': //如果首字母为t 说明是stan函数 调用math.h的tan函数
tempDouble=tan(tempDouble);
//root->flag=4;
break;
case 'e': //如果首字母为e 说明是exp函数 调用math.h的exp函数
tempDouble=exp(tempDouble);
// root->flag=5;
break;
}
strcpy_s(root->expArray,20,str);
nodeDoubleInit(root,tempDouble);
root->flag=2;
return;
}
if(type==5){ //返回值是5的话,说明整个字符是负的浮点数需要进行特殊处理
for(intj=0;j<length-1;j++)
str[j]=str[j+1];
str[length-1]='\0';
tempDouble=-numProcess(str); //numProcess函数前面的负号正是证明了是负小数
nodeDoubleInit(root,tempDouble);
return;
}
if(type==4){ //返回值是4的话 就说明了字符串是包含在括号内部的,需要进行曲括号处理
for(intj=0;j<length-2;j++)
str[j]=str[j+1];
str[length-2]='\0';
str[length-1]='\0';
length=length-2;
root->flag=1;
}
if(type==3){ //返回值是3的话是负整数
for(intj=0;j<length-1;j++)
str[j]=str[j+1];
str[length-1]='\0';
tempDouble=-numProcess(str);
tempInt=(int)tempDouble;
nodeIntInit(root,tempInt);
return;
} //minus
if(type==2){ //返回值是2的话就是浮点数
tempDouble=numProcess(str);
nodeDoubleInit(root,tempDouble);
return;
} //float
if(type==1){ //返回值是1的话就是整数
tempDouble=numProcess(str);
tempInt=(int)tempDouble;
nodeIntInit(root,tempInt);
return;
}
//int
//nodeInit(root,str[0]-'0');
//numProcess();
//return;
}
for(inti=0;i<length;i++){ //如果都不属于以上的情况,就说明整个字符串并未处理完全,还存在着很多运算符,需要进一步处理
temp=operatorPriority(str[i]);
if(temp==1&&i==0)
continue;
if(temp==3){
push(p,'(');
jump=1; //遇到左括号的话要把左括号进栈,并且把jump标志位置位1,使其跳过括号内的内容
}
if(temp==4){
pop(p);
if(is_Empty(p)) //如果遇到右括号的话就把左括号出栈,把标志位置为0,继续扫描
jump=0;
continue;
}
if(jump==1)
continue;
if(temp!=0&&min>=temp){ //because we needthe first operator 找到最低优先级的第一个运算符
min=temp;
position=i;
}
}
root->ope=str[position]; //记录下最小优先级运算符的位置
root->intDoubleOpe=3; //intDoubleOpe=3 指示着这个节点所存储的内容是运算符
root->leftPoint=(Node*)malloc(sizeof(Node)); //运算符作为根节点,并开辟左节点
root->rightPoint=(Node*)malloc(sizeof(Node)); //开辟右节点空间
nodeIntInit(root->leftPoint,0);
nodeIntInit(root->rightPoint,0);
stringSpilit(front,back,str,position); //将整个运算符分割成左右两部分,在运算符左边的就放在左节点下,在运算符右边的就放在右节点下
STRProcess(front,root->leftPoint); //递归处理左节点和右节点
STRProcess(back,root->rightPoint);
}
int operatorPriority(char ope){
int priority=0;
if(ope>=48&&ope<=57)
priority=0;
switch(ope){
/*case '+':
priority=1;
break;
case '-':
priority=2;
break;
case '*': case '/': case '%':
priority=3;
break;
case '(' :
priority=4;
break;
case ')' :
priority=5;
break;
case '^':
priority=6;
break;
case '.':
priority=7;
break;*/
case '+': case '-': //如果当前字符为 加减号的话,其优先级最低,为1
priority=1;
break;
case '*': case '/': case '%': //如果当前符号为 * % %的话 就是优先级为2
priority=2;
break;
case '(' :
priority=3;
break;
case ')' :
priority=4;
break;
case '^': //乘方的优先级较高为5
priority=5;
break;
case '.': //小数点的优先级为6
priority=6;
break;
}
return priority;
}
void stringSpilit(char *front,char *back,char *origin,int position){ //该函数的作用就是讲整个字符串根据中间运算符所在的位置,把字符串分割为两个字字符串
int length=strlen(origin);
for(int i=0;i<position;i++){
front[i]=origin[i];
}
for(int j=0;j<length-position-1;j++){
back[j]=origin[j+position+1];
}
}
/**
isNumber tells the type of the string
if the return number is 1 means it is ainteger 2 stand for the float 3 stand for minus int 4 stand for bracket expression 5 stand forthe minus float
and 6 stands for the math function likesin and cos
**/
int isNumber(char *str){ //if the returnnumber is 1 means it is a integer 2 stand for the float 3 stand for minusint 4 stand for bracket expression 5stand for the minus float
int doubleFlag=0;
int flag=1; //6 stands for themath function
int jump=0;
char Left[5];
int length=strlen(str);
SC *p=(SC*)malloc(sizeof(SC));
init_Stack(p);
left(Left,str,4);
//if(str[0]=='('&&str[strlen(str)-1]==')')
// return 4;
//if(str[i]=='-'||operatorPriority(str[i])==0){
// scan=1;
//flag=3;
//}
if( !(strcmp(Left,"sin(")&&strcmp(Left,"cos(")&&strcmp(Left,"tan(")&&strcmp(Left,"ctan")))
return 6;
if(str[0]=='('){
push(p,'(');
jump=1;
}
for(int j=1;j<length;j++){
if(str[j]=='('){
push(p,'(');
jump=1;
}
if(str[j]==')'){
pop(p);
if(is_Empty(p))
jump=0;
if(j==length-1)
return 4;
continue;
}
if(jump==1)
continue;
if(str[j]=='.'){
doubleFlag=1;
continue;
}
if(str[j]>='0'&&str[j]<='9'){
}
else{
flag=0;
break;
}
}
if(flag==0)
return 0;
else{
if(str[0]=='-'&&doubleFlag==0)
return 3;
if(str[0]=='-'&&doubleFlag==1)
return 5;
if(doubleFlag==1)
return 2;
return 1;
}
}
/**
*numProcesscan transform a set of string into the integer or the float number
**/
double numProcess(char *str){ //该函数的作用就是判断字符串里面是否有小数点号,如果有就按小数点的处理方法,把字符串转化为小数,如果没有,就把字符转化为整数
double intPart=0;
double doublePart=0;
int position=strlen(str);
// int temp=position;
int temp1;
for(int i=0;i<strlen(str);i++){
if(str[i]=='.'){
position=i;
break;
}
}
for(int j=position-1,k=0;j>=0;j--,k++){
temp1=str[j]-'0';
intPart=intPart+temp1*pow(10,k);
}
for(int m=position+1,n=-1;m<strlen(str);m++,n--){
temp1=str[m]-'0';
doublePart=doublePart+temp1*pow(10,n);
}
return intPart+doublePart;
}
/*从字符串的左边截取n个字符*/
char * left(char *dst,char *src, int n)
{
char *p = src;
char *q = dst;
int len = strlen(src);
if(n>len) n = len;
while(n--) *(q++) = *(p++);
*(q++)='\0'; /*有必要吗?很有必要*/
return dst;
}
/**
*assertLinecan scan the whole string and judge if the input form of the string is legal ornot
**/
int assertLine(char *str){
int leng=strlen(str);
int count=0;
SC *p=(SC*)malloc(sizeof(SC));
init_Stack(p);
for(int i=0;i<leng;i++){
/*if(str[i]=='(')
push(p,'(');
else if(str[i]==')')
pop(p);
else if(str[i])*/
switch(str[i]){
case '(':
push(p,'(');
break;
case ')':
pop(p);
break;
case '+': case '-': case '*': case '/': case '%': case '^':
count++;
break;
default:
count=0;
break;
}
if(count==3)
return 0;
}
if(is_Empty(p))
return 1;
else
return 0;
}
对整棵树的中缀输出以及结果的计算,递归删除整个树
#include <assert.h>
#include<math.h>
#include "display.h"
/**
*displayInOrderfunction can express the regular order of the formula
*paramroot is the fundamental root of the whole binary tree
**/
void displayInOrder(Node *root){
if(root->flag==2){ //如果flag为2的时候 就可以输出存在expArray里面的表达式了
printf("%s",root->expArray);
return;
}
if(root->flag==1) //如果flag为1话 说明该节点下所有的内容都是在一个括号里面的 所以在中序遍历节点下的内容时先输出一个左括号
printf("(");
if(root->leftPoint!=NULL) //中序遍历中,左节点的内容时最先遍历的,所以先遍历左节点的内容
displayInOrder(root->leftPoint);
if(root->intDoubleOpe==1) //如果intDoubleOpe为1 表示该节点内存储的数据为int类型 输出%d
printf("%d",root->intNum);
if(root->intDoubleOpe==2) //如果intDoubleOpe为2 表示该节点内存储的数据为float类型 输出%f
printf("%f",root->doubleNum);
if(root->intDoubleOpe==3) //如果intDoubleOpe为3 表示该节点内存储的数据为char类型 输出%c 就是运算符
printf("%c",root->ope);
if(root->rightPoint!=NULL)
displayInOrder(root->rightPoint); //中序遍历中右节点的内容是最后遍历的 所以遍历右节点的内容
if(root->flag==1)
printf(")"); //在最后结束的时候再输出一个右括号
}
/**
*calculatefunction can calculate the result of the formula stored in the binary tree
*paramroot is the fundamental root of the whole binary tree
**/
double calculate(Node *root){
if(root->leftPoint==NULL&&root->rightPoint==NULL){ //如果该节点的左右都是空指针,则需要返回该节点所存储的值,如果inDoubleOpe为1的话,返回值应该为int,如果是2的话 返回值为float
if(root->intDoubleOpe==1)
return root->intNum;
if(root->intDoubleOpe==2)
return root->doubleNum;
}
if(root->leftPoint!=NULL&&root->rightPoint!=NULL){
switch(root->ope){
case '+': //如果子节点是加号,则将左右节点的计算结果想加,如果是负号则把左右节点的结果相减,如果是乘号,则把左右节点结果相乘
return calculate(root->leftPoint)+calculate(root->rightPoint); //如果是/ 号,就把左右结果相除
case '-':
/*if(root->rightPoint->intDoubleOpe==3){
if(root->rightPoint->ope=='-')
returncalculate(root->leftPoint)+calculate(root->rightPoint);
if(root->rightPoint->ope=='+')
returncalculate(root->leftPoint)-calculate(root->rightPoint);
}*/
return calculate(root->leftPoint)-calculate(root->rightPoint);
case '*':
return calculate(root->leftPoint)*calculate(root->rightPoint);
case '/':
return calculate(root->leftPoint)/calculate(root->rightPoint);
case '%':
assert(root->leftPoint->intDoubleOpe!=2&&root->rightPoint->intDoubleOpe!=2); //求余符号两边都是要整数,所以需要assert函数来测试,然后将左右节点结果求余
return (int)calculate(root->leftPoint)%(int)calculate(root->rightPoint);
case '^':
return pow(calculate(root->leftPoint),calculate(root->rightPoint)); //如果是乘方,则把左右节点结果进行乘方运算
}
}
}
/*把申请的内存通过递归的方式释放掉 毕竟树本来就是一种递归的形式*/
void recursiveDelete(Node *root) {
if (root->leftPoint == NULL&&root->rightPoint == NULL) {
free(root);
root = NULL;
return;
}
recursiveDelete(root->leftPoint);
root->leftPoint = NULL;
recursiveDelete(root->rightPoint);
root->rightPoint = NULL;
}
五、思考与问题
1.对于负号的处理:
负号其实是一个很烦的东西,因为它既可以做单目运算符,也可以做双目运算符。我目前没有想到什么好的处理办法,就是每次扫描的时候如果扫描到的是运算符,就把flag置位1,如果下一个字符串也是运算符的话而且是‘-’的话,就说明后面的字符串表示的是负号的形式
2.对于括号的处理
括号里面的内容可以看做一个整体,所以每次遇到左括号的时候,就把左括号入栈,括号内的符号跳过,直到遇到右括号。再将左括号出栈,继续扫描整个字符换。
3.对于变量在计算前的提前声明:
感觉这个点实现难度有点大,如果实现了就可以像编译器一样对变量进行定义赋值计算。我想了一下,大概的思路是对int float这类的字符串进行匹配,然后要找到等号,等号左侧是变量的名称,右侧的所有就是要赋值的变量了。而且要以分号作为语句分格,就可以对语句进行清晰的分格