【C】四则运算生成和核对器----by郁卓、谢明浩
完成功能:
1. 使用 -n 参数控制生成题目的个数
2. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
3. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
4. 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
5. 每道题目中出现的运算符个数不超过3个。
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
6. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
7. 程序应能支持一万道题目的生成。
8. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt
未完成功能:
1. 程序一次运行生成的题目不能重复
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 60 |
Development | 开发 | 1420 | 1430 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 60 |
· Design Spec | · 生成设计文档 | 20 | 10 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 800 | 740 |
· Code Review | · 代码复审 | 240 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 180 |
Reporting | 报告 | 110 | 100 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 1560 | 1590 |
程序设计
需求分析
将程序分为表达式生成和答案核对两部分
-
表达式生成所要求的功能有:
1.随机生成符合正确数学规则的表达式
2.可以通过参数控制题目中数值的大小
3.查重。即任何两道题目不能通过有限次交换+和x左右的算术表达式变换为同一道题目
4.将生成的文件以固定的格式写入txt文件中 -
答案核对所要求的功能有:
1.计算txt文件中的数学表达式的答案
2.将答案文件写入Answers.txt文件中
3.对给定的题目文件和答案文件,判定答案中的对错,并进行数量统计
解题思路
首先是表达式生成,其核心要满足随机性,特别是括号的生成,一开始我们讨论出两套方案:
-
一是利用二叉树的中序遍历,其结构如下:
方法是每次访问父结点时,自动带上括号,这样最终的表达式生成公式为:
$$ ((A op1 B)op2(C op3 D)) $$ 注释:ABCD
是变量,op1
、op2
和op3
是运算符
但是这个方案不一定是随机的,无法生成一对括号中有三个变量的运算,如 \((3+4+5)*6\) -
第二种方案就是正面莽,基于左括号
(
只能在运算符的后面一位,右括号)
只能在变量的后面一位这一特点,分别写左括号生成函数和右括号生成函数。每次生成数值或运算符后进行一次判断。
在计算部分,重点在于计算时对分数的处理和各符号优先级的判断。
-
先定义好表达式结构体和适合计算的分数结构体,分母为1代表该数是整数并且定义好存放分数和运算符的栈,设置好栈的相关操作,用于读题时的优先级判断。
-
写入一个判断符号优先级的函数,并以字符'>', '<'输出结果。
-
写好分数结构体的加减乘除算法并整合到一个函数中。
-
通过一个函数得出一个表达式的答案。
-
通过一个文件打开函数读入题目并将答案分别存入各个表达式结构体数组的答案元素中,存入Answers.txt。
在核对部分,将<exercisefile>.txt内的答案逐个读入,转为分数形式,再和通过问题文件生成的表达式数组里的答案元素逐个比较,当分子于分母都相等时,判断对,正确数加一,题号存入数组,否则错题数加一,题号存入数组。再分别输出到Grades.txt。
代码说明部分
将变量放入结构体中
1 struct Variable
2 {
3 char *val;
4 int left = 0; //1:变量有左侧有左括号
5 int right = 0;
6 int ator = 0; //1:变量为分数
7 int size = 0;
8 };
括号生成函数
1 BOOL BCL(char *calproblem, int x, int num) //nur after op
2 {
3 extern int LB, RB;
4 switch (rand() % 4)
5 {
6 case 0:
7 {
8 if (LB<2&&(num - x) >= 2)
9 {
10 strcat_s(calproblem, strlen(calproblem) + sizeof(char) + 1, "(");
11 LB++;
12 m_VarStruct[x].left = 1;
13 return 1;
14 }
15
16 }
17 break;
18 case 2:
19 {
20 if (LB == 0 && (num - x >= 3))
21 {
22 strcat_s(calproblem, strlen(calproblem) + 2 * sizeof(char) + 1, "((");
23 LB = 2;
24 m_VarStruct[x].left = 2;
25 return 1;
26 }
27 }
28 break;
29 default:
30 break;
31 }
32 return 0;
33 }
34 BOOL BCR(char *calproblem, int x, int num) //nur after Var
35 {
36 extern int LB, RB;
37 if (RB == 2 || LB==0) return 0;
38 if (LB <= 2)
39 {
40 switch (rand() % 2)
41 {
42 case 1:
43 {
44 if (!m_VarStruct[x].left)
45 {
46 strcat_s(calproblem, strlen(calproblem) + strlen("(") + sizeof("\0"), ")");
47 RB++;
48 m_VarStruct[x].right = 1;
49 return 1;
50 }
51 }
52 break;
53 case 0:
54 {
55 if (!m_VarStruct[x].left&&LB == 2 && RB == 0)
56 {
57 strcat_s(calproblem, strlen(calproblem) + 2 * sizeof(char) + 1, "))");
58 RB = 2;
59 m_VarStruct[x].right = 2;
60 return 1;
61 }
62 }
63 break;
64 default:
65 break;
66 }
67 }
68 return 0;
69 }
核对答案代码说明
1 typedef struct { //分数结构体,分母默认为1,即整数
2 int numerator = 0; //分子
3 int denominator = 1; //分母
4 }Element; //计算项结构体
分数结构体的声明。
1 int GetGreatestCommonFactor(int a, int b){ //返回最大公因数
2 int temp = 0;
3 while(a!=0){
4 temp = b % a;
5 b = a;
6 a = temp;
7 }
8 return b;
9 }
10
11 Element Add(Element e1,Element e2){ //加法
12 Element e3;
13 int n = 0;
14 int d = 0;
15 n = e1.numerator * e2.denominator + e2.numerator * e1.denominator;
16 d = e2.denominator * e1.denominator;
17 int C = GetGreatestCommonFactor(n, d); //求出分子和分母的最大公因数
18 e3.numerator = n / C;
19 e3.denominator = d / C;
20 return e3;
21 }
22
23 Element minus(Element e1,Element e2){ //减法,前一个是被减数,后一个是减数
24 Element e3;
25 int n = 0;
26 int d = 0;
27 n = e1.numerator * e2.denominator - e2.numerator * e1.denominator;
28 d = e2.denominator * e1.denominator;
29 int C = GetGreatestCommonFactor(n, d);
30 e3.numerator = n / C;
31 e3.denominator = d / C;
32 return e3;
33 }
34
35 Element Multiply(Element e1,Element e2){
36 Element e3;
37 int n = 0;
38 int d = 0;
39 n = e1.numerator * e2.numerator;
40 d = e2.denominator * e1.denominator;
41 int C = GetGreatestCommonFactor(n, d); //求出分子和分母的最大公因数
42 e3.numerator = n / C;
43 e3.denominator = d / C;
44 return e3;
45 }
46
47 Element Divide(Element e1,Element e2){ //除法,前一个是被除数,后一个是除数
48 Element e3;
49 int n = 0;
50 int d = 0;
51 n = e1.numerator * e2.denominator;
52 d = e1.denominator * e2.numerator;
53 int C = GetGreatestCommonFactor(n, d);
54 e3.numerator = n / C;
55 e3.denominator = d / C;
56 return e3;
57 }
分数的加减乘除并化为既约分数的过程。
1 char CompareOp(char op1, char op2) {
2 char c;
3 switch (op2) {
4 case '+':
5 case '-': {
6 if (op1 == '(' || op1 == '=') c = '<';
7 else c = '>';
8 break;
9 }
10 case '*':
11 case -62: {
12 if (op1 == '*' || op1 == -62 || op1 == ')') c = '>';
13 else c = '<';
14 break;
15 }
16 case '(': {
17 if (op1 == ')') {
18 printf("错误输入\n");
19 return -1;
20 }
21 else c = '<';
22 break;
23 }
24 case ')': {
25 switch (op1) {
26 case '(': {
27 c = '=';
28 break;
29 }
30 case '=': {
31 printf("错误输入\n");
32 return -1;
33 }
34 default:c = '>';
35 }
36 break;
37 }
38 case '=': {
39 switch (op1) {
40 case '=': {
41 c = '=';
42 break;
43 }
44 case '(': {
45 printf("错误输入\n");
46 return -1;
47 }
48 default: c = '>';
49 }
50 }
51 }
52 return c;
53 }
这里是运算符优先级判断函数,输入两个运算符,对运算符进行优先级判断,当出现一对完整的括号时,输出等于(从而立即计算括号内的式子)。
1 Element AnAnswer(char s[40]){ //计算得出答案项
2 NumberStack numberstack;
3 OpStack opstack;
4 Element num1,num2,result,num;
5 char c,sign;
6 char *str = NULL;
7 int count = 0;
8
9 InitNumberStack(&numberstack);
10 InitOpStack(&opstack);
11
12 PushOpStack(&opstack, '=');
13 int j = 0;
14 while(s[j] != '\t') j++; //越过题号,来到题目开始位置
15 int i = j;
16 j = 0;
17 c = s[i];
18 while((c != '=')||opstack.op[opstack.top] != '='){
19 if(JudgeOp(c) == 0){
20 str = (char*)malloc(sizeof(char)*12);
21 do{
22 *str = c;
23 str++;
24 count++;
25 i++;
26 c = s[i];
27 }while(JudgeOp(c) == 0); //提取数字字符串
28 *str = '\0';
29 str = str - count;
30 num = GetNumberFromStr(str);//数字字符串化为分数结构体
31 PushNumberStack(&numberstack, num);
32 str = NULL;
33 count = 0;
34 }
35 else{
36 switch(CompareOp(opstack.op[opstack.top], c)){
37 case '<':{
38 PushOpStack(&opstack, c);
39 i++;
40 c = s[i];
41 break;
42 }
43 case '=':{
44 sign = PopOpstack(&opstack);
45 i++;
46 c = s[i];
47 break;
48 }
49 case '>':{
50 sign = PopOpstack(&opstack);
51 num2 = PopNumberStack(&numberstack);
52 num1 = PopNumberStack(&numberstack);
53 result = CaculateOneOp(num1, sign, num2);
54 PushNumberStack(&numberstack, result);
55 break;
56 }
57
58 }
59 }
60 }
61 result = numberstack.e[numberstack.top];
62 return result;
63 }
在优先级判断中,如果是“<”,意味着之后读到的运算符优先级更高要先算,所以放入栈,获取下一个运算符,如果是“=”,意味着括号配对出现,则将栈里的左括号弹出,如果是">",意味着之后的运算符优先级较低,先将之前的运算级较高的运算符计算完后答案入栈,再进行接下来的判断。
测试运行
***********************
可用参数如下:
-r <题目的数字最大值>
-n <题目的数量>
-e <题目文件位置>
-a <答题卡文件位置>
-h "帮助"
***********************
-
表达式生成部分
可支持生成100k道题目
生成10k道题目的时间为18ms的CPU时间
-
答案生成部分
生成10k道题目的时间为63ms的CPU时间
- 核对答案部分
答题卡(测试文件)包括五道正确,两道错误,其余为空
成绩
对比10k道题目的时间为55ms的CPU时间
效能分析
程序中最占时间的函数是SaveAnswers
、AnAnswer
、OoenFielAndGiveTheAnswer
和CompareAndGiveTheGrade
四个函数,都是和答案生成有关,其中三个函数都调用了AnAnswer
函数,这个函数的定义是:
Element AnAnswer(char s[40]); //计算得出答案项,并将答案存入结构体中
AnAnswer
函数的调用情况如下:
可以看到AnAnswer函数中最耗时间的是malloc
内存分配,第二耗时的是GetNumberFromStr()
函数,其定义如下
Element GetNumberFromStr(char s[]); //从含单个数的字符数组中得出相应的值,将int转化成字符数组存储
其中最耗时的是int转化为字符数组的库函数itoa
项目小结
在本次开发中,我遇到的最困难的问题就是内存分配问题,特别是数组越界的判断,真的是 不怕恋人出轨,就怕数组出界
存放表达式的数组原本我想设计成响应式的,即可以根据不同数值的变量分配不同大小的内存,以减少内存的使用,但是一直触发内存访问错误的断点。大改了两三次,重写了一次,小修补十几次。
已探明的问题主要在以下几个方面:
1.数组大小估计不足。主要是因为对数组的概念不明确
char str[20]="0123456789";
int a=strlen(str); //a=10; >>>> strlen 计算字符串的长度,以结束符 0x00 为字符串结束。
int b=sizeof(str); //而b=20; >>>> sizeof 计算的则是分配的数组 str[20] 所占的内存空间的大小,不受里面存储的内容改变。上面是对静态数组处理的结果,如果是对指针,结果就不一样了
char* ss = "0123456789";sizeof(ss) 结果 4 ===》ss是指向字符串常量的字符指针,sizeof 获得的是一个指针的之所占的空间,应该是长整型的,所以是4
sizeof(*ss) 结果1 ===》*ss
是第一个字符 其实就是获得了字符串的第一位'0' 所占的内存空间,是char类型的,占了 1 位
strlen(ss)= 10 >>>> 如果要获得这个字符串的长度,则一定要使用 strlen
char | [0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] | [8] | [9] |
---|---|---|---|---|---|---|---|---|---|---|
A | B | C | D | E | F | G | H | \0 | \0 |
如果没有结尾的\0
,数组就会出界。calloc
和malloc
两个函数直接输入所需元素的个数,编译器会自动分配一个空间存放\0
接在后面。
2.字符数组结尾没有加上\0
,这个错误主要是strcpy_s
和strcat_s
两个函数中关于数组大小参数的使用。