结对成员:宗义澎、闫浩宇
一、GitHub地址
二、PSP:
1
Personal Software Process Stages
预估耗时(分钟)
实际耗时(分钟)
·Planning
·计划
30
40
· Estimate
· 估计这个任务需要多少时间
30
20
·Development
·开发
300
420
· Analysis
· 需求分析
60
50
· Design Spec
· 生成设计文档
30
20
· Design Review
· 设计复审
30
30
· Coding Standard
· 代码规范
100
120
· Design
· 具体设计
60
20
· Coding
· 具体编码
1200
700
· Code Review
· 代码复审
20
20
· Test
· 测试(自我测试,修改代码,提交修改)
200
100
·Reporting
·报告
100
140
· Test Report
· 测试报告
60
50
· Size Measurement
· 计算工作量
20
20
· Postmortem & Process Improvement Plan
· 事后总结, 并提出过程改进计划
40
50
合计
2010
1800
三、效能分析
相较于各路大神的各种神仙速度,这个速度实在是慢(似乎是别人的100倍之多?),究其原因,我认为有以下几点:
1.用了过多的随机数:为了实现题目的随机,我在生成题目的许多地方都用了随机数,比如算式中有几个运算符,每个运算符是什么,括号有几个,在第几个位置。这些随机数会降低程序的运行速度
2.数据结构:为了处理方便,我将题目中所有的数都封装成了自己写的分数类(一个分子一个分母),在计算答案时要先封装成分数类在去计算,这也会降低程序的运算速度。
四、设计实现过程
首先阅读需求分析,可以大致将问题分为两部分,一部分是生成题目,另一部分是验证题目正确性。为了让程序的数据结构清晰,我将程序中的所有数据封装成了一个类,然后给出分数类的计算方法。生成题目和验证题目的内容放在了Util包里,具体情况如下图
其中Main是主类,用于处理各种给定的参数;bean包内是分数类;util包中,FileUtil用于处理文件,Number解决如何计算分数类,Question用于生成随机程序
五、代码和具体思路
这部分分为三部分来说
1.生成题目
题目要求给定一个数作为数字的上限,然后生成不能重复的题目,题目的运算符不能超过三个,最后的结果不能是负数。由于题目要求不能重复,我开始的想法是不用随机数,用顺序的方法生成题目,但这样根本不可行,因为题目的范围太大,生成的题目非常相似,不能拿给小学生做,于是还是采用了随机数的策略。但是用了随机数可能就会出现重复的题目,于是我干脆就找它的充分条件:只要答案重复,就认为两道题重复,这样做的合理性是题目范围非常大,生成重复题目概率本就很小,就不要为它花费太多的时间了。另外还需要解决的是运算符和括号的问题,这个地方上面也提到过,用的是随机数,生成随机数决定题目的类型。至于括号,用的则是穷举法,列举出每种情况可能出现的情况,然后再用随机数决定题目属于哪种情况。这个部分的代码主要都是随机数的调用,代码从略。
2.生成答案
这应该是这个项目最难的地方,给定了一个题目,如何算出它的答案呢?我们知道,题目中有括号,另外还要考虑乘除号比加减号运算等级高,所以不能简单地从左到右解析题目,这个地方的算法用的是之前数据结构学过的一套东西,具体算法为现将给出的中缀表达式题目转化为后缀表达式,然后借助栈来运算后缀表达式。具体到这个项目上,在实现算法的时候,需要把字符串形式的题目先转化为中缀表达式,再转化为后缀表达式,与此同时,题目中的数要封装为分数类,便于进行入栈出栈操作,下面代码显示如何实现这个算法。
/*** 将字符串形式的题目转化为Queue形式的中缀表达式
**/
public static QueueReadString(String question) {
Queue mid = new LinkedList();int son = -1;
String temp= "";for(int x = 0; x
mid.add(c);
}else if(c==')' || c=='+' || c=='-' || c=='×' || c=='÷') {if(son!=-1) {//如果有分子,则说明现在的temp是分母
mid.add(newNum(son,Integer.valueOf(temp)));
temp= "";
son= -1;
}else if(!temp.equals("")) {//若队列内有字符,则temp是分子,分母为1
mid.add(new Num(Integer.valueOf(temp),1));
temp= "";
}
mid.add(c);
}else {//读到数字或者分母号
if(c!='/') {//不是分数号,放到temp队列里
temp = temp+c;
}else {//遇到分数号,队列的东西提出来做分子
son =Integer.valueOf(temp);
temp= "";
}
}
}//把最后一个数搞出来
if(son!=-1) {//如果有分子,则说明现在的temp是分母
mid.add(newNum(son,Integer.valueOf(temp)));
temp= "";
}else if(!temp.equals("")) {//若队列内有字符,则temp是分子,分母为1
mid.add(new Num(Integer.valueOf(temp),1));
temp= "";
}returnmid;
}/*** 将Queue计算为最后结果
* 应输入中缀表达式*/
public staticNum CountQueue(Queue mid) {
Queue after =MidToAfter(mid);
Stack tempStack = new Stack();while(after.peek()!=null) {if(after.peek() instanceof Num) {//操作数直接入栈
tempStack.push(after.poll());
}else{char op = (char)after.poll();
Num n,first,second;switch(op) {case '+':
n=NumberUtil.add((Num)tempStack.pop(),(Num)tempStack.pop());
tempStack.push(n);break;case '-':
first=(Num)tempStack.pop();
second=(Num)tempStack.pop();
n=NumberUtil.sub(second,first);
tempStack.push(n);break;case '×':
n=NumberUtil.mul((Num)tempStack.pop(),(Num)tempStack.pop());
tempStack.push(n);break;case '÷':
first=(Num)tempStack.pop();
second=(Num)tempStack.pop();
n=NumberUtil.div(second,first);
tempStack.push(n);break;
}
}
}
Num answer=(Num)tempStack.pop();returnNumberUtil.normal(answer);
}/*** 将中缀表达式转换为后缀表达式
**/
public static QueueMidToAfter(Queue mid) {
Queue after = new LinkedList();
Stack tempStack = new Stack();while(mid.peek()!=null) {if(mid.peek() instanceof Num) {//操作数直接入队列
after.add(mid.poll());
}else {//符号的判定
char c = (char) mid.poll();if(c=='(') {//左括号直接入栈
tempStack.add(c);
}else if(c==')') {//右括号把栈里的东西全弄到队列里,直到遇到左括号
while (true) {if(tempStack.empty()) {
System.out.println("缺少左括号! ");return null;
}else if ((char)tempStack.peek()=='(') {
tempStack.pop();break;
}else{
after.add(tempStack.pop());
}
}
}//非括号类运算符
else if (!tempStack.empty()) {char peek = (char)tempStack.peek();//当前运算符优先级大于栈顶运算符优先级,或者栈顶为左括号时,当前运算符直接入栈
if(((c=='×' ||c=='÷')&&((peek=='+') || (peek=='-'))) || peek=='(') {
tempStack.push(c);
}//否则,将栈顶的运算符取出并存入队列,然后将自己入栈
else{
after.add(tempStack.pop());
tempStack.push(c);
}
}else{
tempStack.push(c);
}
}
}while(!tempStack.empty()) {
after.add(tempStack.pop());
}returnafter;
}
以上的三个方法可以实现上述算法。
3.检查用户输入
这个部分是由队友完成,我直接把他的实现方法贴上来:
1)打开习题册,创建一个文件选择器,由用户自己选择要练习的习题册。将选择的文件逐行读取,并显示在控制台上。前提是已经有生成过的题目文件在储存盘中。
2)输入答案。由用户自行根据习题册内容输入自己的答案,并以换行为题目分割。同样用文件选择器保存到目标路径下。
3)检查。分别将答案文件与用户用io流读取。逐行比较,并记录错题数与题号。将保存好的错题记录输出。
六、测试运行
输入生成题目的指令:
在项目的questionbank中生成了对应文件
题目文件的具体内容:
输入检查的指令
输入指令后成功验证答案
七、项目小结
通过这个项目,体会到了结对编程的好处,结对编程可以动用两个人的智慧,想到更多的好方法,除此之外,通过这个项目,对之前的Java知识也有了更深的认识。这个项目完成的不好的地方是没有过多的考虑程序运行时间的问题,导致了程序效能极低,以后需要在这方面多下功夫。