博客二记之四则运算
运算进化
在博客一记中,随机生成小学30道带有判断功能的加减法,根据需求的变化,在这一记中将有新的需求。不只是简单的加减法,而是要生成四则运算。根据有关资料表明四则运算要有三个及以上的运算符,只有这样的式子运算才能称的上是四则运算。
- 第一步:继承博客一记中的思想也是将题用一个类表示。相对于博客一记中的类,新的类明显要复杂得多。不只是有两个操作数,一个运算符,必须要有一段空间来保存操作数和运算符。因为要随机生成操作数及运算符,操作数的个数也是不确定的,故采用动态内存分配的方式。
class Problem { private: int *Operand;//存放操作数 int *Type;//存放运算符,0+ ,1- ,2* ,3/ string *Prob;//存放完整的问题 int opnum;//操作数个数 int brackets[2];//括号的位置 int Right_Result;//正确结果 int Write_Result;//填写的结果 public: Problem(); ~Problem(); void CreatProblem(int kuohao = 1);//创建题目 void SaveProblem();//将题目保存到Prob[] void ShowProblem();//显示题目 void Fill_Result();//填写结果 void Judge_Result();//判断结果 };
- 第二步,类的创建。将所有的操作数都放在了一个整型数组里,将所有的运算符也都放在数组里,最后形成的问题放到string类数组里。根据实际情况,预先设定宏定
# define OPERAND_NUM 4 //操作数不小于3,小于3+OPERAND_NUM
Problem::Problem() { opnum = 3+rand()%OPERAND_NUM;//操作数不小于3,小于3+OPERAND_NUM Operand = new int[opnum]; Type = new int[opnum-1]; Prob = new string[3*opnum]; for(int t = 0;t<3*opnum;t++) { Prob[t] = "\0"; } }
第三步,公有成员函数void CreatProblem()的设计。该函数用于问题的生成,有无括号有参数括号的值决定,默认为1,有括号。
void Problem::CreatProblem(int kuohao) { for(int i = 0;i<opnum-1;i++)//生成少一个的操作数、生成运算符 { Operand[i] = rand()%100; Type[i] = rand()%4; if(Type[i] == 3&&Operand[i] == 0)//排除除数为0的可能 { while(Operand[i] == 0) { Operand[i] = rand()%100; } } } Operand[opnum-1] = rand()%100;//生成最后一个操作数 if(kuohao == 1)//有括号 { brackets[0] = rand()%(opnum -1); brackets[1] = brackets[0] + 1 + rand()%(opnum -1-brackets[0]);//不在第一个括号的位置且不超过 } else//无括号 { brackets[0] = -1; brackets[1] = -1; } }
- 第四步,公有成员函数void SaveProblem()的设计。该函数用于问题的保存。
- 如何选择保存问题的结构一开始时纠结了好一段时间。
首先想到的是直接用字符串来保存,可是当要进行计算结果时,用字符串保存明显为后续的递归下降分析来计算结果带来新的问题——区分数字,也就不好办了。也就否决了用字符串来保存问题。
突然想到c++有string这个标准类,是字符串类,动态申请内存,能够保存多个字符串,保证了Prob里能保证数字不再是单个数而是一串数,也就不用判断组合成数字。
2. 如何设计算法将问题保存到结构里。
前opnum-1个操作数与运算符是成对出现的,而左括号只能出现在操作数前面,右括号只能出现在操作数的后面。在循环进行判断之前声明变量num来记录当前数组的下标。每有成员写入Prob里就将num加1。
void Problem::SaveProblem() { int num = 0;//string数组的下标 for(int temp =0;temp<opnum-1;temp++) { char str[10]; sprintf(str,"%d",Operand[temp]);//将整型转换成字符串 switch(Type[temp]) { /********case后的循环是去除除数为0的可能*********/ case 0: { while((Type[temp-1]==3)&&(Operand[temp]==0)) { srand(time(0)); Operand[temp] = rand()%100; } if(temp == brackets[0]) { Prob[num] = "("; num++; //strcat(tempprob,"("); } Prob[num] = str; num++; //strcat(tempprob,str); if(temp == brackets[1]) { Prob[num] = ")"; num++; //strcat(tempprob,")"); } Prob[num] = " + "; num++; //strcat(tempprob," + "); break; } case 1: { while((Type[temp-1]==3)&&(Operand[temp]==0)) { srand(time(0)); Operand[temp] = rand()%100; } if(temp == brackets[0]) { Prob[num] = "("; num++; //strcat(tempprob,"("); } Prob[num] = str; num++; //strcat(tempprob,str); if(temp == brackets[1]) { Prob[num] = ")"; num++; //strcat(tempprob,")"); } Prob[num] = " - "; num++; //strcat(tempprob," - "); break; } case 2: { while((Type[temp-1]==3)&&(Operand[temp]==0)) { srand(time(0)); Operand[temp] = rand()%100; } if(temp == brackets[0]) { Prob[num] = "("; num++; //strcat(tempprob,"("); } Prob[num] = str; num++; //strcat(tempprob,str); if(temp == brackets[1]) { Prob[num] = ")"; num++; //strcat(tempprob,")"); } Prob[num] = " * "; num++; //strcat(tempprob," + "); break; } case 3: { while((Type[temp-1]==3)&&(Operand[temp]==0)) { srand(time(0)); Operand[temp] = rand()%100; } if(temp == brackets[0]) { Prob[num] = "("; num++; //strcat(tempprob,"("); } Prob[num] = str; num++; //strcat(tempprob,str); if(temp == brackets[1]) { Prob[num] = ")"; num++; //strcat(tempprob,")"); } Prob[num] = " / "; num++; //strcat(tempprob," / "); break; } } } /*******最后一个操作数***********/ char strlast[10]; sprintf(strlast,"%d",Operand[opnum-1]); Prob[num] = strlast; num++; //strcat(tempprob,strlast); if(opnum - 1 == brackets[1]) { Prob[num] = ")"; } }
- 第五步,问题显示,结果输入。
- 第六步,结果的判断。四则运算结果计算,在数据结构中就曾经提到过,可以用二叉树、中序遍历、后续遍历等方法实现。在这次程序中,我采用的是用两个栈的方法。
构建两个栈,一个保存操作数,一个保存算符,从左向右遍历表达式。
1.遇到数字,压入操作数栈
2.遇到算符则比较栈顶元素和算符的优先级
- 1).栈顶元素比算符的优先级低,算符压栈
- 2).栈顶元素与操作符的优先级相同,证明是配对的()或#,栈顶算符出栈
- 3).栈顶元素比算符的优先级高,算符出栈1个,操作数出栈2个,计算完的结果压入操作数栈
在优先级确定时,
char Precedure(char a1, char a2) //a1为前一个运算符,a2为后一个运算符 { char r; switch(a2) { case '+': case '-': if(a1 == '(' || a1 == '#') r = '<'; else r = '>'; break; case '*': case '/': if(a1 == '*' || a1 == '/' || a1 == ')') r = '>'; else r = '<'; break; case '(': if(a1 == ')') { cout<<"括号匹配错误"<<endl; exit(-1); } else r = '<'; break; case ')': if(a1 == '(') r = '='; else if(a1 == '#') { cout<<"error,没有左括号"<<endl; } else r = '>'; break; case '#': switch(a1) { case'#': r='='; break; case'(': cout<<"error!没有右括号"<<endl; exit(-1); default: r='>'; }//switch break; } return r; }
- 最后就是主函数的设计来实现定制数目、是否包含括号等功能,这一部分相对于前面几部分要容易的多,有一点需要注意,就是Problem类声明之前要有srand(time(0)),而非在类的声明里,如果是在里面的话,生成的题目的操作数的个数都是一样的会出现如下情况,失去了随机的意义。
Problem类声明之前有srand(time(0)),类里没有,才真正实现操作数目的随机性,如下是修改后的情况。
经网上资料及亲身实践知srand()反应时间大概50~60ms,而循环是很快就完成了,因此要将srand()放在循环外,才有用。
好了,这个四则运算题就此结束,具体代码见 https://coding.net/u/zht01/p/Four_arithmetic-_operations/git/blob/master/kidtest_2.cpp