作业 20180925-6 四则运算试题生成
此作业的要求参见:https://edu.cnblogs.com/campus/nenu/2018fall/homework/2148
结对伙伴:张俊余
使用语言:C#
要求1
一、给出每个功能的重点、难点、编程收获。
功能一:随机生成二十道四个数的四则运算
1.重点、难点:
(1)随机生成题目:生成四个随机数和三个随机运算符,连接即可。
(2)计算结果:求其逆波兰式,然后计算结果。
(3)对比用户输入的结果与实际结果,进行交互:根据二者的异同进行交互。
重要代码展示:
交互
1 userResult = Convert.ToDecimal(Console.ReadLine()); 2 if (userResult == trueResult) 3 { 4 trueNum++; 5 Console.WriteLine("答对啦,你真是个天才!"); 6 } 7 else 8 { 9 Console.WriteLine("再想想吧,答案似乎是" + trueResult.ToString() + "喔!"); 10 } 11 } 12 13 Console.WriteLine("你一共答对" + trueNum.ToString() + "道题,共20道题。");
2.编程收获:
功能一实现较简单,生成四个随机数和三个随机运算符即可随机生成题目,计算时求其逆波兰式即可。我的编程能力较差,张俊余帮我梳理了程序具体功能后,也很快得以实现。
功能二:在功能一基础上添加括号功能
1.重点、难点:
(1) 加括号:由于功能二是在功能一的基础之上,所以部分不设置括号,另一部分含括号,考虑到只有四个运算数,故可设置含0个,1个,2个括号的表达式,分析可知共有十种可能性,此处采用暴力枚举的方式生成题目。
(2)根据中缀表达式求后缀表达式(逆波兰式): 数据结构教材或网上有很多相关介绍,在此不赘述算法 ,详细可见代码。
(3)求逆波兰式结果:思路是新建一个栈R用来放结果,依次取出表达式的栈顶,
如果取出的值为操作数,则放入栈R;如果取出的值为运算符,则取出栈R顶的两个,进行运算,运算结果在存放入栈R。最后R顶的元素,即为计算的值。
重要代码展示:
(1) 加括号
1 switch (flag % 10) 2 { 3 case 0: // (1+2+3)+4 4 equation = "(" + " " + nums[0] + " "+ operators[0] + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + ")" + " " + operators[2] + " " + nums[3] + " " ; 5 equationCal = "(" + nums1[0] + operators[0] + nums1[1] + operators[1] + nums1[2] + ")" + operators[2] + nums1[3]; 6 break; 7 case 1: // 1+(2+3+4) 8 equation = nums[0] + " " + operators[0] + " " + "(" + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + operators[2] + " " + nums[3] + " " + ")" + " " ; 9 equationCal = nums1[0] + operators[0] + "(" + nums1[1] + operators[1] + nums1[2] + operators[2] + nums1[3] + ")"; 10 break; 11 case 2: // (1+2)+3+4 12 equation = "(" + " " + nums[0] + " " + operators[0] + " " + nums[1] + " " + ")" + " " + operators[1] + " " + nums[2] + " " + operators[2] + " " + nums[3] + " " ; 13 equationCal = "(" + nums1[0] + operators[0] + nums1[1] + ")" + operators[1] + nums1[2] + operators[2] + nums1[3]; 14 break; 15 case 3: // 1+(2+3)+4 16 equation = nums[0] + " " + operators[0] + " " + "(" + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + operators[2] + " " + nums[3] + " " + ")" + " " ; 17 equationCal = nums1[0] + operators[0] + "(" + nums1[1] + operators[1] + nums1[2] + operators[2] + nums1[3] + ")"; 18 break; 19 case 4: // 1+2+(3+4) 20 equation = nums[0] + " " + operators[0] + " " + nums[1] + " " + operators[1] + " " + "(" + nums[2] + " " + operators[2] + " " + nums[3] + " " + ")" + " " ; 21 equationCal = nums1[0] + operators[0] + nums1[1] + operators[1] + "(" + nums1[2] + operators[2] + nums1[3] + ")"; 22 break; 23 case 5: // (1+2)+(3+4) 24 equation = "(" + " " + nums[0] + " " + operators[0] + " " + nums[1] + " " + ")" + " " + operators[1] + " " + "(" + nums[2] + " " + operators[2] + " " + nums[3] + " " + ")" + " " ; 25 equationCal = "(" + nums1[0] + operators[0] + nums1[1] + ")" + operators[1] + "(" + nums1[2] + operators[2] + nums1[3] + ")"; 26 break; 27 case 6: // ((1+2)+3)+4 28 equation = "(" + " " + "(" + " " + nums[0] + " " + operators[0] + " " + nums[1] + " " + ")" + " " + operators[1] + " " + nums[2] + ")" + " " + operators[2] + " " + nums[3] + " " ; 29 equationCal = "(" + "(" + nums1[0] + operators[0] + nums1[1] + ")" + operators[1] + nums1[2] + ")" + operators[2] + nums1[3]; 30 break; 31 case 7: // (1+(2+3))+4 32 equation = "(" + " " + nums[0] + " " + operators[0] + " " + "(" + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + ")" + " " + ")" + " " + operators[2] + " " + nums[3] + " " ; 33 equationCal = "(" + nums1[0] + operators[0] + "(" + nums1[1] + operators[1] + nums1[2] + ")" + ")" + operators[2] + nums1[3]; 34 break; 35 case 8: // 1+((2+3)+4) 36 equation = nums[0] + " " + operators[0] + " " + "(" + " " + "(" + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + ")" + " " + operators[2] + " " + nums[3] + " " + ")" + " " ; 37 equationCal = nums1[0] + operators[0] + "(" + "(" + nums1[1] + operators[1] + nums1[2] + ")" + operators[2] + nums1[3] + ")"; 38 break; 39 case 9: // 1+(2+(3+4)) 40 equation = nums[0] + " " + operators[0] + " " + "(" + " " + nums[1] + " " + operators[1] + " " + "(" + " " + nums[2] + " " + operators[2] + " " + nums[3] + " " + ")" + " " + ")" + " " ; 41 equationCal = nums1[0] + operators[0] + "(" + nums1[1] + operators[1] + "(" + nums1[2] + operators[2] + nums1[3] + ")" + ")"; 42 break; 43 default: 44 Console.WriteLine("Error"); 45 break; 46 }
(2)逆波兰
1 class ReversePolishEquation 2 { 3 public static int GetOperationLevel(string c) // 定义运算符优先级 4 { 5 switch (c) 6 { 7 case "+": return 1; 8 case "-": return 1; 9 case "*": return 2; 10 case "/": return 2; 11 case "#": return -1; 12 case "(": return -1; 13 case ")": return -1; 14 default: return 0; 15 } 16 } 17 18 public static int GetCompleteValue(string formula) //获取完整操作数 19 { 20 int index = formula.Length; 21 for (int i = 0; i < formula.Length; i++) 22 { 23 int num = GetOperationLevel(formula[i].ToString()); 24 if (num != 0) 25 { 26 index = i; 27 break; 28 } 29 } 30 return index; 31 } 32 33 public static void MoveOperator(Stack<string> opStack, Stack<string> numStack) // 移动运算符 34 { 35 string s = opStack.Pop(); 36 if (s == "(") 37 { 38 return; 39 } 40 else 41 { 42 numStack.Push(s); 43 MoveOperator(opStack, numStack); 44 return; 45 } 46 } 47 48 public static void JudgeOperator(Stack<string> opStack, Stack<string> numStack, string x)//判断运算符 49 { 50 int xNum = GetOperationLevel(x); 51 int opNum = GetOperationLevel(opStack.Peek()); 52 if (xNum > opNum || numStack.Peek() == "(") 53 { 54 opStack.Push(x); 55 return; 56 } 57 else 58 { 59 string opStr = opStack.Pop(); 60 numStack.Push(opStr); 61 JudgeOperator(opStack, numStack, x); 62 return; 63 } 64 } 65 66 public Stack<string> getReversePolish(string equation) 67 { 68 //equation = "(1+2)*3"; 69 Stack<string> opStack = new Stack<string>(); // 定义运算符栈 70 opStack.Push("#"); 71 Stack<string> numStack = new Stack<string>(); // 定义操作数栈 72 for(int i = 0; i < equation.Length;) 73 { 74 int opNum = GetOperationLevel(equation[i].ToString()); 75 if (opNum == 0) 76 { 77 int index = GetCompleteValue(equation.Substring(i, equation.Length - i)); 78 numStack.Push(equation.Substring(i, index)); 79 i = (i + index); 80 } 81 else 82 { 83 if (equation[i] == '(') 84 { 85 opStack.Push(equation[i].ToString()); 86 } 87 else if (equation[i] == ')') 88 { 89 MoveOperator(opStack, numStack); 90 } 91 else 92 { 93 if (opStack.Peek() == "(") 94 { 95 opStack.Push(equation[i].ToString()); 96 } 97 else 98 { 99 JudgeOperator(opStack, numStack, equation[i].ToString()); 100 } 101 } 102 i++; 103 } 104 } 105 if (opStack.Count != 0) 106 { 107 while (opStack.Count != 0 && opStack.Peek() != "#") 108 { 109 numStack.Push(opStack.Pop()); 110 } 111 } 112 return numStack; 113 } 114 }
(3)计算
1 class CalculatePolishEquation 2 { 3 public Stack<string> getRpnEquation(Stack<string> numStack) 4 { 5 Stack<string> rpnEquation = new Stack<string>(); // 逆波兰 6 foreach (string s in numStack) 7 { 8 rpnEquation.Push(s); 9 } 10 return rpnEquation; 11 } 12 13 public string CalcRPNFormula(Stack<string> rpnFormula) 14 { 15 string result = ""; 16 Stack<string> resultStack = new Stack<string>(); 17 foreach (string s in rpnFormula) 18 { 19 int num = GetOperationLevel(s); 20 if (num == 0) 21 { 22 resultStack.Push(s); 23 } 24 else 25 { 26 CalcResult(resultStack, s); 27 } 28 } 29 result = resultStack.Pop(); 30 //Console.WriteLine(result); 31 return result; 32 } 33 34 public static int GetOperationLevel(string c) // 定义运算符优先级 35 { 36 switch (c) 37 { 38 case "+": return 1; 39 case "-": return 1; 40 case "*": return 2; 41 case "/": return 2; 42 case "#": return -1; 43 case "(": return -1; 44 case ")": return -1; 45 default: return 0; 46 } 47 } 48 49 private static void CalcResult(Stack<string> resultStack, string operatorStr) 50 { 51 if (resultStack.Count >= 2) 52 { 53 decimal num2 = Convert.ToDecimal(resultStack.Pop()); 54 decimal num1 = Convert.ToDecimal(resultStack.Pop()); 55 if (operatorStr == "+") 56 { 57 resultStack.Push(Convert.ToString(num1 + num2)); 58 } 59 else if (operatorStr == "-") 60 { 61 resultStack.Push(Convert.ToString(num1 - num2)); 62 } 63 else if (operatorStr == "*") 64 { 65 resultStack.Push(Convert.ToString(num1 * num2)); 66 } 67 else if (operatorStr == "/") 68 { 69 if(num2 != 0) 70 { 71 resultStack.Push(Convert.ToString(num1 / num2)); 72 } 73 else 74 { 75 resultStack.Push(Convert.ToString(1)); 76 } 77 } 78 } 79 } 80 }
2.编程收获
求逆波兰式在本科期间数据结构时学习过,我和张俊余翻了本科和考研的数据结构教材又在网上查阅了之后,理解了求逆波兰式并计算其值的算法。生成括号时,我俩决定枚举括号位置的可能性,一开始只考虑到不含括号和含一个括号的情况,后来俩人讨论后发现还有可能是两个括号即共十种可能性。
功能三:限定题目数量,"精美"打印输出,避免重复
1.重点、难点:
(1)命令行参数限定题目数量:正则判断用户输入的数字是否合法,合法则开始输出指定数目的题目。
(2)打印输出内容写入文件:在网上搜索学习后决定使用C#语言的StreamWriter类,StreamWriter (String, Boolean, Encoding) // String指定路径。true表示如果该文件存在,则可以向其追加。false表示将其全部重写。如果该文件不存在,则此构造函数将创建一个新文件。Encoding表示编码。此处按照功能需求布尔值为false。
(3)避免重复:重复有两种可能性,一种是完全重复,这种情况直接判断表达式字符串即可。第二种可能性是经过有限次结合律、交换律和分配律后一致。我们在随机数生成时规避了0,因此避免了分配律的影响。至于结合律和交换律,我们没有特别好的处理思路,询问上一届师兄得知他们判断题目生成结果,只要一致就不出该题,我们认为当出题量较大时,此种处理方法并不可取,因此我们在此处决定“只要两个表达式四个运算数(不考虑顺序)、三个运算符(不考虑顺序)、最终结果均一致的情况下”就丢弃该题目。也算是能比较好的规避交换律和结合律的影响。
重要代码展示:
(1)命令行参数判断
1 else if (args.Length == 2) 2 { 3 string num = args[1]; 4 //Console.WriteLine(num); 5 if (isNumeric(num)) 6 { 7 int equationNums = Convert.ToInt32(num); 8 // Console.WriteLine("success"); 9 ProduceEquation produceEquation = new ProduceEquation(); 10 produceEquation.produceEquations(equationNums); 11 } 12 else 13 { 14 Console.WriteLine("题目数量必须是 正整数。"); 15 } 16 } 17 18 static bool isNumeric(string value) // 判断第二个命令行参数是否为正整数 19 { 20 return Regex.IsMatch(value, @"^[+]?\d+$"); 21 // return true; 22 }
(2)写入文件
1 class ProduceFiles 2 { 3 public void produceFiles(string filename,List<string> equations) 4 { 5 StreamWriter streamWriter = new StreamWriter(filename, false, Encoding.Default); 6 for(int i = 0; i < equations.Count;i++) 7 { 8 streamWriter.WriteLine(equations[i]); 9 } 10 streamWriter.Flush(); 11 streamWriter.Close(); 12 } 13 }
(3)避免重复出题
1 for (j = 0; j < results.Count;) // 判断是否存在相等的方程式 2 { 3 if (trueResult == results[j]) // 判断结果是否相等 4 { 5 if (compareArray(operatorNums[j], operatorNum))// 判断操作数是否相同 6 { 7 if (compareArray(operatorOnes[j], operatorOne))// 判断操作符是否相同 8 { 9 break; 10 } 11 } 12 } 13 j++; 14 } 15 if (j >= results.Count) // 如果不存在则正常生成 16 { 17 Console.WriteLine(output + trueResult); 18 equations.Add(output + trueResult); 19 results.Add(trueResult); 20 operatorNums.Add(operatorNum); 21 operatorOnes.Add(operatorOne); 22 } 23 else // 如果存在则重新生成 24 { 25 i--; 26 continue; 27 }
2.编程收获
命令行参数在此前词频统计作业刚学习过,因此并不困难。输出内容写入到文本文件中我和张俊余各自在网上学习后也很快找到了解决方案。对于去重,我俩相互商量了很久,也询问了师兄,最后结合多人的想法有了我们的思路,不得不说集思广益还是挺不错的。
功能四:支持分数出题和运算
1.重点、难点:
(1)分数出题与计算:由于功能四是在前三个功能的基础之上,因此出题的时候要设置带括号,不带括号,含有分数,不含有分数等各种情况。此处我们随机生成一个flag,平分其为整数还是分数的可能性,平分其含括号不含括号的可能性。含括号依旧是枚举出所有可能然后随机数进行选择。计算的时候我们在输出显示为分数形式,实际计算时先将其转化为小数形式,这样就可以重用前面的计算代码。
(2)分子分母分界符与除号的混淆:这个问题是在我们的测试工作中出现的,当连续有三个”/”时很难判断究竟是除法还是分数,若均是除法则一直除以对应数字,若是除以一个分数实则除以后两个数字生成的分数,例如 8/4/2 可以理解为8除以4除以2等于1,也可以理解为8除以二分之四等于4(当然还有别的可能),张俊余提出一个好的思路,表达式的展示在运算数与运算符之间加一个空格,而在实际计算中无需加空格(分数形式显示,小数形式计算),这样就比较好的解决了用户在做题目时的迷惑。
(3)小数转化为分数:将小数点前后两位划分,小数点前若不为零则为带分数的整数部分,小数点后部分通过,约分其数据与其对应的权重即可,eg 0.678,即约分678与1000.
重要代码展示:
(1)分数出题
1 int flag = rm.Next(1, 11); 2 switch (flag % 2) 3 { 4 case 0: 5 number1 = rm.Next(1, 20).ToString(); 6 randomFractions[i] = number1; 7 randomRealNum[i] = number1; 8 break; 9 case 1: 10 number1 = rm.Next(1, 20).ToString(); 11 number2 = rm.Next(1, 20).ToString(); 12 randomFractions[i] = number1 + "/" + number2; 13 randomRealNum[i] = (Convert.ToDecimal(number1)/Convert.ToDecimal(number2)).ToString(); 14 break; 15 }
(2)解决混淆"/"的问题
1 if (flag == 0) 2 { 3 equation = nums[0] + " " + operators[0] + " " + nums[1] + " " + operators[1] + " " + nums[2] + " " + operators[2] + " " + nums[3] + " " ; // 用于输出 4 equationCal = nums1[0] + operators[0] + nums1[1] + operators[1] + nums1[2] + operators[2] + nums1[3]; // 用于计算 5 } 6 else 7 { 8 switch (flag % 10)
(3)小数转化为分数
1 class DecimalToFraction 2 { 3 public string decimalTranverse(string value) // 小数转化为分数 4 { 5 string result = ""; 6 string[] str = value.Split('.'); 7 int decimalLen = str[1].Length; 8 if (Regex.IsMatch(str[1], @"^[0]*$")) 9 { 10 return str[0]; 11 } 12 long weitght = Convert.ToInt32(Math.Pow(10, decimalLen)); 13 long num = Convert.ToInt32(str[1]); 14 long gcd = gCD(num, weitght); 15 if (Regex.IsMatch(str[0], @"^[+-]?[0]*$")) 16 { 17 result = String.Format("{0}{1}{2}", num / gcd, "/", weitght / gcd); 18 } 19 else 20 { 21 result = String.Format("{0}{1}{2}{3}{4}", str[0], " ", num / gcd, "/", weitght / gcd); 22 23 } 24 25 return result; 26 } 27 28 public long gCD(long m, long n) //求最大公约数 29 { 30 long r, t; 31 if (m < n) 32 { 33 t = n; 34 n = m; 35 m = t; 36 } 37 while (n != 0) 38 { 39 r = m % n; 40 m = n; 41 n = r; 42 43 } 44 return (m); 45 } 46 47 }
2.编程收获
我们在功能4遇到了很多困难,初步实现功能4后经过测试发现了很多错误,一直在单元测试,张俊余的编程能力比较强,我主要负责测试,我及时测试告知他,俩人讨论解决方案后张俊余总是迅速给出期待结果。在此处也感谢张俊余同学在本次作业中给我的帮助与启迪。
二、给出结对编程的体会
起初老师说本课程会有结对编程作业,由于我和张俊余是本科同学,因此俩人提前打好了招呼准备线上结对编程(构建之法有提到可以这样进行),后来老师要求二人必须面对面编程后,我俩就利用国庆假期一起在食堂或者实验室进行结对编程。
张俊余的编程能力远在我之上,所以此次作业大部分归功于他。我主要负责查阅一些关于实现功能的资料和单元测试,我的理解能力编程能力一般,感谢他一直帮助我理解该项目。结对编程对我来说收获就是可以及时分享自己的想法也可以互相沟通寻找解决问题的方法,也减轻了个人开发的工作量。
三、至少5项在编码、争论、复审等活动中花费时间较长,给你较大收获的事件。 (10)
(1)功能分析:正式编码之前确定每个小功能的具体细节,制定出测试样例。虽然这个花费时间比较长,但是很好的帮助了我们确定编码目标,也方便了我们最终检验程序完成度。
(2)学习求逆波兰式和计算逆波兰式:这原本是数据结构的内容,但是俩人都忘得差不多了,而此算法对于我们的程序是必须的,因此花费了时间也是非常值得的,我们不仅复习了算法也实现了功能。
(3)学习单元测试并测试功能4:此前我使用过JUnit框架,此次使用NUnit框架,学习了冉华学长的博客后进行单元测试,单元测试花费时间特别特别久,但也正是单元测试我们发现了功能4的很多bug,所以收获不小。当然也学习了一种新的测试框架。
(4)版本控制:我俩原本计划全程只用一台电脑,后来因为个别时候某个人独处时突然有个好的思路就会写下来,所以俩人在merge的时候出现了一点困难,花费了较长时间研究git。这应该对我们日后的团队开发积累了经验。
(5)研究如何避免生成重复问题:解决细节我在前面已经提到。我们的做法虽然并不是很好,但是俩人都积极地进行了思考,个人觉得在功能上还是比较有效的。
要求二:结对编程照片一张
编程地点:信息科学与技术学院232机房 数据可视化与可视分析实验室
使用实验室电脑进行编程(有时候实验室不开门就在食堂用张俊余电脑编程)
要求三:版本控制
项目git地址为:https://git.coding.net/zhangjy982/ArithMetic.git