Coding.net源码仓库地址:https://git.coding.net/wanghz499/2016012032week2-2.git
测试步骤:
1.进入src文件夹
2.在命令行输入javac -encoding utf-8 Main.java
3.回车再输入java Main 20
4.回车,将会在根目录下(与src同级)产生result.txt
一、需求分析
通过对题目要求的分析,我共提取出以下7个需求(实现带括号和真分数的附加功能):
1.程序可从命令行接收一个输入参数n,然后随机产生n道加减乘除练习题。
2.每个数字在 0 和 100 之间,运算符在3个到5个之间。
3.每个练习题至少要包含2种运算符。
4.所出的练习题在运算过程中不得出现负数与非整数。
5.将学号与生成的n道练习题及其对应的正确答案输出到文件“result.txt”中。
6.支持有括号的运算式,包括出题与求解正确答案。算式中存在的括号必须大于2个,且不得超过运算符的个数。
7.支持真分数的加减法,并且每个分数都化到最简
二、功能设计
能够根据用户输入的参数n随机产生n道符合要求的练习题,自动算出答案,并将式子与答案以文档的形式呈现。并实现附加功能:支持有括号的运算、支持真分数的加减运算。
三、设计实现
我共设计了5个类,如图:
Creat类:负责随机产生一条带括号的至少2种运算符四则运算的式子,且有3-5个运算符
Calculator类:负责筛选运算过程中不产生负数和小数的式子,并计算答案
MakeFile类:负责产生result.txt文件,并将学号和产生的练习题写入文件
properFraction类:负责产生真分数式子并计算答案
Main类:主类,负责接收命令行的参数并启动程序
5个类的相互调用关系为:
比较重要的函数:
Creat类:creatProblem():随机产生一条带括号的含3-5个运算符四则运算式子,
若式子不符合条件,会递归直到产生符合条件的式子。
index(int n):产生运算符下标数组,并保证至少有2个不同的运算符。
Calculator类:
algorithm(String s):结合了调度场算法和逆波兰表达式的求值,计算出式子的答案。
calculate(int a,int b,String stmp):计算式子每一部分的运算,排除运算过程中出现小数和负数的式子。
ProperFraction类:
createProblem():随机产生一条含3-5个运算符的真分数加减运算式子,并计算出结果。
greatFactor(int x,int y):求最大公因数,用于化简
函数间的逻辑关系:creatProblem()调用index(int n)和algorithm(String s),algorithm(String s)调用calculate(int a,int b,String stmp),还有生成文件相关的方法就不列举了。
四、算法详解
本项目的关键在于Calculator类的计算,结合了调度场算法和逆波兰表达式(即后缀表达式)的求值,一步实现计算四则运算式子。
关于调度场算法和逆波兰表达式求值,我花费了大量时间浏览博客理解它的实现过程,现总结如下:
调度场算法的作用是将中缀表达式变为后缀表达式,它需要一个队列来装后缀表达式和一个栈来装符号。先从左到右遍历中缀表达式的每个符号和数字,若是数字就入队;若是符号,则判断其与栈顶符号的优先级,若该符号是右括号或其优先级低于或等于栈顶符号,则栈顶元素依次出栈并输出进入队列,并将当前符号进栈,一直到最终输出后缀表达式。
逆波兰表达式求值步骤(只需要一个栈):
1.先初始化一个空栈,开始遍历后缀表达式。
2.如果字符是一个操作数,则令其入栈。
3.如果字符是个运算符,则弹出栈里的两个操作数(一定会有两个数在栈里,因为是后缀表达式),进行运算,再把结果入栈。
4.到后缀表达式末尾,从栈中弹出结果。
理解了这两个算法后,就可将两个算法结合,只需两个栈就可一次性求出答案:
1.初始化两个栈,分别是数字栈和符号栈。
2.遍历中缀表达式,如果是数字,则入数字栈。
3.如果是符号,则判断其与符号栈的栈顶符号的优先级。若当前符号是右括号或其优先级低于或等于栈顶符号,则栈顶元素依次出符号栈,并在数字栈弹出两个数进行相应运算,再使结果入数字栈,当前符号也入符号栈。
4.当遇到等号,则将符号栈里的符号依次出栈,从数字栈弹出两个数进行相应运算,再把结果入数字栈,直到最后一个符号出栈。把数字栈的数字弹出,即为结果。
至于符号的优先级,则使用Hashmap<String,int>建立多组键值对,使每个符号对应一个数值,数值越高说明优先级越高。
优先级从小到大:)小于 + - 小于 ×÷ 小于 (
五、测试运行
进入src文件夹,在命令行输入javac -encoding utf-8 Main.java 将类编译成class文件,再输入java Main 20 运行class文件,这里先做一个非法输入和越界测试,如输入java Main e或java Main 1200:
再正常输入如java Main 20,将会在根目录下(与src同级)产生result.txt文件:
测试完成!
六、代码展示
产生整数式子的方法:
public static String createProblem(){ Random random = new Random(); String[] operator = {"+","-","×","÷"}; int operatorCount = 3+random.nextInt(3); //操作符的个数3-5 int[] num = new int[operatorCount+1]; //操作数的个数比操作符多1 int[] index = index(operatorCount); //操作符的下标 String s = new String(); for(int j=0;j<operatorCount+1;j++){ num[j] = random.nextInt(101); //产生0-100范围的操作数,random.nextInt(n)的取值范围是[0,n) } int choose = random.nextInt(2); //选择式子括号形态 switch (operatorCount){ case 3:{ if(choose==0){ s=num[0]+operator[index[0]]+"("+"("+num[1]+operator[index[1]]+num[2]+")"+operator[index[2]]+num[3]+")";//1+((2×3)-4)型 }else s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×(3+4)型 break; } case 4:{ if(choose==0){ s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+num[4]+operator[index[3]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×3÷(4-1)型 }else s=num[4]+operator[index[3]]+"("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//3×(1+2)+(4÷2)型 break; } case 5:{ if(choose==0){ s="("+num[0]+operator[index[0]]+num[1]+operator[index[4]]+num[5]+")"+operator[index[1]]+"("+num[4]+operator[index[3]]+num[2]+")"+operator[index[2]]+num[3];//(6+2×3)-(1+2)×3型 }else s="("+num[0]+operator[index[0]]+"("+num[1]+operator[index[1]]+num[2]+operator[index[2]]+num[3]+")"+")"+operator[index[3]]+"("+num[4]+operator[index[4]]+num[5]+")";//(1+(2×3+4))-(6÷3)型 break; } } s+="="; //给式子加上等号 int answer = Calculator.calculate(s); if(answer>=0){ //判断式子是否符合要求,凡是返回负数的就是不合格的 s+=answer; }else { return createProblem(); //递归,直到产生合格的式子 } return s; }
保证式子里至少有2个不同操作符的方法:
private static int[] index(int n,int m){ //产生操作符的下标数组 Random random = new Random(); int similar=0; int[] a = new int[n]; for(int j=0;j<n;j++){ a[j] = random.nextInt(m); } for(int j=1;j<n;j++){ if(a[0]==a[j]) similar++; } if(similar==n-1) return index(n); //保证一个式子里至少有2个不同的操作符,若所有操作符下标都一样,则重新产生操作符下标 else { return a; } }
产生真分数式子并计算的方法:
public String createProblem(){ Random random = new Random(); String[] operator = {"+","-"}; int operatorCount = 3+random.nextInt(3); //操作符的个数3-5 Create create = new Create(); int[] index = create.index(operatorCount,2); //操作符的下标 int sumx = 1+random.nextInt(10); //第一个数的分子1-10 int sumy = 1+random.nextInt(10);//第一个数的分母1-10 int greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; //化简 sumy/=greatFactor; while (sumx>=sumy){ sumx = 1+random.nextInt(10); sumy = 1+random.nextInt(10); greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; sumy/=greatFactor; } String s=sumx+"/"+sumy; //第一个数 for(int i=0;i<operatorCount;i++){ int numx = random.nextInt(25); //分子分母不宜过大 int numy = 1+random.nextInt(25); //否则通分可能会产生很大的数导致溢出 String currentOpreator = operator[index[i]]; while (numx>=numy){ //当分子大于分母,即假分数,则重新生成 numx = random.nextInt(25); numy = 1+random.nextInt(25); greatFactor = greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } if(currentOpreator.equals("+")){ //加法 while(sumx*numy+sumy*numx>sumy*numy) //和为假分数 { numx=random.nextInt(25); numy=1+random.nextInt(25); greatFactor=greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } sumx=sumx*numy+sumy*numx; sumy=sumy*numy; } else { //减法 while(sumx*numy-sumy*numx<0) //差为负数 { numx=random.nextInt(25); numy=1+random.nextInt(25); greatFactor=greatFactor(numx,numy); numx/=greatFactor; numy/=greatFactor; } sumx=sumx*numy-sumy*numx; sumy=sumy*numy; } s+=currentOpreator+numx+"/"+numy; } greatFactor = greatFactor(sumx,sumy); sumx/=greatFactor; //最终结果化简 sumy/=greatFactor; if(sumx==0) s+="="+sumx; else if(sumx==1&&sumy==1) s+="="+sumx; else s+="="+sumx+"/"+sumy; return s; }
判断非法输入和越界输入的方法(主方法):
public static void main(String[] args) { int n = 0; try { n = Integer.parseInt(args[0]); if(n>1000||n<1){ System.out.println("对不起,只允许输入1-1000的数字!"); return; //结束运行 } }catch (NumberFormatException e){ //输入非数字字符等 System.out.println("对不起,只允许输入1-1000的数字!"); return; //结束运行 } MakeFile.creatFile(n); }
其他代码请见coding.net,就不一一展示了。
六、PSP
SP2.1 |
任务内容 |
计划共完成需要的时间(h) |
实际完成需要的时间(h) |
Planning |
计划 |
26 |
47 |
· Estimate |
· 估计这个任务需要多少时间,并规划大致工作步骤 |
26 |
47 |
Development |
开发 |
20 |
40 |
· Analysis |
· 需求分析 (包括学习新技术) |
3 |
5 |
· Design Spec |
· 生成设计文档 |
0 |
0 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
0 |
0 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
0 |
0 |
· Design |
· 具体设计 |
3 |
5 |
· Coding |
· 具体编码 |
10 |
15 |
· Code Review |
· 代码复审 |
2 |
5 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
2 |
10 |
Reporting |
报告 |
6 |
7 |
· Test Report |
· 测试报告 |
5 |
6 |
· Size Measurement |
· 计算工作量 |
0.5 |
0.5 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
0.5 |
0.5 |
七、总结
这次项目比我想象中的要难,原以为一两天就可以搞定,实际上花了整整4天时间在图书馆钻研。其实做这个项目我并没有完整地按照软件开发的步骤,也没有提前设计代码,想到哪就写到哪,导致敲代码的过程中遇到很多小问题,一遇到问题就得停下来去找相应的解决方法,经常是代码写了又删,删了又写,效率比较低,下次一定要事先设计,想好整个流程再写代码。此外,通过这次作业,我感受到了算法在项目中的重要性,算法是一个项目的灵魂。就如调度场算法和逆波兰表达式求值算法是这次作业的核心,我再也不敢说类似于不知算法有什么用之类的话了。
原本我是没有实现分数加减的附加功能的,因为我潜意识里觉得它很难,所以压根没想过也不敢做这个附加功能。但是看了其他同学的博客后,发现很多同学都实现了,我分析他们的代码,突然觉得也不是很难了,几经思考后我最终也实现了分数的功能,开心不已。我想写博客的意义就在此吧,互相分享自己的学习成果,共同进步。很感谢那些愿意写博客分享技术的人,从他们的博客中真的可以学到很多东西!
这4天,看了一篇又一篇博客,改了一段又一段代码,很疲惫却也很充实,让我感受到了全身心投入一件事情的快乐,专注与钻研,我喜欢这样的感觉。同时,也让我意识到自己的水平远比想象中的低,做一个四则运算就让我费了这么大的劲,说明我的水平真的还不够,我要好好努力。最后,还是忍不住分享独自完成一个小项目的喜悦,真的很开心,也给了我很大的鼓励!专注的感觉真好!