结对对象:许峰铭
一.Github项目地址:https://github.com/Leungdc/Leungdc/tree/master/%E5%9B%9B%E5%88%99%E8%BF%90%E7%AE%97/ASMDframe
二.PSP表格
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
560 |
1440 |
· Estimate |
· 估计这个任务需要多少时间 |
120 |
180 |
Development |
开发 |
2000 |
2160 |
· Analysis |
· 需求分析 (包括学习新技术) |
360 |
360 |
· Design Spec |
· 生成设计文档 |
120 |
180 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
60 |
120 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
60 |
90 |
· Design |
· 具体设计 |
150 |
300 |
· Coding |
· 具体编码 |
2000 |
2160 |
· Code Review |
· 代码复审 |
60 |
60 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
300 |
540 |
Reporting |
报告 |
120 |
180 |
· Test Report |
· 测试报告 |
30 |
30 |
· Size Measurement |
· 计算工作量 |
20 |
20 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
20 |
20 |
合计 |
|
5980 |
7840 |
三. 效能分析
对于改进程序效能都是在刚开始构思该功能如何实现的时候想好的,通过实现改功能想法的相互对比,然后留下最好的方法,功能实现后就没有对方法进行很大的变动。在实现运算逻辑这一功能的时候是花费时间最多的,总共花费了两天的时间去实现这一功能,这一功能要考虑符号的优先级,包括括号和操作符等,由于使用的是栈存取数,要考虑存入的数据,和取出的方式。但栈只能先进后出,不能先取出头部数据,后改为用链栈进行数据的存储,从而使功能得以实现。
为了提速而改变数据结构的耗时:≈总耗时*0.2
1.该课设中大量使用StringBuilder类型数据而不是String是因为有对字符串频繁的增删查操作,节省了许多耗时
2.查重算法中本是用出栈的方式一个一个出栈然后对比查重,但是考虑到时间较慢,就用了String类型数组来存放整个中间结果数据来对比查重
最耗时函数:calculate()函数
calculate函数是对两个数字的解析函数,因为操作符两边的数字使用String来存放的,首先calculate函数需要解析该数据的数据类型(是整数还是分数)
然后化为假分数,再根据不同的操作符做不同的运算,其中该函数执行一次要调用至少六次其他方法。
四.设计实现过程
1.生成随机数算法
生成随机数看似简单,但是涉及到的方法却不少,主要包括三种数 整数 带分数 真分数
整数:直接用random方法生成
带分数和分数:首先要将分子n和分母m分开生成,在这生成之间
第一个要注意的点是:n%m!=0;1.屏蔽了分子和分母相等的情况 2.屏蔽了分子除与分母为整数的情况
第二个要注意的点是:要对分子分母进行通分,我们的代码就使用了欧几里得方法求最大公约数,然后约分
第三个要注意的点是:若n>m,则是带分数,先要用一个carrier来表示带分数的“带”,然后n=n%m;
第四个要注意的点是:对于特别情况的屏蔽,例如m=1或m=0或n=0或carrier=0;都是不可取的
2.两个数的运算
只研究两个数的运算而不是三个数的运算,是因为在确定好运算逻辑的前提下,我们可以多次地调用运算两个数的方法,来得到我们最后的答案,下面介绍两个数运算的主要思路:
1.采用假分数的计算方法;首先将所有的数转换成假分数如5=5/1, 6'7/8=55/8, 然后分别取这两个数的分子分母,根据不同的运算符,做不同的运算
如:5÷6'7/8 第一步:5=5/1,n1=5,m1=1 6'7/8=55/8,n2=55 m2=8 第二步:N=n1*m2=5*8 M=m1*n2 最后再把n和m之间通分,若是带分数就转为带分数。
由于这个运算方法要求可以服务于输出最终结果,所以运算的中间结果我们都用了题目要求的最终结果的格式(1或5'3/5或7/8)
3.随机运算式的生成
1.先生成随机个运算符n
2.再生成随机个运算数字m
3.在1和2的基础上插空生成括号
经研究,包括无括号的情况,共有对于3个运算符一共有13种括号的生成可能,分别为:
一、1运算符2数字 不能生成括号
二、2运算符3数字 只能是单括号,包含以下情况
1. (1+2)+3
2. 1+(2+3)
三、3运算符4数字 单括号或双括号,包含以下情况
单括号:
3. (1+2)+3+4
4. (1+2+3)+4
5. 1+(2+3)+4
6. 1+(2+3+4)
7. 1+2+(3+4)
双括号:
8. (1+2)+(3+4)
9. ((1+2)+3)+4
10. (1+(2+3))+4
11. 1+((2+3)+4)
12. 1+(2+(3+4))
13.1+2+3+4(无括号情况)
我们采用枚举方式列出所有括号情况然后生成
5.查重算法
经过对四则运算的运算顺序的反复推敲后,我们得出了以下的运算优先级。
靠左边的括号内运算>靠右边的括号内运算>最靠左的乘法运算=最靠左的除法运算>靠右的乘法运算=靠右的除法运算>从左至右运算>最靠左加法运算=最靠左减法运算>靠右的
加法运算=靠右的减法运算;
查重算法思路:
①从左至右扫描式子的第一个左括号,并记录位置,若无,则直接跳到③
②从左至右扫描式子的第一个右括号,截取①中左括号位置到②中第一个右括号位置之间的式子,并递归返回①,直到跳到③为止
③判断括号内(或无括号内)操作符的优先级高低,对最高优先级操作符的左右两边的数字进行运算,得到中间值,第一操作符a,将操作符和操作的两个数字压进过程栈,再判断
括号内(或无括号内)次优先级的操作符是什么,再算出中间结果,再将操作符合操作的两个数字押进过程栈,最后对比所有式子的过程栈是否存在相同的,若有,则重新生成式子
例如1+2+3的过程栈为+21+33;而3+2+1的过程栈为+32+51;又如3+(2+1)的过程栈为+21+33;与第一个式子的过程栈相同,则删掉重新生成式子。
五.代码说明
1.输入题目的数量num,和数字的范围range,调用GenerateEquation()生成式子,然后定义一个Calculator类对象并调用algorithm方法得到式子答案和式子的运算顺序,再用if语句判断返回答案是否为空,或式子的运算顺序已经存在,则重新生成式子。
private void CreatProblems(int num,int range) {
Random rand=new Random();
while(Count int[] operator=GenerateOpertor(operatorNum,rand); //随机生成operatorNum个字符String equation=GenerateEquation(operator,operdataNum,range);Calculator answer=new Calculator();list=answer.algorithm(equation);if(!list.isEmpty()) {String STR=Sb.toString();if(STR.indexOf(list.get(1).toString())==-1) {Sb.append(list.get(1)).append(" ");problemTemp.append("第").append(Integer.toString(Count+1)).append("题:").append(equation).append("\n");answerTemp[Count]=list.get(0).toString();System.out.println(answerTemp[Count]);Count++;}}else{CreatProblems(num-Count,range);}}}2.
/** 随机生成operatorNum个字符,并生成一个数组储存(下标数组)*/private int[] GenerateOpertor(int operatorNum, Random rand) {int[] operator=new int[operatorNum];for(int i=0;i operator[i]=rand.nextInt(4);}return operator;}3.
/*** 生成随机数算法*/private String getRandomNum(int limit){Random all = new Random();StringBuilder temp = new StringBuilder();int numtype = all.nextInt(2);int num = 0, carrier=0, numerator = 0, denominator = 0;int j = 0;if(numtype==0){num=1+all.nextInt(limit);return Integer.toString(num);}else{//此行生成分数numerator = 1+all.nextInt(limit);denominator = 2+all.nextInt(limit-1);int n = numerator, m = denominator;if(n%m==0) { //如果生成的分子分母不规范for(int i= 0 ; i<=100; i++){n = 1+all.nextInt(limit);m = 2+all.nextInt(limit-1);if(n%m!=0) break;}}if(m>n) j = Euclid(m, n);else{carrier = n/m;j = Euclid(n,m);}if(j==1){ //判断最大公约数是否等于1;if(carrier!=0){ //判断该分数是不是假分数,是就转成带分数形式n=(n%m);temp.append(carrier);temp.append("'");}temp.append(n);temp.append("/");temp.append(m);return temp.toString();}else{ //判断该分数是不是假分数,是就转成带分数形式n/=j;m/=j;if(carrier!=0){n=(n%m);temp.append(carrier);temp.append("'");}temp.append(n);temp.append("/");temp.append(m);return temp.toString();}}}/*** 欧几里得判断是否有公约数*/private int Euclid(int m,int n){while(n!=0){int r;r=m%n;m=n;n=r;Euclid(m,n);}return m;}4.用于计算式子的结果,计算逻辑在第四部分有逻辑图
public class Calculator {public ArrayList algorithm(String str) {LinkedListnumList=new LinkedList<>();//存放数字 StackoperatorStack=new Stack<>();//放操作符 HashMaphashMap=new HashMap<>();//存放字符优先级 hashMap.put("(", 0);hashMap.put("+", 1);hashMap.put("-", 1);hashMap.put("*", 2);hashMap.put("÷", 2);ArrayList list=new ArrayList();CheckOut check=new CheckOut();//生成运算顺序,用于查重StringBuilder ba = new StringBuilder();String str1[]=str.split("\\ ");// for(String string:str1)// System.out.println(string);int leftBrankets = 0;//用于检测‘(’的个数int operatorCount=0;for(int i=0;i StringBuilder digit=new StringBuilder();if(!"".equals(str1[i])) {//判断是否是数字char num[]=str1[i].toCharArray();if(Character.isDigit(num[0])) {// System.out.println(str1[i]);numList.addLast(String.valueOf(str1[i]));//压进数字栈continue;//结束本次循环,回到for语句 }//不是数字,是符号else {char operatorOfChar=str1[i].charAt(0);// System.out.println(operatorOfChar+"符号");switch(operatorOfChar) {case '(':{leftBrankets++;break;}case ')':{String stmp;//取符号栈元素,考虑到(1+2*3)+4这种情况,要比较操作符的优先级 String stmd;if(!operatorStack.isEmpty()) {if(operatorCount==2&&leftBrankets==1) {//取出符号栈里的操作符(两个)stmp=operatorStack.pop();stmd=operatorStack.pop();if(hashMap.get(stmp)>hashMap.get(stmd)) {String a=numList.removeLast();String b=numList.removeLast();String result=calculate(b,a,stmp);if(result.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(b, a, stmp));numList.push(result);//将结果压入栈operatorStack.push(stmd);//将未进行计算的操作符压回符号栈stmp = operatorStack.pop(); //符号指向下一个计算符号,再进行一次运算String c=numList.removeLast();String d=numList.removeLast();String result02=calculate(d,c,stmp);if(result02.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(d, c, stmp));numList.push(result02);//将结果压入栈}else {String a=numList.removeFirst();String b=numList.removeFirst();String result=calculate(a,b,stmd);if(result.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(a, b, stmd));numList.addLast(result);operatorStack.push(stmp);stmp = operatorStack.pop(); //符号指向下一个计算符号String c=numList.removeLast();String d=numList.removeLast();String result02=calculate(d,c,stmp);if(result02.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(d, c, stmp));numList.push(result02);//将结果压入栈 }}else if(leftBrankets==2||(operatorCount==1&&leftBrankets==1)){stmp=operatorStack.pop();String a=numList.removeLast();String b=numList.removeLast();String result=calculate(b,a,stmp);if(result.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(b, a, stmp));numList.addLast(result);/*判定下一个str[i]是什么*/}break;}}case '=':{String stmp;while (!operatorStack.isEmpty()) { //当前符号栈里面还有+ - * /,即还没有算完stmp = operatorStack.pop();String a = numList.removeLast();String b = numList.removeLast();String result = calculate(b, a, stmp);if(result.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(b, a, stmp));// System.out.println(ba.toString());numList.addLast(result);}break;}default:{operatorCount++;String stmp;while (!operatorStack.isEmpty()&&leftBrankets==0) { //如果符号栈有符号stmp = operatorStack.pop(); //当前符号栈,栈顶元素if (hashMap.get(stmp) >= hashMap.get(String.valueOf(operatorOfChar))) { //比较优先级String a = numList.removeLast();String b = numList.removeLast();String result =calculate (b, a, stmp);if(result.contentEquals("出现了负值"))return list ;ba.append(check.checkOut(b, a, stmp));numList.addLast(result);}else {operatorStack.push(stmp);break;}}operatorStack.push(String.valueOf(operatorOfChar));break;}}}}}list.add(numList.getLast());list.add(ba);return list;}// }/*** 计算结果*/private String calculate(String s1, String s2, String processor){int theinteger=0, theN=0, theM = 0;int position1 = -1, position2 = -1;int j = 1; //放最大公约数String num1 = null, num2 =null;StringBuilder temp = new StringBuilder();int Nfornum1 = 0, Mfornum1 = 0, Nfornum2 = 0, Mfornum2 = 0;int carrier1 = 0, carrier2 = 0;num1 = s1.substring(0);num2 = s2.substring(0);position1 = num1.indexOf("'");position2 = num1.indexOf("/");if(processor.equals("+")){Mfornum1 = gettheM(num1); //确定加号前面的数字的分母值(此处要保证该数字的最后一位元素是空格)Nfornum1 = gettheN(num1, Mfornum1); //确定加号后面的数字的分子值(此处要保证该数字的0号元素为数字,最后一位是空格(整数情况))Mfornum2 = gettheM(num2); //"2'4/21 "Nfornum2 = gettheN(num2, Mfornum2); //" 89"theN = Nfornum1*Mfornum2+Nfornum2*Mfornum1;theM = Mfornum1*Mfornum2;if(theN>theM){j=Euclid(theN,theM);carrier1 = theN/theM;}elsej=Euclid(theM,theN);theN/=j;theM/=j;if(theN%theM==0){theN = theN/theM;temp.append(Integer.toString(theN));}else{if(carrier1!=0){ //判断该分数是不是假分数,是就转成带分数形式theN=(theN%theM);temp.append(carrier1);temp.append("'");}temp.append(Integer.toString(theN));temp.append("/");temp.append(Integer.toString(theM));}return temp.toString();}else if (processor.equals("-")){Mfornum1 = gettheM(num1); //确定加号前面的数字的分母值(此处要保证该数字的最后一位元素是空格)Nfornum1 = gettheN(num1, Mfornum1); //确定加号后面的数字的分子值(此处要保证该数字的0号元素为数字,最后一位是空格(整数情况))Mfornum2 = gettheM(num2); //"2'4/21 "Nfornum2 = gettheN(num2, Mfornum2); //" 89"theN = Nfornum1*Mfornum2-Nfornum2*Mfornum1;theM = Mfornum1*Mfornum2;if(theN==0){temp.append("0");}else if(theN>0){if(theN>theM){j=Euclid(theN,theM);carrier1 = theN/theM;}elsej=Euclid(theM,theN);theN/=j;theM/=j;if(theN%theM==0){theN = theN/theM;temp.append(Integer.toString(theN)); }else{if(carrier1!=0){ //判断该分数是不是假分数,是就转成带分数形式theN=(theN%theM);temp.append(carrier1);temp.append("'");}temp.append(Integer.toString(theN));temp.append("/");temp.append(Integer.toString(theM));}}else{temp.append("出现了负值");}return temp.toString();}else if (processor.equals("*")){Mfornum1 = gettheM(num1); //确定加号前面的数字的分母值(此处要保证该数字的最后一位元素是空格)Nfornum1 = gettheN(num1, Mfornum1); //确定加号后面的数字的分子值(此处要保证该数字的0号元素为数字,最后一位是空格(整数情况))Mfornum2 = gettheM(num2); //"2'4/21 "Nfornum2 = gettheN(num2, Mfornum2); //" 89"theN = Nfornum1*Nfornum2;theM = Mfornum1*Mfornum2;if(theN>theM){j=Euclid(theN,theM);carrier1 = theN/theM;}elsej=Euclid(theM,theN);theN/=j;theM/=j;if(theN%theM==0){theN = theN/theM;temp.append(Integer.toString(theN));}else{if(carrier1!=0){ //判断该分数是不是假分数,是就转成带分数形式theN=(theN%theM);temp.append(carrier1);temp.append("'");}temp.append(Integer.toString(theN));temp.append("/");temp.append(Integer.toString(theM));}return temp.toString();}else if (processor.equals("÷")){Mfornum1 = gettheM(num1); //确定加号前面的数字的分母值(此处要保证该数字的最后一位元素是空格)Nfornum1 = gettheN(num1, Mfornum1); //确定加号后面的数字的分子值(此处要保证该数字的0号元素为数字,最后一位是空格(整数情况))Mfornum2 = gettheM(num2); //"2'4/21 "Nfornum2 = gettheN(num2, Mfornum2); //" 89"theN = Nfornum1*Mfornum2;theM = Mfornum1*Nfornum2;if(theN>theM){j=Euclid(theN,theM);carrier1 = theN/theM;}else j=Euclid(theM,theN);theN/=j;theM/=j;if(theN%theM==0){theN = theN/theM;temp.append(Integer.toString(theN));}else{if(carrier1!=0){ //判断该分数是不是假分数,是就转成带分数形式theN=(theN%theM);temp.append(carrier1);temp.append("'");}temp.append(Integer.toString(theN));temp.append("/");temp.append(Integer.toString(theM));// temp.append("\n"); }return temp.toString();}else return "运算符号没有规范传入";}/*** 算结果的分子分母方法*/private int gettheN(String beforeM,int M){int theN = 0;int position1 = -1, position2 = -1;position1 = beforeM.indexOf("/");position2 = beforeM.indexOf("'");if(position1<0&&position2<0){try{theN = theN + M * (Integer.parseInt(beforeM.substring(0)));} catch (NumberFormatException e) {e.printStackTrace();}return theN;}else if(position1>0&&position2<0){try{theN += Integer.parseInt(beforeM.substring(0,position1));} catch (NumberFormatException e) {e.printStackTrace();}return theN;}else if(position1>0&&position2>0){try{theN = theN + M * (Integer.parseInt(beforeM.substring(0,position2)));} catch (NumberFormatException e) {e.printStackTrace();}try{theN += Integer.parseInt(beforeM.substring(position2+1,position1));} catch (NumberFormatException e) {e.printStackTrace();}return theN;}else return -1;}private int gettheM(String afterN){int theM = 0;int position2 = -1;position2 = afterN.indexOf("/");int thezero = 0;if(position2>0){try{theM = Integer.parseInt(afterN.substring(position2+1));} catch (NumberFormatException e) {e.printStackTrace();}}else {theM=1;}return theM;}private int Euclid(int m,int n){while(n!=0){int r;r=m%n;m=n;n=r;Euclid(m,n);}return m;}5.用于生成运算式的子运算顺序式(格式:+ab(a>b))存进StringBuilder类并返回
public class CheckOut {StringBuilder checkOut(String a,String b,String operator) {int aNum=0;int bNum=0;StringBuilder SB=new StringBuilder();//判断a,b分别是什么类型的数字int[] ajudge=judgeNum(a);int[] bjudge=judgeNum(b);//比较a,b的大小if(ajudge[0]*bjudge[1]>bjudge[0]*ajudge[1]) {//存进StringBuilder的顺序 SB.append(operator).append(a).append(b);}else {SB.append(operator).append(b).append(a);}return SB;}private int[] judgeNum(String str) {int position1=-1;int position2=-1;int[] Array = new int[3];//存分子分母position1 = str.indexOf("/");position2 = str.indexOf("'");if(position1<0&&position2<0){//整数Array[0]=Integer.valueOf(str);Array[1]=1;}if((position1>0&&position2>0)||(position1>0&&position2<0)){//带分数或分数String str1[]=str.split("\\'|\\/");int[] sons = new int[str.length()];for(int i=0;i sons[i]=Integer.parseInt(str1[i]);}if(str1.length==3) {Array[0]=sons[0]*sons[3]+sons[2];Array[1]=sons[3];}else {Array[0]=sons[0];Array[1]=sons[1];}}return Array;}}
六、测试运行测试用例1:题目个数num<=0或数字上限值<=1 结果:报错
测试用例2:数目个数num=1000,正常的数字上限 结果:程序执行由3到5秒钟不等
测试用例3:生成的答案文件或者成绩文件在指定目录存在同名同类型文件 结果:清除源文件信息,每次程序运行都更新文件信息
测试用例4:生成100道式子,检查运算符个数的分布情况。结果:分布均匀
测试用例5:生成100道式子,检查括号的分布情况。结果:分布均匀
测试用例6:生成100道式子,检查不同数字类型的分布情况。结果:分布均匀
测试用例7:特别地生成两个重复的式子,并进行查重 结果:查重成功,并成功删掉其一
测试用例8:用户输入答案时输入特殊字符,结果:报错,错题+1
测试用例9:用户只输入部分答案 结果:错题与对题统计无误
测试用例10:正常生成式子与答案 结果:答案匹配
八、项目小结
我的结对对象是梁迪希同学
做得好的方面:在程序刚开始的时候,我还没有什么头绪,迪希同学先有思路,然后共同讨论了查重这部分的实现方法,之后的两天时间里我们都是分开做,各有各自的思路,我的思路也清晰了,考虑到需要生成不同的式子类型,应该去怎么实现。到晚上的时候,我们也会交流各自的想法。迪希同学在功能的实现方面的想法是要比我多的,在我某些地方卡住的时候会分享他的想法给我,这是值得我学习的地方。
做得不好的方面:在做项目的后段时间,由于时间的紧迫,我们各自分开实现不同的功能,但由于没能及时交流,使得功能之间没法很好地契合,所以花费了不少时间修整代码。往后在做类似团队项目时,要多交流,明确好分工,各自想法的优劣和各自项目功能的实现谁来做等。