Coding.net源码仓库地址:
https://git.coding.net/Sunlight0507/2016012068Calculation.git
一、 需求分析
- 程序接收一个输入参数n,然后随机产生n道加减乘除练习题
- 每个数字在0到100之间,运算符在3到5个之间
- 每个练习题至少要包含2种运算符。同时
- 你所出的练习题在运算过程中不得出现负数与非整数,比如不能出3/5+2=2.6,2-5+10=7的算式。
- 练习题生成好后,将你的学号与生成的n道练习题及其对应的正确答案输出到文件“result.txt”中。
- 不要输出额外信息,文件目录与程序目录一致。
- 分母不能为0
二、 功能设计
基本功能:
- 接收参数n,并对参数的合法性做出判断,若不输入不符合要求则提醒用户重新输入合法数据。
- 根据用户输入的参数生成相应数量的题目并符合基本需求。
- 自动算出答案并将答案以文档的形式呈现
附加功能:
- 实现附加功能,支持带括号的四则运算。
三、 设计实现
开始设计思路图
但是在翻阅优秀的博客和同学们的博客中,以及老师们对同学们提出的问题之后,我发现这样就写复杂了,出现了大量的无效代码,如果题目变成了7个运算符我还得重新加大量代码。完全可以将出现负数、除下来是小数等问题直接放到逆波兰方法中排除,也就是不管式子生成的合不合适,先生成,在逆波兰的计算中不合适了在重新生成一个式子就可以了,这样写代码精炼,通用性比较高。
所以在之后的实现过程中,我一共设计了四个类:
Main类:为程序入口,接收命令行参数并判断参数的合法性,同时启动程序。
ProFile类:产生result.txt文件,并将学号和产生的练习题写入文件。
Operation类: 随机产生一条带括号的含3-5个运算符的四则运算式子。
Calculation类:筛选出不带负数和小数的式子,并计算答案。
用到的重要函数:
产生四则运算:problem函数,其中Random随机产生3-5个运算符,且带括号的四则运算式子。若式子不符合要求,则用递归到合法为止。
计算结果:calculate函数,在四则运算式生成后,计算每一部分的结果,且排除带有小数和负数的式子。
启动程序:main函数,接收参n,并判断其合法性。
四、 算法和代码详情
首先我感觉,不论是出这种基础的四则运算题,还是在以后做东西的时候,得先换位思考,站在用户的角度考虑一下,才能逐渐明白做出来的东西是否具有实用性以及可用性。拿这个四则运算来说,如果所有运算符都一样,那么就失去了四则运算原本的意思,所以就增添了保证一个式子里至少有两个不同运算符的方法,代码如下:
private static int[] index(int n){ //产生操作符的下标数组 Random random = new Random(); int similar=0; int[] a = new int[n]; for(int j=0;j<n;j++){ a[j] = random.nextInt(4); } for(int j=1;j<n;j++){ if(a[0]==a[j]) similar++; } if(similar==n-1) return index(n); //所有操作符下标都一样,则重新产生操作符下标,保证一个式子里至少有2个不同的操作符 else { return a; } }
其次呢,做这个项目比自己想象花费的时间长,原因的关键应该就是在于Calculation类的计算。最开始需求分析是明白了,但是究竟如何计算又陷入了疑惑。学习了优秀博客之后,便采用了调度场算法,逆波兰表达式的求值方法。
调度场算法思想:
从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素一次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
逆波兰表达式求值:
从左到右遍历后缀表达式的每个数字和字符,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈进行运算,运算结果进栈,一直到最终获得结果。
一个栈存放数字,一个栈存放操作符,至于运算符的优先级,便使用了map容器,通过对键值赋予不同大小的数值用来表示优先级的高低。代码如下:
public class Calculation { public static int calculate(String s) { Stack<Integer> stack1 = new Stack<>(); //放数字 Stack<String> stack2 = new Stack<>(); //放操作符 HashMap<String, Integer> hashmap = new HashMap<>(); //存放运算符优先级 hashmap.put("(", 0); hashmap.put("+", 1); hashmap.put("-", 1); hashmap.put("×", 2); hashmap.put("÷", 2); for (int i = 0; i < s.length();) { StringBuffer digit = new StringBuffer(); //StringBuffer类中的方法主要偏重于对于字符串的变化,例如追加、插入和删除等,这个也是StringBuffer和String类的主要区别。 char c = s.charAt(i); //将式子字符串切割为c字符 while (Character.isDigit(c)) { //判断字符是否为10进制数字,将一个数加入digit digit.append(c); i++; c = s.charAt(i); } if (digit.length() == 0){ //当前digit里面已经无数字,即当前处理符号 switch (c) { case '(': { stack2.push(String.valueOf(c));//如果是( 转化为字符串压入字符栈 break; } case ')': { //遇到右括号了计算,因为(的优先级最高 String stmp = stack2.pop(); //如果是),将符号栈栈顶元素取到 while (!stack2.isEmpty() && !stmp.equals("(")) { //当前符号栈里面还有+ - * / int a = stack1.pop(); //取操作数a,b int b = stack1.pop(); int sresulat = calculate(b, a, stmp); //计算 if(sresulat<0) return -1; stack1.push(sresulat); //将结果压入栈 stmp = stack2.pop(); //符号指向下一个计算符号 } break; } case '=': { //遇到等号了计算 String stmp; while (!stack2.isEmpty()) { //当前符号栈里面还有+ - * /,即还没有算完 stmp = stack2.pop(); int a = stack1.pop(); int b = stack1.pop(); int sresulat = calculate(b, a, stmp); if(sresulat<0) return -1; stack1.push(sresulat); } break; } default: { //不满足之前的任何情况 String stmp; while (!stack2.isEmpty()) { //如果符号栈有符号 stmp = stack2.pop(); //当前符号栈,栈顶元素 if (hashmap.get(stmp) >= hashmap.get(String.valueOf(c))) { //比较优先级 int a = stack1.pop(); int b = stack1.pop(); int sresulat =calculate (b, a, stmp); if(sresulat<0) return -1; stack1.push(sresulat); } else { stack2.push(stmp); break; } } stack2.push(String.valueOf(c)); //将符号压入符号栈 break; } } } else { //处理数字,直接压栈 stack1.push(Integer.valueOf(digit.toString())); //Integer.valueof()返回的是Integer对象,而Integer.parseInt()返回的是int型 continue; //结束本次循环,回到for语句进行下一次循环,即不执行i++(因为此时i已经指向符号了) } i++; } return stack1.peek(); //返回栈底数字即等式的答案。 } private static int calculate(int a, int b, String stmp) { //计算a stmp b的值 int res = 0; //存结果 char s = stmp.charAt(0); switch (s) { case '+': { res = a + b; break; } case '-': { res = a - b; break; } case '×': { res = a * b; break; } case '÷': { if(b==0) return -1; else if(a%b!=0) return -2; else res = a / b; break; } } return res; } }
五、测试运行
进入src文件下,输入javac -encoding utf-8 Main.java 编译出相应的class文件,再输入java Main 20进行测试:
1. 错误的输入
2. 正确的输入
五、 总结
在这次项目对于模块化这个问题,我事先还是有考虑的,所以我设计了四个类,把Main这个主类分离出来,其他类再分别写,以此实现逐级调用,使其更灵活。每个类分别实现自己的功能,更清晰明了,及时多出几个运算符也能更快的调整。
这次代码,除了模块化这个环节,其他地方感想也颇多。首先对于需求分析和功能设计,第一次看到这个题目的时候给我的感觉是之前做过类似的程序,但是当我认真分析了题目之后,我发现这个题目和我之前做过的内容都不太一样,要实现所有的功能还是有一定难度的,而且在做的过程中我由于提前没有先好好的分析题目,匆忙动手,有很多细节的问题都没有考虑清楚,在做的过程中就需要大量的返工,然后就会发现这个运算附的优先级和扩展性问题,所以我改变了方向,采取了逆波兰后缀表达式的方法来解决运算式优先级问题,我就发现在一个项目中前期的设计分析是非常重要的,只有做好基础的工作才能在后面的实现中进展顺利,但是我最后的项目还是有许多的冗余代码,代码不够精炼,也没有实现真分数的运算,在后面我也会对此次项目代码进行完善和补充,希望越来越进步。
还有就是关于学习态度上的思考了。说来惭愧,Java基础不太好的我当年还抱着考过了这门课就万事大吉了的心态,现在想来当时那种不负责任的态度对现在造成的影响可以说是很大了。作业完成非常缓慢,突然加紧学习Java, 在完成的过程中也是在自己在慢慢拾起来,在探索的基础上借鉴优秀的博客加之同学的帮助才算完工。
我真的意识到完成作业这件事赶早不赶晚,不下功夫是不会有所谓的灵光乍现的,踏踏实实做功课才是道理。
六、 PSP展示
PSP2.1 |
任务内容 |
计划完成需要的时间(min) |
实际完成需要的时间(min) |
Planning |
计划 |
10 |
15 |
· Estimate |
·估计这个任务需要多少时间,并规划大致工作步骤 |
20 |
27 |
Development |
开发 |
420 |
600 |
· Analysis |
·需求分析 (包括学习新技术) |
60 |
80 |
· Design Spec |
·生成设计文档 |
0 |
0 |
· Design Review |
·设计复审 (和同事审核设计文档) |
0 |
0 |
· Coding Standard |
·代码规范 (为目前的开发制定合适的规范) |
15 |
25 |
· Design |
·具体设计 |
50 |
60 |
· Coding |
·具体编码 |
300 |
550 |
· Code Review |
·代码复审 |
20 |
30 |
· Test |
·测试(自我测试,修改代码,提交修改) |
13 |
21 |
Reporting |
报告 |
30 |
40 |
· Test Report |
·测试报告 |
30 |
35 |
· Size Measurement |
·计算工作量 |
25 |
20 |
· Postmortem & Process Improvement Plan |
·事后总结 ,并提出过程改进计划 |
60 |
57 |