二、结对成员:高明莹3218004990 刘珮琳3218004993
三、项目需求
自然数:0, 1, 2, …。
- 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 运算符:+, −, ×, ÷。
- 括号:(, )。
- 等号:=。
- 分隔符:空格(用于四则运算符和等号前后)。
- 算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
- 四则运算题目:e = ,其中e为算术表达式。
- 使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10 将生成10个题目。
- 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
- 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
- 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
- 每道题目中出现的运算符个数不超过3个。
- 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
- 四则运算题目1
- 四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
- 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
- 答案1
- 答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
- 程序应能支持一万道题目的生成。
- 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
四、已完成以及未完成功能
生成题目序号以及题目并写入文件(完成)
控制题目中数值的范围。(完成)
计算四则运算的答案并写入文件(完成)
每道题目中出现的运算符个数不超过3个。(完成)
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。(完成)
程序支持对给定的题目文件和答案文件,判定学生书写答案中的对错并进行数量统计。(完成)
程序应能支持一万道题目的生成。(完成)
程序一次运行生成的题目不能重复------------由于在项目开始之前没有考虑到边生成题目边查重,而是先生成题目,再调用查重函数,由于先生成题目之后再查重需要的递归次数随着操作符的增加而增多,所以查重函数的运行效率很低,无法判断是否实现了该功能。
五、效能分析
运行时间
由于在项目开始之前没有考虑到边生成题目边查重,而是先生成题目,再调用查重函数,由于先生成题目之后再查重需要的递归次数随着表达式个数的增加而增加,所以查重函数的运行效率很低,当两个表达式二叉树的运算符个数不同时,执行效率较高,能够较快得出结果,但当两个表达式二叉树的运算符个数相同时,光标持续闪烁且无输出结果,因此无法判断是否完全实现了该功能。
情况如图:
六、设计实现过程
对答案以及查重的流程图:
其他功能函数:
七、详细代码
随机生成操作符函数
char* caozuofu(char* ziFu,int a) //a为想要获得的操作符个数 { char *p; int shuZi[a]; shuZi[a]= {0}; for(int j=0; j<a; j++) { shuZi[j]=rand()%4; switch(shuZi[j]) { case 0: ziFu[j]='+'; break; case 1: ziFu[j]='-'; break; case 2: ziFu[j]='*'; break; case 3: ziFu[j]='/'; break; default: ; } } return ziFu; }
求两个数公约数函数
int gongyueshu(int x,int y) //约分用的 { int i,p; if(x%y==0||y%x==0) { i=x<=y?x:y; } else //x不能整除y或y不能整除x { if(x<y) { p=x; for(i=p; i>1; i--) { if(x%i==0&&y%i==0) break; } } else //x>y { p=y; for(i=p; i>1; i--) { if(x%i==0&&y%i==0) break; } } } return i; }
将分数化为真分数函数,并将真分数写入txt文件
void zhenfenshu(int x,int y,FILE *fp) //将分数变成真分数 { int z,a,r; z=x/y; a=x%y; if(a==0) //x整除y得时候即该数不为分数 { fprintf(fp,"%d ",z); } else //x不整除y { if(z==0) // x<y { fprintf(fp,"%d/%d ",x/gongyueshu(x,y),y/gongyueshu(x,y)); } else //x>y { r=x-z*y; fprintf(fp,"%d'%d/%d ",z,r/gongyueshu(r,y),y/gongyueshu(r,y)); } } }
将两个整数约分之后的函数,并返回分子和分母
//整数除法的函数 int* int_chuFa(int n1,int n2,int* num) { int z1,a1,r1; z1=n1/n2; a1=n1%n2; if(a1==0) //n1整除n2得时候即该数不为分数 { num[0]=z1; num[1]=1; } else //x不整除y { int yueshu = gongyueshu(n1,n2); num[0]=n1/yueshu; num[1]=n2/yueshu; } return num; }
对答案的函数
Status checkAnswer(){ char student[30]; char answer[30]; //s用来存储学生答案的文件名(含后缀) printf("---------------------------------------------IMPORTANT!!!!----------------------------------------------- "); printf("****************************请先将学生答案放入文件夹的根目录下再进行下述操作***************************** "); printf(" 请输入学生答案的文件名(附文件后缀名):"); scanf("%s",student);//学生答案的文件名 // printf(" 请输入标准答案的文件名(附文件后缀名):"); // scanf("%s",answer); FILE *fpStudent = fopen(student,"r"); //读学生答案 if(fpStudent == NULL) //无法读取文件的异常情况 { printf(" 无法读取学生的答案文件,请重试! "); getchar(); exit(1); } //默认标准答案的文件名为answer.txt FILE *fpAnswer = fopen("answer.txt","r"); //读标准答案 if(fpAnswer == NULL) { printf(" 无法读取标准答案的文件,请重试! "); getchar(); exit(1); } int CorrectNum = 0,WrongNum = 0; int control1 = 0,control2 = 0,control3 = 0; //control1:wrongList,control2:correctList,control3:modifyList,control4:lackList char *p, *q; //p指针用来遍历学生答案,q指针用来遍历标准答案 char *p1, *q1, *p2; char temp[30]={0}; char temp1[30]={0}; char temp2[30]={0}; char temp3[30] = {0}; AnswerList wrongList,correctList,modifyList; InitAnswerList(wrongList); //初始化错误题目的链表 InitAnswerList(correctList); //初始化正确题目的链表 InitAnswerList(modifyList); //初始化需要修改题目的链表 char string1[30], string2[30],string3[30], string4[30]; if(fgetc(fpStudent) == EOF) { printf(" 学生答案的文件是一个空文件! "); fclose(fpStudent); return 0; } if(fgetc(fpAnswer) == EOF) { printf(" 标准答案的文件是一个空文件! "); fclose(fpAnswer); return 0; } rewind(fpStudent); //回到文件开头 rewind(fpAnswer); //循环直至读到文件末尾 /* 首先对题号进行校对,可能会有漏题、或者题号错误的情况,如果题号出错,直接return */ while(!feof(fpStudent) && !feof(fpAnswer)) { if(fgets(string3,15,fpStudent)==NULL) printf("error in string3 "); //把学生答案里的数据读出来存取进字符串string3,且每次读取一行 if(fgets(string4,15,fpAnswer)==NULL) printf("error in string4 "); //把答案中第一行的数据读出来并存进字符串string2中 p1 = string3; //p1指向string3 q1 = string4; //q1指向string4 while(*p1!='.') { if(*p1 == *q1) { p1++; q1++; } else { //学生答案中的数字与标准答案的题号对不上(漏题或题号对应错) p2 = string3; control3 = 0; while(*p2!='.') { control3++; p2++; } strncpy(temp2,string3,control3); //将.之前的字符转换成字符串再变成int,赋给链表的结点 InsertInList(modifyList,CreateAnswerNode(temp2)); //将新结点插入错误链表中 p1++; q1++; } } /* if(*p1=='.' && *q1!='.') { printf(" 学生答案题号有误,可能原因:题号有跳跃或题号对应错误!出现错误的题号如下: "); visitAnswerList(modifyList); system("pause"); exit(1); }*/ } //判断少题的情况 if(!feof(fpAnswer) && feof(fpStudent)) { printf(" 出现错误!学生答案出现末尾少做题的状况,请重新检查并修改! "); exit(1); } if(!feof(fpStudent) && feof(fpAnswer)) { printf(" 出现错误!学生答案中出现比标准答案题数多的情况,请重新检查并修改! "); exit(1); } rewind(fpStudent); rewind(fpAnswer); /* 下面是没有漏题或者题号错误的情况下,对答案进行的校对 */ while(!feof(fpStudent) && !feof(fpAnswer)) //如果还没有到文件的结尾 { if(fgets(string1,15,fpStudent)==NULL) printf("error in string1 "); //把学生答案里的数据读出来存取进字符串string1,且每次读取一行 //string2出现问题 if(fgets(string2,15,fpAnswer)==NULL) printf("error in string2 "); //把答案中第一行的数据读出来并存进字符串string2中 p = string1; //p指向string1 q = string2; //q指向string2 while(*p!='.') { p++; //将p指针指向. } while(*q!='.') { q++; //将q指针指向. } p++; //p指针指向第一位答案位 q++; //q指针指向第一个答案位 while(*p!='