211606313 李佳 211606381 吴伟华
一、预估与实际
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 10 | 5 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 200 | 230 |
• Design Spec | • 生成设计文档 | 30 | 50 |
• Design Review | • 设计复审 | 10 | 15 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
• Design | • 具体设计 | 30 | 25 |
• Coding | • 具体编码 | 500 | 400 |
• Code Review | • 代码复审 | 30 | 40 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 100 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 40 | 60 |
• Size Measurement | • 计算工作量 | 5 | 5 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 50 |
合计 | 1015 | 990 |
二、需求分析
- 1、小学一年级算的是100以内整数的加减法,没有负数,也没有小数。
- 2、小学二年级除了要满足一年级的要求,还要算仅限于九九乘法表的乘法与除法,没有小数。
- 3、小学三年级进行四则混合运算,运算符在2~4个之间,可以加括号,减法运算的结果不能有负数,除法运算除数不能为0,不能有余数。
- 4、输入格式:
- 程序需从命令行中接收两个参数 年级 和 题目数量,分别用 -n 和 -grade 表示,这两个参数的顺序不限,当 n 为 2 或 1 时,分别代表给 2 年级,1 年级出题
- 5、输出格式:
- 把题目和标准答案写入out.txt文件,输出的格式要求
- 每道题目占一行
- 每道题的格式:
- (1)题号:数字 + 一对英文括号;
- (2)题目:数字、符号之间空一格;
- (3)题号与题目之间也需要加一空格,行末不需要加空格。
- 题目和标准答案分开输出,先输出所有题目,后输出所有答案,答案的输出格式与题目一样。
三、设计
1. 设计思路
第一次设计
- 程序有三个类
- 程序主流程中设计四个方法,main函数、判断输入参数、生成题目、写入文件
- 另外两个类为由老师提供的调度场和逆波兰表达式计算的C++算法改变而来的类
第二次设计
- 分析调度场和逆波兰表达式算法后发现对于本题可以省略掉调度场算法,将逆波兰表达式算法变成一个方法,最后只留了一个类
- main函数,程序主流程
- 判断输入参数是否合法
- 生成运算符号,并以后缀表达式的形式储存
- 判断生成身为后缀表达式形式中运算符的位置是否是可以计算出结果的
- 生成数字,并计算结果
- 写入文件
算法的关键:
- 经过分析调度场算法就是解析传入的字符串将数字转换出来,利用栈将字符串中的操作数和操作符按照计算的顺序排序,(不是按照原始优先级)。即生成后缀表达式,例如:99x(50+50)的后缀表达式为90 50 50 + x,逆波兰表达式算法就是计算后缀表达式的结果,这样的话我们可以直接构造后缀表达式,然后通过逆波兰算法计算结果,避免了生成的数字拼接成字符串,然后又从字符串解析出来
2. 实现方案
- 准备工作:先在Github上创建仓库,克隆到本地,编写代码后通过git上传到Github上
- 技术关键点:
- 经过分析去掉了调度场算法,那么接下来的工作就在于如何构造一个后缀表达式,我们知道在不算括号的情况下,n个运算符就有n+1个数字,所以先生成运算符并确定运算符的位置,可以发现数字和运算符的关系就像完全二叉树中度为2的节点个数和叶子节点的个数的关系,但是如果使用二叉树来储存的话代码又复杂了,继续分析可以发现一定有一个运算符在后缀表达式的最后(对应的二叉树的根节点一定是运算符,后缀表达式就是对应二叉树的后序遍历),最后选择了用数组储存,利用集合中的shuffle方法让数组中运算符随机排序,然后检验一下是否是可以计算出结果。
- 能计算出结果的后缀表达式的特点:
- 长度为n的数组中,运算符只能在下标为2~n-1的位置中
- 从n-1处向前遍历,每个运算符前面的非运算符个数减去前面的运算符个数必须大于等于2
- 在生成的后缀表达式子中只是在相应位置填入了运算符字符,其余位置为null,在利用逆波兰表达式求值算法中当要使用两数计算时,才随机生成数字并判断生成的数字在做这个运算时是否符合三年级题目需求(这也是为什么不能提前生成好所有数字,提前生成无法保证运算过程中是否满足需求)
- 最后是如何字计算时将正真的表达式还原成字符串,整个后缀表达式是没有括号的,当发现当前准备执行的运算符优先级比后面未执行的运算符优先级低时证明此时运算的两个数要加括号,生成的字符串也是利用入栈操作给下一个运算符使用,注意点:减号要另作判断,因为减号有括号和无括号的运算结果不一样
四、编码
- 设计思路:
- 1、参数的输入检测基本思想和上一次作业差不多,需要注意的是对-n和-grade位置的判断,因为这次的输入要求是用这两个符号标识后面参数的意义
- 2、先随机一个数字n来确定整个表达式的长度,并创建这么大的数组来储存后缀表达式,随机生成(n-1)/ 2个操作数储存在数组的最后几位中,利用集合中的shuffle方法将数字小标在2~n-2处的顺序随机(这样可保证数组最后一个一定是运算符),判断后缀表达式是否有效
- 3、利用逆波兰表达式求值算法,当从数组中取出的是一个null对象时将' '字符入站,当取出的是一个运算符时(即非空)从出栈两个数,如果出栈的这两个是Character类型时随机生成数字,计算时判断计算的数字是否符合要求,最后将结果入栈,此时要准备另一个字符串栈,判断当前运算符的优先级是否比后面的优先级低,低的话在拼接字符串时加上括号,将拼接的字符串入栈,在判断之前出栈的两个对象不是Character类型时,字符串栈也要出栈
- 每计算完一题就要将生成的题目存入StringBuffer中,方便以后输出到文件中
- 最后就是输出文件了
- 遇到的问题与解决方案:
- 最开始的问题就是在于后缀表达式的生成,要确保生成的后缀表达式是可以计算出结果的,最后通过观察后缀表达式中运算符的位置总结出其特征
- 长度为n的数组中,运算符只能在下标为2~n-1的位置中
- 从n-1处向前遍历,每个运算符前面的非运算符个数减去前面的运算符个数必须大于等于2
- 然后是对于除数运算时遇到除数为0或者除不尽的情况,一开始是做各种判断,试图修正数据,但很多情况是此时相除的两个数并不是当前随机出来的,而是其他运算产生的结果,这时候就没办法修正,最后采用的方法是抛出异常,在调用这个方法的地方捕获异常,然后让循环再来一次弥补这道题目的生成
- 最后是在导出正常表达式时括号的处理,一开始只判断了当前运算符优先级是否比紧跟着的下一个运算符优先级低,但发现生成的题目始终有该加括号的地方没有加括号,最后发现不仅是判断当前运算符和下一个运算符的优先级,而是要判断当前运算符和其后所有运算符的优先级,最后一个注意点就是减号情况的处理
- 最开始的问题就是在于后缀表达式的生成,要确保生成的后缀表达式是可以计算出结果的,最后通过观察后缀表达式中运算符的位置总结出其特征
1. 调试日志
- 数组越界:
- 最开始使用调度场算法时由于算法是由C++代码改写而来,没有注意到对于字符串数组中C++中时有' '作为结束标志,而java的数组不是,在改写时少加了角标的判断
- 空指针异常:
- 主要出现在自己定义了字符串数组,只是在开始开辟了存放引用的空间,没有对每个空间赋值对象,然后在使用时没有检测是否为空引用
- char数组使用的是包装类Character,而在检测时是直接判断数组中的对象是否为0,应该判断是否为null
- 生成的题目该加括号的地方没有加,一个原因是之前提到的要判断当前运算符和后面所有运算符的优先级比较,再这是对于'-'的情况的检测
2. 关键代码
/**
* 作用:生成最终题目并计算逆波兰表达式
* @param infix 后缀表达式数组
* @param n 第几个题目
* @param str 操作符数组
* @return 返回计算结果
* @throws Exception 当表达式计算过程中出错时抛出异常
*/
static int CalPoland(Character infix[],int n,char[] str) throws Exception{
Stack<Object> s = new Stack<Object>(); //用于保存每一步计算的结果
Stack<String> tops = new Stack<String>();//用于保存每一步生成的题目
int k = 1;//运算符计数器,记录当前是第几个运算符
for(int i = 0; i < infix.length; i++)
{//后缀表达式中不是运算符时,将' '作为数字的占位标志入栈
if(infix[i] == null) {
s.push(' ');
}else{//当为运算符时,从栈中取出两个数字进行计算
int a,b;
String str1,str2;//两个字符串用于记录生成的题目
//取出的是字符型时代表是之前存入的' ',这时要生成数字进行计算
if(s.peek() instanceof Character) {
a = 1 + (int) (Math.random() * 100);
str1 = " " + a;
}else {//不是字符型代表是其他部分计算产生的结果
a = (int) s.peek();
str1 = tops.peek();
tops.pop();
}
s.pop();
if(s.peek() instanceof Character) {
b = 1 + (int) (Math.random() * 100);
str2 = " " + b;
}else {
b = (int) s.peek();
str2 = tops.peek();
tops.pop();
}
s.pop();
//计算结果,计算的结果都将入栈等待下一次的运算
switch(infix[i])
{
case '+':
s.push(b + a);
break;
case '-':
if(b < a ) {
int temp = b;
b = a;
a = temp;
String st = str1;
str1 = str2;
str2 = st;
}
s.push(b - a);
break;
case '×':
s.push(b * a);
break;
case '÷':
if(b % a != 0) {
throw new Exception();
}
s.push(b / a);
break;
}
//拼接出题目
tops.push(str2 + " " + infix[i] + str1);
int j;
//判断当前运算符与后面的运算符优先级的关系,小的时候就要给题目也加括号
for(j = k;j<str.length;j++) {
if(mp.get(infix[i]) < mp.get(str[k])||(mp.get(infix[i]) == mp.get(str[k]) && str[k] == '-')) {
break;
}
}
if(j<str.length) {
String s1 = tops.pop();
tops.push(" (" + s1 + " )");
}
k++;
}
}
//将本轮生成的题目记录下来,用于以后输出到文件
topic[n].append(tops.peek());
standAnswer[n].append(tops.pop());
//返回计算结果
return (int) s.pop();
}
2. 关键代码
- 第一条... 变量名应为驼峰式风格且首字母大写
- 第二条… 异常进行手动处理,不抛出。
- 第三条… 进行适当合理的代码注释,方便理解,修正
五、测试
测试 | 预期结果 | 实际结果 |
---|---|---|
不输入参数 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 |
只输入一个参数:100 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 |
-n 10 -grade 1 | 小学1年级数学题题目已生成,请查看out.txt文件 | 小学1年级数学题题目已生成,请查看out.txt文件 |
-n 10.5 -grade 1 | 题目数量不是正整数,请重新运行,输入一个正整数 | 题目数量不是正整数,请重新运行,输入一个正整数 |
-n ascc -grade 2 | 题目数量不是正整数,请重新运行,输入一个正整数 | 题目数量不是正整数,请重新运行,输入一个正整数 |
-n 10 -grade vsdv | 目前只支持13年级,请重新运行,输入13中的一个数字 | 目前只支持13年级,请重新运行,输入13中的一个数字 |
-n 00001 -grade 3 | 小学3年级数学题题目已生成,请查看out.txt文件 | 小学3年级数学题题目已生成,请查看out.txt文件 |
-n 1000 -grade 2.3 | 目前只支持13年级,请重新运行,输入13中的一个数字 | 目前只支持13年级,请重新运行,输入13中的一个数字 |
-n 10 -grade 002 | 小学2年级数学题题目已生成,请查看out.txt文件 | 小学2年级数学题题目已生成,请查看out.txt文件 |
-n 10000 -grade 3 | 小学3年级数学题题目已生成,请查看out.txt文件 | 小学3年级数学题题目已生成,请查看out.txt文件 |
-n -1 -grade 3 | 题目数量不是正整数,请重新运行,输入一个正整数 | 题目数量不是正整数,请重新运行,输入一个正整数 |
1000 -n -grade 2 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 |
-n 10 -grade -3 | 目前只支持13年级,请重新运行,输入13中的一个数字 | 目前只支持13年级,请重新运行,输入13中的一个数字 |
-n 1000 2 -grade | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 | 输入的参数形式有误,请按照 -n 题量 -grade 数量 或者 -grade 数量 -n 题量 格式输入。 |
-grade 2 -n 1000 | 小学2年级数学题题目已生成,请查看out.txt文件 | 小学2年级数学题题目已生成,请查看out.txt文件 |
-grade 0.1 -n 800 | 目前只支持13年级,请重新运行,输入13中的一个数字 | 目前只支持13年级,请重新运行,输入13中的一个数字 |
-grade a1 -n 10 | 目前只支持13年级,请重新运行,输入13中的一个数字 | 目前只支持13年级,请重新运行,输入13中的一个数字 |
-grade 001 -n 20 | 小学1年级数学题题目已生成,请查看out.txt文件 | 小学1年级数学题题目已生成,请查看out.txt文件 |
-grade 1 -n 0000000002 | 小学1年级数学题题目已生成,请查看out.txt文件 | 小学1年级数学题题目已生成,请查看out.txt文件 |
-grade 0.1 -n 0.1 | 题目数量不是正整数,请重新运行,输入一个正整数 | 题目数量不是正整数,请重新运行,输入一个正整数 |
-n a1 -grade a1 | 题目数量不是正整数,请重新运行,输入一个正整数 | 题目数量不是正整数,请重新运行,输入一个正整数 |
六、总结
- 有了上一次的经验之后,这次的作业我并没有盲目的开始敲代码,而是开始仔细分析需求,设计思路,在一开始就确定好要实现哪些功能,每个功能之间如何协调搭配,每次只写一钟功能,在确定无误后继续对下一功能再次分析,这样做的好处在于敲代码时会行云流水,一气呵成,但缺点在于前期准备工作过长,所以下次得寻求合理分配时间。
- 感觉自己在单元测试方面还是有所欠缺,没写晚一个方法后,对次方法测试得不是很到位,在后续的开发中还是会出现在之前写好的方法中发现这样那样的错误
- 这次的项目采用的是结对编程的方式,通过两个人的探讨会发现自己所未考虑道德思维漏洞,这对于我前期的需求分析很有帮助。对于结对编程有如下两点看法:
- 结对编程并不适用于简单的写代码的工作,结对编程更适用于解决一些方向性的问题。
- 结对编程中,双方的互动目的在于开启思路,避免单独编程时思维容易阻塞的情况。
附上结对编程工作图:
- 在将C++代码转换成java代码时,得对两种语言的特性,异同点有所了解,这一点也是一直困扰着大家,让一些人只能望而却步,再就是对空指针异常的处理,java中发生空指针异常一般就是这个数组、集给等中没有赋值对象,要做好临界值的判断
- 在写代码的过程中不能太死板,不要生搬硬套别人的代码,至少大概逻辑要理解清除这样才有利于设计出对整个程序更有利的代码