结对对象:许峰铭
一.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<num) {
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<operatorNum;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) {
LinkedList<String> numList=new LinkedList<>();//存放数字
Stack<String> operatorStack=new Stack<>();//放操作符
HashMap<String,Integer> hashMap=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<str1.length;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;
}
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));
}
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;
}
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));
}
}
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;
}
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));
}
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<str1.length;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:正常生成式子与答案 结果:答案匹配
八、项目小结
我的结对对象是梁迪希同学
做得好的方面:在程序刚开始的时候,我还没有什么头绪,迪希同学先有思路,然后共同讨论了查重这部分的实现方法,之后的两天时间里我们都是分开做,各有各自的思路,我的思路也清晰了,考虑到需要生成不同的式子类型,应该去怎么实现。到晚上的时候,我们也会交流各自的想法。迪希同学在功能的实现方面的想法是要比我多的,在我某些地方卡住的时候会分享他的想法给我,这是值得我学习的地方。
做得不好的方面:在做项目的后段时间,由于时间的紧迫,我们各自分开实现不同的功能,但由于没能及时交流,使得功能之间没法很好地契合,所以花费了不少时间修整代码。往后在做类似团队项目时,要多交流,明确好分工,各自想法的优劣和各自项目功能的实现谁来做等。