结对作业
写在前面
这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序并熟悉结对开发的流程以及锻炼编程能力 |
项目地址 | 传送门 |
项目成员1 | 3119005420李文龙 |
项目成员2 | 3119005440赵有为 |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 96 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 96 |
Development | 开发 | 2480 | 3165 |
· Analysis | · 需求分析 (包括学习新技术) | 240 | 200 |
· Design Spec | · 生成设计文档 | 120 | 100 |
· Design Review | · 设计复审 (和同事审核设计文档) | 120 | 70 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 45 |
· Design | · 具体设计 | 240 | 550 |
· Coding | · 具体编码 | 900 | 1240 |
· Code Review | · 代码复审 | 300 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 500 | 550 |
Reporting | 报告 | 390 | 470 |
· Test Report | · 测试报告 | 120 | 180 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 240 | 250 |
合计 | 2930 | 3601 |
二、效能分析
- 整体情况
- 类占用情况
- cpu调用情况视图
改进思路:在获取表达式方法中(Expression类),首先构造一个HashMap,根据生成的表达式的长度来匹配对应的根节点集合,使得每次表达式只与和自己长度相同表达式进行根节点的比较,减少比对次数,然后再通过Compare类中的比较两棵树的方法,
判断两个表达式是否相同或满足交换律。
三、设计实现过程
- Calculator类:计算表达式
- Compare类:与标准答案进行比对,分别算出正确和错误的题目的数量以及题号
- Compute类 :四则运算,加减乘除
- ConstructTree类:为每个式子构造一个二叉树,首先先从左到右依次遍历表达式,如果遇到乘号,除号或者括号的情况就先处理,然后从下到上逐层构造二叉树,最后返回由表达式构成的二叉树的根节点
- TreeNode类 :用来生成树结点,以便构建每个表达式的二叉树形式
- Expression类:生成表达式
- Fraction类:分数构造类
- OperationFrame类:所搭建的简易界面工具类
- WriteUtil类:将结果写入txt的工具类
类关系图
代码说明
表达式计算方法
public static String Calculate(String question) {
//初始化
question=removeStrSpace(question);
BiggerFlag=false;
fractionStack = new Stack<>();//
operationStack = new Stack<>();
NumStack = new Stack<>();
StringBuilder nowFractionNum = new StringBuilder();
int aux=0;//用来记录当前真分数的值
int length = question.length();
for (int i = 0; i < length; i++) {
char temp = question.charAt(i);
if (isNumber(temp)) nowFractionNum.append(temp);//如果是数字就放入到builder之中
else if (temp=='(') {
//括号情况(如果出现左括号,说明前面的数已经是完毕了的,就生成然后装填)
if(!NumStack.empty()) {
int demoninator = 1;
int molecule = NumStack.pop();
fractionStack.push((new Fraction(molecule, demoninator)));
}
i=DealBracket(i,question);
if(i==-1) return null;
} else if (temp == '’') { //处理真分数的真值
StringToNum(nowFractionNum);
aux = NumStack.pop(); //得出真分数的真值;
}else if(temp=='/'){
operationStack.push('/');
StringToNum(nowFractionNum);
}
else { //其他符号
if(nowFractionNum.length()!=0)
StringToNum(nowFractionNum); //得出数字并且压入栈
aux = convertMixed2Improper(aux);
if (!NumStack.empty()) //如果分数的分母为1
{
int demoninator=1;
int molecule=NumStack.pop();
fractionStack.push((new Fraction(molecule, demoninator)));
}
//优先计算乘除法
if (fractionStack.size() > 1&&!operationStack.empty()&&(operationStack.peek()=='*'||operationStack.peek()=='÷')) {
operator();
}
//然后计算加减法
if (fractionStack.size() > 1&&!operationStack.empty()&&(operationStack.peek()=='+'||operationStack.peek()=='-')) {
if(temp=='+'||temp=='-')
operator();
}
if(temp!='=')
operationStack.push(temp);//将符号压入栈
else{
//最后处理剩余的符号
while(!operationStack.empty()){
operator();
if(BiggerFlag) return null;
}
}
}
}
//判断最后得出的结果是否唯一
if (fractionStack.size() == 1) {
//将最后的结果转为真分数形式
Fraction result = fractionStack.pop();
return Fraction.GetFraction(result.molecule,result.demoninator);
}
return null;
}
括号处理方法
public static int DealBracket(int point,String question){ //返回处理表达式的长度
StringBuilder nowFractionNum1=new StringBuilder();
int aux=0;
for(int j=point+1;j<question.length();j++) {
char temp = question.charAt(j);
if (isNumber(temp)) nowFractionNum1.append(temp);
else if (temp == '’') {
StringToNum(nowFractionNum1);
aux = NumStack.pop(); //得出真分数的真值;
} else if (temp == '/') {
operationStack.push('/');
StringToNum(nowFractionNum1);
} else { //其他符号
StringToNum(nowFractionNum1); //得出数字并且压入栈
aux=convertMixed2Improper(aux);
if (!NumStack.empty()) {
int demoninator = 1;
int molecule = NumStack.pop();
fractionStack.push((new Fraction(molecule, demoninator)));
}
if (fractionStack.size() > 1&&temp==')') {
operator();
if (BiggerFlag) return -1;
}
if (temp != ')')
{
operationStack.push(temp);
}
else {
return j;
}
}
}
return -1;
}
为每个式子构造一个二叉树,首先先从左到右依次遍历表达式,如果遇到乘号,除号或者括号的情况就先处理,然后从下到上逐层构造二叉树,最后返回由表达式构成的二叉树的根节点
public static TreeNode buildTree(String str){
// 分别存放数字和操作符
ArrayList<TreeNode> numbers = new ArrayList<>();
ArrayList<TreeNode> operations = new ArrayList<>();
//初始化一个根节点
TreeNode root=new TreeNode();
TreeNode binaryNode;
StringBuilder temp = new StringBuilder();
int flag1=0;
for (int i = 0; i < str.length(); i++) {
// 取出字符串的各个字符
char ch = str.charAt(i);
//判断为符号还是数字,若为数字,则将s+=ch(防止数字为十位百位数)
if (ch >= '0' && ch <= '9') {
temp.append(ch);
}else if(ch=='('||ch==')'){
if(ch==')') flag1++;
}else if(ch=='/'||ch=='’'){
numbers.add(new TreeNode(temp.toString()));
flag1++;
operations.add(new TreeNode(ch + " "));
temp = new StringBuilder();
}
//若为运算符,则将s和ch分别放入numbers、operations数组队列
else {
numbers.add(new TreeNode(temp.toString()));
// 这是处理/与),*和÷的情况
if(flag1>0){
root=FirstBuild(flag1,numbers,operations,root);
flag1=0;
}
operations.add(new TreeNode(ch + " "));
if(ch=='*'||ch=='÷') flag1++;
temp = new StringBuilder();
}
}
if(flag1>0)
root=FirstBuild(flag1,numbers,operations,root);
// 再处理剩下的符号和数字
while(operations.size()>1) {
// 从运算符中取出第一个作为node的数据;
binaryNode = operations.get(0);
operations.remove(0);
//从数字取出第一个、第二个作为左、右;
if(numbers.get(0)!=null){
binaryNode.setLeft(numbers.get(0));
numbers.remove(0);
}
else binaryNode.setLeft(null);
// System.out.println("左节点值为"+binaryNode.getLeft().getString());
if(numbers.get(0)!=null){
binaryNode.setRight(numbers.get(0));
numbers.remove(0);
}
else binaryNode.setRight(null);
// System.out.println("右节点值为"+binaryNode.getRight().getString());
//构建node,将其作为根节点root放回数字列表
root = binaryNode;
// System.out.println("根节点的值为"+binaryNode.getString());
numbers.add(0, binaryNode);
}
// 返回根节点
return root;
}
private static TreeNode FirstBuild(int flag1,ArrayList<TreeNode> numbers,ArrayList<TreeNode> operations,TreeNode root){
TreeNode binaryNode;
while (flag1>0){
if(operations.size()<1) break;
// 首先取出最后操作符,然后先构建一个树,再加入到List中
binaryNode=operations.get(operations.size()-1);
operations.remove(operations.size()-1);
// 取出右节点(若为空,设置为空)
if(numbers.get(numbers.size()-1)!=null){
binaryNode.setRight(numbers.get(numbers.size()-1));
numbers.remove(numbers.size()-1);
}
else binaryNode.setRight(null);
//System.out.println("右节点值为"+binaryNode.getRight().getString());
// 取出左节点(若为空,设置为空)
if(numbers.get(numbers.size()-1)!=null){
binaryNode.setLeft(numbers.get(numbers.size()-1));
numbers.remove(numbers.size()-1);
}
root=binaryNode;
numbers.add(numbers.size(), binaryNode);
flag1-=1;
}
return root;
}
分数的构造
public static String GetFraction(int molecule, int denominator){
if(denominator==0) {//判断当前分母是否为0
Calculator.BiggerFlag=true;
return null;
}
molecule=Math.abs(molecule);//求绝对值
denominator=Math.abs(denominator);
int x=gcd(molecule, denominator);//约分
molecule/=x;
denominator/=x;
int temp=molecule/denominator;//真分数
//求分子的最小值
molecule%=denominator;
//如果分子是分母的整数倍
if(molecule==0) return temp+"";
String result= molecule +"/"+ denominator;
if(temp==0){
return result;
}else{
return temp +"’"+result;
}
}
表达式的生成与获取
static Random ran = new Random();
private static final char[] symbol=new char[]{'+','-','*','÷'};
public static HashMap<Integer, List<TreeNode>> map=new HashMap<>();//用来存放映射,检验是否有重复的式子
public static void GetExpression(){//获得随机生成的表达式
int i=1;
boolean flag=false;
//根据输入生成指定数目的表达式
while(i<=Main.OperatorNumber) {
String question = GetQuestion() + "=";
// 判断当前生成的表达式是否已经存在(二叉树方法)
//System.out.println("生成的式子为 "+question);
TreeNode root= ConstructTree.buildTree(question);
int size=question.length();
// 因为二叉树中不存在括号。所以要特殊判断
if(question.contains("(")){
size-=2;
}
// 根据式子的长度,得到对应的映射。映射中包含该长度式子的所有根节点
List<TreeNode> treeNodes=map.get(size);
// List如果为空,就初始化List
if(treeNodes==null)
{
map.put(size,new LinkedList<>());
treeNodes=map.get(size);
}
else{
// 如果不为空。则循环遍历List
for (TreeNode treeNode : treeNodes) {
//如果不存在就加入,并计算结果,如果存在就提前结束
if(root.getString().equals(treeNode.getString())) {
if (Compare.CompareTreeNode(root, treeNode)) {
flag = true;
break;
}
}
}
if(flag) {
flag=false;
continue;
}
}
// 将这个式子将入到map中
treeNodes.add(root);
map.put(size,treeNodes);
String result = Calculator.Calculate(question);
//如果结果不为null(即符合题目要求)
if (result != null) {
// 将结果写入到文件中
String FinalQuestion = "第" + i + "道题目: " + question;
WriteUtil.write(Main.QuestionPath, FinalQuestion);
try {
String FinalResult = "第" + i + "道答案: " + result;
WriteUtil.write(Main.AnswerPath, FinalResult);
} finally {
i++;
}
}
}
}
//获取表达式
private static String GetQuestion(){
int operatorNum = ran.nextInt(3) + 1;//操作符号个数
StringBuilder builder=new StringBuilder();
int molecule=ran.nextInt(Main.range);//分子
int denominator=ran.nextInt(Main.range)+1;//分母,分母不能为0
int brackets = 0;
//判断添加括号 (0,2,4是不生成括号,1,3,5是生成括号)
if(operatorNum>1) brackets= ran.nextInt(6);
if(operatorNum>1&&brackets==1) {
builder.append("(");
}
builder.append(Fraction.GetFraction(molecule,denominator));
for(int j=0;j<operatorNum;j++){
molecule=ran.nextInt(Main.range);
denominator=ran.nextInt(Main.range)+1;
int position=ran.nextInt(4);//运算符号在symbol数组中的位置
if(symbol[position]=='÷'&&molecule==0){//除号后面不能为0
j--;
continue;
}
builder.append(symbol[position]);
if(AddLeftBracket(operatorNum,brackets,j)) builder.append("(");
builder.append(Fraction.GetFraction(molecule,denominator));
if(AddRightBracket(operatorNum,brackets,j)) builder.append(")");
}
return builder.toString();
}
测试运行
需求满足情况
1、参数控制(需求1、2)
使用 -n 参数控制生成题目的个数,使用 -r 参数控制题目中数值
命令行
所生成的文件在该jar包的同一目录下,文件名为Exercises.txt 和Answers.txt
PS:以上是未添加界面时打包运行的版本,后续版本因添加了界面因此只需要直接运行
不需要带上参数
2、计算过程中不能产生负数
在代码中的实现
case '-':
Compute.sub(x, y);
if(!isBigger()) BiggerFlag=true;//如果计算出负数就设置为
System.out.println("计算过程不能产生负数");
break;
测试编写
public static void main(String[] args) {
String question="81-8*91+12=";
System.out.println("问题是: "+question);
String calculate = Calculator.Calculate(question);
System.out.println("结果为 "+calculate);
}
结果
3、其结果应是真分数
测试编写
public static void main(String[] args) {
String result = Fraction.GetFraction(12, 8);
System.out.println("结果为 "+result);
}
结果
4、运算符个数不超过3个
从Exercises.txt文件可以看到,参考部分截图,生成的10000个表达式中,不存在超过三个运算符以上的表达式
而在代码中,是通过GetQuestion()方法中定义
进行限制的
详情可以浏览代码说明中 生成与获取表达式部分代码
5、不产生重复的表达式
测试1 从这里可以看到为什么能够不产生不重复的表达式
@Test
public void testDuplicate() {
List<TreeNode> treeNodes;
int size;
TreeNode root;
String question="81-9*8+6=";
root=ConstructTree.buildTree(question);
size=question.length();
treeNodes=map.get(size);
if(treeNodes==null)
{
map.put(size,new LinkedList<>());
treeNodes=map.get(size);
}
treeNodes.add(root);
map.put(size,treeNodes);
String question1="81-8*9+6=";
root=ConstructTree.buildTree(question1);
size=question1.length();
treeNodes=map.get(size);
for (TreeNode treeNode : treeNodes) {
if(root.getString().equals(treeNode.getString())) {
//如果不存在就加入,并计算结果,如果存在就提前结束
if (Compare.CompareTreeNode(root, treeNode)) {
System.out.println("存在式子一样的情况");
return;
}
}
}
System.out.println("不存在式子一样的情况");
treeNodes.add(root);
map.put(size,treeNodes);
}
根据以上可以发现程序会将此类式子视为同一个式子,这个时候就可以保证不会生成重复的表达式,再来看看
测试2 这里的测试使用的是比较***钻的取值
@Test
public void testGenerateExpression(){
Main.range = 1;
Main.OperatorNumber = 10;
Expression.GetExpression();
}
可以看到,即便是在这种情况下,也没有一样的表达式生成
当然,这里题目的个数不能设置过多,我测试的极限是66道题,如果设置的数字大于66,会因为无法构造出不同式子而导致陷入死循环
6、生成的题目存入到执行程序当前目录下的Exercises.txt文件,并同时计算所有题目,并将答案保存在当前目录下的Answer.txt文件中
7、程序能支持生成一万道题目的生成
界面参数控制
生成结果
8、批改对错,以及数量统计
单元测试用例展示
@Test
public void testAdd() {
String question="( 3 + 1 ) + ( 4 + 3 ) =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testSub() {
String question="( 3 - 2 ) - ( 1 - 1 ) =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testDiv() {
String question="( 3 ÷ 4 )÷ 2 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testMul() {
String question="( 3 * 12 ) * 1=";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFraction() {
String question="36 / 14 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFractionAdd() {
String question=" 36 / 14 + 120 / 31 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFractionSub() {
String question=" 360 / 14 - 120 / 31 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFractionDiv() {
String question=" 360 / 14 ÷ 120 / 31 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFractionMul() {
String question=" 361 / 14 * 119 / 31 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testFour(){
String question=" (11 / 14 * 11 / 31) + 4/19 - 1/99 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testMixed() {
String question=" 1’1/4 * 3 ÷ (3+8-4) =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testZero() {
String question=" 4/0 =";
String question1="3 ÷ 0=";
System.out.println("结果为 "+ Calculator.Calculate(question));
System.out.println("结果为 "+ Calculator.Calculate(question1));
}
@Test
public void testNeg(){
String question=" 3-4 =";
System.out.println("结果为 "+ Calculator.Calculate(question));
}
@Test
public void testCompare(){
String path1="MyAnswer.txt";
String path2="Answers.txt";
Compare.CompareTxt(path1,path2);
System.out.println("文件比较完成");
}
@Test
public void testGenerateExpression(){
Main.range = 1;
Main.OperatorNumber = 10;
Expression.GetExpression();
}
@Test
public void testDuplicate() {
List<TreeNode> treeNodes;
int size;
TreeNode root;
String question="81-9*8+6=";
root=ConstructTree.buildTree(question);
size=question.length();
treeNodes=map.get(size);
if(treeNodes==null)
{
map.put(size,new LinkedList<>());
treeNodes=map.get(size);
}
treeNodes.add(root);
map.put(size,treeNodes);
String question1="81-8*9+6=";
root=ConstructTree.buildTree(question1);
size=question1.length();
treeNodes=map.get(size);
for (TreeNode treeNode : treeNodes) {
if(root.getString().equals(treeNode.getString())) {
//如果不存在就加入,并计算结果,如果存在就提前结束
if (Compare.CompareTreeNode(root, treeNode)) {
System.out.println("存在式子一样的情况");
return;
}
}
}
System.out.println("不存在式子一样的情况");
treeNodes.add(root);
map.put(size,treeNodes);
}
覆盖以及通过情况
关键部分都成功覆盖到位了
测试全部通过
项目小结
成员1 李文龙:首先感谢队友在这次实验中的大力帮助。在刚开始接触这个题目的时候,我感觉难度还是有一点的。在写的过程的中,我们一开始只想着先实现前面的几个功能,导致我们要实现后面的一些功能时,发现前面的结构和数据和后面的功能不太好衔接,使得实现后面功能的时候要重新建立一个新的结构,造成空间浪费和逻辑赘余。在实现查询重复的功能时,也花费了较长的时间去创建结构和进行优化处理。整体来说,虽然程序在一定的数据和数量范围内能较快的运行,但是对于一些运算过程和逻辑判断还是存在不必要的重复现象,导致内存消耗。在这次实验中得出的教训就是:一开始构建项目的时候就应该要整体把握项目的结构与功能实现,对各个类的实现有一个基本的认知,在编写程序的时候把握好逻辑判断情况,才能更好地完成一个项目。
成员2 赵有为:在这次项目中,我主要起的是辅助的作用,负责的内容是代码审查、代码测试以及整理资料撰写博客。让我印象最深刻的是,相比个人项目,结对项目在过去个人项目不太起眼的细节问题比如命名、格式、分类等会直接决定项目进展的效率,但也正因为这样,这次结对编程在带来了两个人合作磨合的经验的同时,也让我重新审视了自己过去编写代码的习惯问题,收益颇丰。