这个作业属于哪个课程 | 班级的链接 |
---|---|
这个作业要求在哪里 | 作业要求的链接 |
这个作业的目标 | 实现生成和计算四则运算题的程序,并学会与他人合作做项目,积累实践经验 |
一、姓名、学号以及Github地址
姓名 | 黄铭涛 | 曾琳备 |
---|---|---|
学号 | 3122004393 | 3122004411 |
Github地址 | Github | Github |
二、psp表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 15 |
Development | 开发 | 480 | 600 |
· Analysis | · 需求分析 (包括学习新技术) | 360 | 180 |
· Design Spec | · 生成设计文档 | 0 | 0 |
· Design Review | · 设计复审 | 60 | 45 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 60 | 80 |
· Coding | · 具体编码 | 300 | 960 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 80 |
Reporting | 报告 | 0 | 0 |
· Test Repor | · 测试报告 | 0 | 0 |
· Size Measurement | · 计算工作量 | 20 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 25 |
· 合计 | 1510 | 2150 |
三、效能分析
1、在改进程序性能上花费了多少时间
两个小时
2、改进的思路
(1)创建变量isexist来记录生成的重复四则运算题个数,当isexist大于10000时跳出循环并提示用户无法生成给定数量的四则运算题,以此避免无限循环。
(2)在生成运算题的同时计算答案,若计算过程中出现小于0的数立刻退出,避免造成不必要的运算。
(3)性能分析图
(4)消耗最大的函数:main方法
public class main {
public static void main(String[] args) throws IOException {//主函数接收命令行
getParameter(args);//从命令行接收参数并执行程序
}
四、设计实现过程
1、包含几个类
一共有五个类,每个类中有多个方法,都由构造函数调用。五个类分别为:
Number//存储运算数
Expression//存储四则运算算式
makeExpression//生成四则运算算式
Correct//根据Exercises.txt文件和Answers.txt文件来统计对题和错题
main//程序执行入口
2、存储方式
(1)Number类:
Number类用于存储运算数,里面包含int类型的numerator(分子)和denominator(分母)
public class Number {//运算数
int numerator;//分子
int denominator=1;//分母
public Number(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
public Number() {
}
}
(2)Expression类:
Expression用来生成四则运算表达式,里面有运算数集合、符号集合、括号集合等。
public class Expression {//算术表达式
Random R=new Random();
boolean islegal=true;//判断算式是否合法(不产生负数)
int countNumber=R.nextInt(3)+2;//运算数个数
ArrayList<Number> number2=new ArrayList<>();//复制运算数
ArrayList<Number> number=new ArrayList<>();//运算数
ArrayList<Integer>bracket=new ArrayList<>();//括号集合,表示括号位置,位于相同索引的运算数的左边
static String Symbol="+-×÷";//所有运算符
ArrayList<Character> symbol=new ArrayList<>();//存储运算符
ArrayList<Character> symbol2=new ArrayList<>();//复制运算符
Number answer=new Number();//运算结果
public Expression(int r) {
makenumber(r);//生成运算数集合
makeSymbol();//生成运算符集合
makeBracket();//生成括号数组
answer=getAnswer(0,symbol.size());
if(answer.denominator==0){
islegal=false;
}
}
3、程序执行主要流程
五、代码说明
关键算法:
1、Expression类下的getAnswer方法(根据存储在集合中的运算数以及运算符和括号计算出答案)
(1)思路:
计算四则运算时采用人类的思维,即先算括号里的算式或乘除运算符,再将算出来的结果与还未计算的运算数进行计算。
方法执行过程:
根据括号位置以及符号位置来优先计算算式中的一个符号两边的运算数,将计算过的两个运算数移出运算数集合,再把运算结果放入集合中两个运算数被移出之前的位置,最后将计算过的运算符移出运算符集合,循环往复,直到运算符集合为空或运算数只剩一个。
(2)代码:
public Number getAnswer(int begin, int end){
if(bracket.size()==0)//没有括号时
return countExpression(begin,end);//直接计算
else{
//有括号时,先算括号
if(bracket.size()==2){//一个括号
countExpression(bracket.get(0),bracket.get(1)-1);//先算括号
return countExpression(0,symbol.size());//再算外面
}
else{
//判断两个括号是否有交集
if(bracket.get(0)<=bracket.get(2)&&bracket.get(1)>= bracket.get(3))//一个括号包另一个括号
{
countExpression(bracket.get(2),bracket.get(3)-1);//先算内括号
countExpression(bracket.get(0),bracket.get(1)-2);//再算外括号
return countExpression(0,symbol.size());
}
else{//无交集
countExpression(bracket.get(0),bracket.get(1)-1);//先算左括号
countExpression(bracket.get(2)-1,bracket.get(3)-2);//再算右括号
return countExpression(0,symbol.size());
}
}
}
}
private Number countExpression(int begin,int end) {//根据传来的begin和end索引计算该索引内的算式,并将运算数替换为答案
Number sum = new Number(0, 1);
for (int i = begin; i < symbol.size()&&i<end; i++) {//第一遍遍历先算乘除
switch (symbol.get(i)) {
case '×':
case '÷':
sum = count(number.get(i), number.get(i + 1), symbol.get(i));
number.set(i, sum);//替换算过的运算数
number.remove(i + 1);//移除另一个算过的运算数
symbol.remove(i);//移除算过的运算符
end--;//由于移除了运算符,end要减1
i--;//移除了运算数,i也要减1
if(sum.numerator<0||sum.denominator<=0){//运算过程中出现小于0的结果
islegal=false;//将标志变量置为false
return sum;//直接停止计算
}
}
}
for (int i = begin; i < symbol.size()&&i<end; i++) {//顺序计算加减
sum=count(number.get(i),number.get(i+1),symbol.get(i));
if(sum.denominator<=0||sum.numerator<0){//分母小于等于0或分子小于0直接退出
islegal=false;
return sum;
}
number.set(i, sum);
number.remove(i + 1);
symbol.remove(i);
i--;
end--;//同上,替换、移除运算数以及运算符
}
}
public static Number count(Number n1,Number n2,char c)//根据运算数和运算符计算结果
{
switch(c){
case '÷':
if(n2.numerator==0){//分母为0直接退出
Number nl=new Number(-1,-1);
return nl;
}
n1.numerator*=n2.denominator;
n1.denominator*=n2.numerator;
break;
case '×':
n1.denominator*=n2.denominator;
n1.numerator*=n2.numerator;
break;
case '+':
n1.numerator*=n2.denominator;
n2.numerator*=n1.denominator;//通分
n1.denominator*=n2.denominator;
n1.numerator+=n2.numerator;
break;
case '-':
n1.numerator*=n2.denominator;
n2.numerator*=n1.denominator;//通分
n1.denominator*=n2.denominator;
n1.numerator-=n2.numerator;
break;
}
n1=simplify(n1.numerator,n1.denominator);
return n1;
}
2、Correct类下的Correct方法(根据算式文件和答案文件统计对错)
(1)思路:
用String接受文件中的算式,再将算式里的数字和运算符存入Expression类中,调用Expression类的getAnswer方法计算答案,最后与答案文件中的答案进行比较。
(2)代码:
public Correct(String exerciseFile,String answerFile) throws IOException {
BufferedReader exf=new BufferedReader(new FileReader(exerciseFile));
BufferedReader ans=new BufferedReader(new FileReader(answerFile));//打开文件
int questonNumber=1;//题号
int point = 0;//用来跳过小数点
while (true) {
String expression=exf.readLine();//读取表达式于expression中
String answer=ans.readLine();//读答案
if(expression==null||answer==null)
break;
int i=0;
int j=0;
for(i=0;i<expression.length();i++){//跳过题号
if(expression.charAt(i)=='.')
break;
}
for(j=0;j<answer.length();j++){
if(answer.charAt(j)=='.')
break;
}
i++;跳过小数点
while(j<answer.length()&&(answer.charAt(j)>'9'||answer.charAt(j)<'0'))跳到答案字符串数字初始断
j++;
int begin=j;//记录初始断
while(j<answer.length()&&(answer.charAt(j)<='9'&&answer.charAt(j)>='0'||
answer.charAt(j)=='’'||answer.charAt(j)=='/'))//跳到答案数组尾端
j++;
Number rightAnswer=creatExpression(expression.substring(i));//创建Expression类并计算正确答案
Number pathAnswer=getNumber(answer.substring(begin,j));//得到文件中的答案
if(isright(rightAnswer,pathAnswer)==true){//对比答案
cor++;//
correctQuestion.add(questonNumber);
}
else
{
wro++;
wrongQuestion.add(questonNumber);
}
questonNumber++;
}
exf.close();
ans.close();
writeResult(cor,wro);
}
六、测试运行
1、测试答案的正确性
由于该程序用java语言实现,全部采用自己创建的类来存储数据,要计算时才将数据从集合中拿出来计算,并用多层if和switch语句来判断运算符和运算数的索引来进行计算,答案难免会有差错,这使得测试答案是否正确成了难题,好在合作伙伴用python实现了测试答案正确性的功能。
原理如下:
读取题目文件,对题目表达式的字符串进行分割去掉序号,再将表达式中如"×"、"÷"的符号替换为语言能识别的运算符号。然后读取答案文件,对答案字符串进行相同的操作。创建正确和错误的列表以统计正误题目,表长就为正误题目数。最后将两个列表输出到新文档中,在控制台上打印检查完毕提示答案比对完成。
python测试代码:
def check(exercisefile,answerfile):
with open(f'{exercisefile}','r',encoding='utf-8') as eF:
exercise=eF.readlines()
with open(f'{answerfile}','r',encoding='utf-8') as aF:
answer=aF.readlines()
correctList=[]
wrongList=[]
for index,(exercise,answer) in enumerate(zip(exercise,answer),start=1):
split_exercises=exercise.split('.')
split_ans = answer.split('.')
exp = split_exercises[1].replace('’','+').replace('÷','/').replace('×','*').replace('=','')
ans = split_ans[1].replace('’','+')
# if split_exercises[1]==split_ans[1]:
if format(eval(exp),'.10f')==format(eval(ans),'.10f'):
correctList.append(index)
else:
wrongList.append(index)
with open('Grade.txt','w')as gF:
gF.write(f'Correct:{len(correctList)},({','.join(map(str,correctList))})\n')
gF.write(f'Wrong:{len(wrongList)},({','.join(map(str,wrongList))})')
print('检查完毕')
进过多轮测试,四则运算生成的答案均全对,以此确定我的程序是正确的
2、测试用例
测试用例1:生成10000道10以内的四则运算题,即n=10000,r=10
测试用例2:生成10000道30以内的四则运算题,即n=10000,r=30
测试用例3:生成100道2以内的四则运算题,即n=100,r=2
测试用例4:生成50道1以内的运算题,即n=50,r=1
测试用例5:将测试1生成的10000道练习题以及答案文件传入命令行,统计对错数量
测试用例6:将测试用例5的Answers.txt文件里的内容稍作修改,统计对错数量
测试用例7:生成100道题目,并将生成的Exercises.txt文件与空白文本比较,统计对错数目
测试用例8:生成100道题目,并将生成的Answers.txt文件与空白文本比较,统计对错数目
测试用例9: 输入错误参数
测试用例10: 输入不存在的文件
七、项目小结
1、本次项目实现了作业要求的所有功能。
2、结对开发时缺少交流,未确定最终思路,导致开发效率较低。
3、写代码时要尽量多测试,边开发边写测试代码,测试要尽量全面,否则后面代码堆起来容易出大问题。
4、结对做项目比单独做项目效率有了显著的提升,在开发遇到瓶颈时合作伙伴可以提供新的思路。同时两个人可以关注到更多的问题,使程序设计的更为全面,在测试程序方面也更加细腻。
5、本次结对两人都忠于开发,但缺少交流,建议多多加强交流,提高开发效率。