首先进行需求分析:从txt文件中读取四则运算题目后显示在控制台中,用户依次输入答案,程序判断对错并记录成绩,最后输出正确数。要求实现加减乘除并带括号的运算,数字支持整数,真分数及假分数,结果以分数表示。拓展功能为四则运算自动生成,要求算符最多不超过10个,支持加减乘除及括号,不能出现负数且题目不能重复。
起初在经过需求分析之后,我认为本程序的难点在于分数运算的处理。在学习数据结构的过程中我已经了解到由中缀表达式转为后缀表达式可使运算逻辑变得简明,只需将输入的算式依照算法转化为后缀表达,即可方便求解。但如果直接利用后缀表达式进行计算无法使结果显示分数,没有将小数转为分数的方法。在这个问题上确实困扰了很久。
浏览过一些帖子和博客之后,我发现了一个有关分数计算的帖子想法很巧妙。在这篇帖子的方法中,可以定义一个分数结构体,存放分母与分子,在计算时只需要对分母分子进行通分,剩下的就是单纯的整型四则运算,最后只需要把结果约分就可以了。只要将分母设置成1就可以表示整数了,我定义的结构体如下:
1 #define NUM 0 2 #define PUN 1 3 struct Word{ 4 int deo;//分母 5 int ele;//分子 6 int inte;//整数部分 7 int mode;//判定是符号还是数字 8 char pun;//符号内容 9 };
我简单粗暴的把数字中间的四则运算符也用这个结构体来表示,当变量mode为0时表示这个结构体存储的是数字,为1时表示符号,在表示算式时只需要一个Word型数组就可以保存。而且我留下了inte表示整数部分,可以拓展带分数的形式,若全部采用假分数的表示方法就可以忽略不计。
在运算过程中首先读取整个算式,再依照转换逻辑将中缀表达的char型字符串数组转换成后缀表达的Word型数组,在Word型数组中数字均以分母为1的整数形式存储,这就是该程序的核心函数之一:
1 int MiddleToSuffix(const char* ques,Word word[]){//中缀转后缀 2 int i,j=0; 3 i=0;//指示算式中元素的角标 4 while((j<strlen(ques))&&(ques[j]!='=')){//读取表达式转为后缀 5 switch(ques[j]){ 6 case ' ': break; 7 case '+': 8 case '-': while(!punc.empty()&&(punc.top()!='(')){//加减号优先级低 9 InsertPunc(word[i],punc.top()); 10 i++; 11 } 12 punc.push(ques[j]); 13 break; 14 case '*': 15 case '/': 16 case '%': while((!punc.empty())&&(punc.top()!='+')&&(punc.top()!='-')&&(punc.top()!='(')){//乘除号优先级高 17 InsertPunc(word[i],punc.top()); 18 i++; 19 } 20 punc.push(ques[j]); 21 break; 22 case '(': punc.push(ques[j]);break;//遇到左括号直接入栈 23 case ')': while(punc.top()!='('){//遇到右括号弹栈至左括号 24 InsertPunc(word[i],punc.top()); 25 i++; 26 } 27 punc.pop(); 28 break; 29 default: RecognizeInt(word[i],ques,j);i++;break;//将字符转化为数字 30 } 31 j++; 32 } 33 while(!punc.empty()){//若栈中仍有算符则出栈 34 InsertPunc(word[i],punc.top()); 35 i++; 36 } 37 return i; 38 }
输入的参数是读入的算式,以字符串数组形式存放,转换完成后将结果存放在Word型数组word中,返回值为Word型数组长度。以下是在转换过程中用到的函数,一个是将读入的数字由字符型转为整型存到Word中,一个是将读到的算符插入Word数组中:
1 void RecognizeInt(Word &w,const char* ques,int ×){//将读到的字符型数字转换为整型存入Word中 2 int temp,j=0; 3 j = times; 4 if((ques[j]>='0')&&(ques[j]<='9')){ 5 temp = ques[j] - '0'; 6 //i = times; 7 w.inte = 0; 8 w.deo = 1;//分母为1 9 w.ele = temp;//分子为读到的数字 10 w.mode = NUM; 11 w.pun = '\0'; 12 j++; 13 while((j<strlen(ques))&&(ques[j]>='0')&&(ques[j]<='9')){ 14 temp*=10; 15 temp+=(ques[j]-'0'); 16 w.ele = temp; 17 j++; 18 } 19 j--; 20 times = j;//指向当前word数组的下标 21 } 22 } 23 24 void InsertPunc(Word &w,char p){//插入符号 25 w.deo = 0; 26 w.ele = 0; 27 w.inte = 0; 28 w.mode = PUN;//表示存入的是算符 29 w.pun = p; 30 punc.pop();//转换过程中存放算符的栈 31 }
转换后缀之后当然要进行计算,依次读取word数组中的数字与符号进行计算,按照后缀表达式的逻辑进行,直至word数组的末尾,结果存放在word结构体中,总体逻辑如下:
1 Word Calculate(Word w[],int times){//计算结果 2 int i = 0; 3 stack<Word> ws;//数字栈 4 Word num1,num2; 5 while(i<times){ 6 if(w[i].mode==NUM) 7 ws.push(w[i]);//若为数字 直接入栈 8 else if(w[i].mode==PUN)//若为算符,计算 9 switch(w[i].pun){ 10 case '+': num2 = ws.top();ws.pop(); 11 num1 = ws.top();ws.pop(); 12 num1 = Add(num1,num2); 13 ws.push(num1); 14 break; 15 case '-': num2 = ws.top();ws.pop(); 16 num1 = ws.top();ws.pop(); 17 num1 = Sub(num1,num2); 18 ws.push(num1); 19 break; 20 case '*': num2 = ws.top();ws.pop(); 21 num1 = ws.top();ws.pop(); 22 num1 = Mult(num1,num2); 23 ws.push(num1); 24 break; 25 case '%': 26 case '/': num2 = ws.top();ws.pop(); 27 num1 = ws.top();ws.pop(); 28 num1 = Div(num1,num2); 29 ws.push(num1); 30 break; 31 default: break; 32 } 33 i++; 34 } 35 return ws.top();//计算结果存在数字栈顶 36 }
可以看到,为避免分数的分号与除号相混淆,我将除号用百分号"%"表示,在运算时用到了以下函数,使表达更加简洁:
1 Word Add(Word num1,Word num2){//两数相加 2 Word a; 3 a.inte = 0; 4 a.deo = num1.deo*num2.deo; 5 a.ele = num1.ele*num2.deo+num1.deo*num2.ele+(num1.inte+num2.inte)*num1.deo*num2.deo; 6 a.mode = NUM; 7 return a; 8 } 9 Word Sub(Word num1,Word num2){//两数相减 10 Word a; 11 a.inte = 0; 12 a.deo = num1.deo*num2.deo; 13 a.ele = num1.inte*num1.deo*num2.deo+num1.ele*num2.deo-(num2.inte*num1.deo*num2.deo+num2.ele*num1.deo); 14 a.mode = NUM; 15 return a; 16 } 17 Word Mult(Word num1,Word num2){//两数相乘 18 Word a; 19 a.inte = 0; 20 a.deo = num1.deo*num2.deo; 21 a.ele = num1.ele*num2.ele; 22 a.mode = NUM; 23 return a; 24 } 25 Word Div(Word num1,Word num2){//两数相除 26 Word a; 27 a.inte = 0; 28 a.deo = num1.deo*num2.ele; 29 a.ele = num1.ele*num2.deo; 30 a.mode = NUM; 31 return a; 32 }
这时就可看出为何要建立结构体将分子与分母分开存放,这样存放在进行计算时只需要进行通分,单纯进行分子的整型计算,逻辑简单明了,如同小学时学习的计算方法。这正是我在开始没有想到的,我只是单纯想着计算结果要怎么转化才能表示成分数,却没有想到分开存放。这样进行计算之后,即使一开始把所有数字都按照整数的格式存放,最后的计算结果也会变成分数,因为遇到分号可以理解成除号,就会使除数的分子与被除数分母相乘,得到的自然是分数了。
相对有一点还要提一下就是约分,我采用的约分方法是从1一直试到分子分母中最小的数,找到最大公约数,然后同时除以最大公约数:
1 void YueFen(Word &w){//约分 2 int temp=0; 3 w.ele = w.inte*w.deo+w.ele; 4 for(int i = 2;(i<=w.deo)&&(i<=w.ele);i++){ 5 if(!(w.ele%i)&&!(w.deo%i))//如果是公约数,就记录在temp 6 temp = i; 7 } 8 if(temp){//如果存在非1的公约数,同时除以它得到约分结果 9 w.ele/=temp; 10 w.deo/=temp; 11 } 12 }
剩下值得注意的就是在输出结果显示分数时的问题,代码如下:
1 void DisplayWord(Word w){//显示表达式元素 2 if(w.mode==PUN) 3 cout<<w.pun; 4 else{ 5 if(w.inte) 6 cout<<w.inte<<"'"; 7 if(!w.deo){ 8 cout<<"分母为0,算式不成立";//若分母为0则报错 9 return ; 10 } 11 cout<<w.ele; 12 if((w.deo!=1)&&(w.ele!=0))//若分母为1或分子为0则不显示分母 13 cout<<"/"<<w.deo; 14 } 15 cout<<" "; 16 }
该函数可以根据word结构体中存放的数据类型显示,若为算符则直接显示,若为数字就要进行判定了,若存在整数部分则先显示整数部分,用" ' "隔开,如果分母为0,结果不合法(当然在实际不会出现这种情况),报错,显示分子过后还要判断分母是否为1,若不是说明是分数,还要显示分号及分母,但如果分子是0就不用显示分母了。
经过测试,全部输入输出都满足条件,附截图如下:
在本次输入输出方面学到一个小技巧,输入输出重定向。
平常在使用的时候我们都是通过控制台进行输入输出,如果从文件读取或向文件写入就不够方便,这时候找到了重定向:
freopen("question.txt","w",stdout); freopen("question.txt","r",stdin);
经过重定向之后,会将所有读键盘输入,写屏幕输出的函数包括printf,scanf,cout,cin全部都改用文件,这样对文件输入输出变得非常方便,但是这个方法有个弊端,就是重定向过后没有将定向还原的方法,不过可以通过继续讲输入输出方向重定位到控制台进行实现,根据不同操作系统表示也不同,在Windows下控制台的重定位方式如下:
freopen( "CON", "w", stdout ); freopen( "CON", "r", stdin );
由此可见,该方法并不适合常用,更加泛用的方法可以使用ifstream等方法。
作为第一次个人项目感觉到非常新鲜,实践的内容过去也都有所接触,算上拓展内容却花了大概两天时间,在变量的使用以及逻辑的框架方面还不够严谨,使后来的维护和优化绕了不少弯路。而且自我感觉代码还有很多可以优化的地方,未来除了保证代码质量以外,看来还需要加快速度。下一篇将会加入拓展功能算式自动生成。