软工结对项目之四则运算的生成计算界面
这是第一次真正意义上的结对项目,两个人一起完成增加了写代码的效率和正确性,往往自己不容易发现自己的bug,但你的队友却能很快发现bug并一起debug。另一方面,两个人的想法也更加丰富,比如在功能的设置拓展上,在界面的美化上,在细节处理上,1+1都是大于2的。希望以后可以多多尝试这种结对项目~
1、Github地址
首先给出我们组的github库的地址:
https://github.com/laopangpyy/Four-arithmetic-operation
2、我的队友的csdn博客地址
https://blog.csdn.net/weixin_41067683/article/details/86560252
ps:我和她虽然写得有所不同,但思路都是一样的。如果觉得我有哪里写得不明白的地方,欢迎大家去看看她的博客~
3、psp表格—估计花费时间
| psp2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 80 | |
| Estimate | 估计这个任务需要多少时间 | 2875 | |
| Development | 开发 | 1550 | |
| Design Spec | 生成设计文档 | 60 | |
| Design Rewiew | 设计复审(和同事审核设计文档) | 30 | |
| Coding Stantard | 代码规范(为目前的开发制定合适的规范) | 30 | |
| Design | 具体设计 | 210 | |
| Coding | 具体编码 | 1400 | |
| Code Review | 代码复审 | 170 | |
| Test | 测试(自我测试、修改代码、提交修改) | 270 | |
| Reporting | 报告 | 150 | |
| Test Report | 测试报告 | 100 | |
| Size Measurement | 计算工作量 | 40 | |
| Postmortem Process Improvement Plan | 事后总结,并提出改进计划 | 45 | |
| 合计 | 2875 |
4、解题设计思路
这里我按照题目要求的三个阶段对我的三个思路进行分别说明,以最后的图形界面为最终目标,逐步完善功能。
首先我给出我们的总体思路图,之后再为大家逐一说明。

1)第一阶段:
a、一次可以出一千道题且没有重复的
生成题目我放到后面说,这句话里的重点是生成的题目没有重复,即要保证任何两道题目不能通过有限次交换加号和乘号左右的算术表达式交换变为同一道题目。如果是简单的随机生成式子并简单的用数组保存是很难完全记录所有相等的情况的,所以,这里我们采取用二叉树来保存随机生成的算式。
每一个算式对应一个二叉树,有限次交换+和左右的算术表达式即所有二叉树的+和根节点的左右子节点任意交换次数不能得到相同的二叉树结构。因此,我们只需要对二叉树的节点有明确的规则限制即可直接对二叉树进行比较来确保不重复。这里我们采取的调整规则有:保证左子节点的值大于右子节点的值;如果左子树的根节点和右子树的根节点一个为运算符一个为数值的话,保证运算符节点在左子树上;如果左右子树都是运算符,则运算符优先级高的放在左边。之后再对调整过的子树进行比较,这样即可保证要求中3+(2+1)等价于1+2+3,而1+2+3不等价与3+2+1,具体测试在后面的部分有写,具体代码见github代码库里。
这里的1000道题的设定,由于图形界面的产生,我们将数字不仅仅局限于1000,用户可以输入自己想要的题目数量,我们会随机生成一个对应数量的没有重复题目的题库,保存至一个list里面,同时将题目输入至文件,用户做题时我们再一道一道地将题目显示出来。
b、多于一个运算符进行表达式求值
-
对于生成算式部分:
一道题目有四个部分,运算符、数字、题目长度、括号。这里,我们将括号和运算符都用数字表示,便于利用随机函数生成,从101到107分别表示+、-、*、/、^、(、)。首先随机生成一个数字为题目长度,然后再随机生成一个数字紧接着随机生成一个运算符。这里需要注意的是除号后面不能为0和乘方后面不能为太大的数。另外,对于括号,我们要特别注意前后括号的对应关系,随机生成左括号后需要生成右括号并且左右括号的数目应该相等,因此在表达式的末尾我们需要加上所有缺少的后括号。 -
对于求解算式部分:
为了对应查重部分,我们将算式放入二叉树进行求解并保存结果,这里的二叉树的节点我们区分了运算符和数值,叶子节点都是数值,非叶子节点都是运算符,而数字类型我们采取了分数,便于后面的运算。我们的思路是:首先将中缀的算式利用栈和队列转换成对应的后缀表达式,这样可以消除表达式中的括号;其次,我们将后缀表达式利用栈转换成对应的二叉树:遇到数字则入栈,遇到运算符则弹出两个数字进行树的构造,之后再将此子树放入栈内,最终栈内的结果为整棵二叉树的构造;最后,我们在二叉树内部从叶子节点开始逐一向根节点进行运算,每一步运算的结果保留在每一个根节点(运算符节点)中,便于下一步运算和之后的查重。
c、除了整数外还要支持真分数的计算
由于分数的运算规则和整数相比还是有很大的不同的,为了将所有的数值用一个数据类型保存,我们将所有整数都转换成分母为1的分数进行分数的加减乘除乘方运算。我们定义一个分数类,属性主要有分子、分母、值、符号四个部分,我们在这个类里将所有运算进行重构,将+、-、*、/、^五个运算符号分别用分数运算来代替。因此,我们在生成算式部分用整数进行生成,生成后有整数和分数,进行运算时,我们保存至二叉树里的值便是分数类型的表示形式,利用分数形式进行计算。这里有个要注意的地方就是,如果全部都是分数运算的话,最后的分数形式很有可能不是最简形式,所以我们需要化简函数,利用最后的分子分母同时除以他们的最大公约数达到最后的正确答案。
d、接受用户输入的答案并判定对错,最后给出对/错的数量
前面也有说到,我们将用户自定义数量的随机生成的题库放入list中,同时我们将每道题的对应二叉树也保存在一个list中,其中,二叉树的根节点中保存着算式的正确答案。在图形界面里一次只显示一道算式,用户在界面里输入自己的答案,点击按钮的同时,我们将用户的答案与我们的答案进行对比,一样即为正确,否则错误。同时,我们利用两个全局变量记录用户的正确题数和错误题数,在最后结束游戏的时候messagebox.show用户的对错数量,达到此要求。
2)第二阶段:
a、增加乘方运算
由于这是第二阶段,我们根据用户的选择来决定是否进行乘方计算,乘方计算其实和其他运算符类似,前面我也有说乘方运算。只需要注意乘方运算的优先级高于其他运算符,在将中缀表达式生成后缀表达式时注意即可,其他的和其他运算符的程序走向并无太大不同。
b、可以设置两种表达方式
由于我们运算过程中,将符号都用数字表示,因此,^ 和** 的两种表示形式仅仅只在生成算式的最后一部分即将数字变为运算符中有区别,只需要利用两种转变函数,一种转变成^,一种转变成两个*,其他的并没有什么区别。而关于设置两种表达方式,只需要用到radiobutton和全局变量即可满足要求。
3)第三阶段:ui界面
这里我们采用了两种ui界面,都用到的是c#语言,一种是基本的windows窗口程序,一种是unity。这里,由于时间有限,我们只将前者满足了所有功能及所需细节和错误处理,而unity我们仅仅实现了基本的功能。对于具体的界面,我会在后面进行图片展示。
a、20s倒计时
这里我们利用工具箱里自带的time工具进行倒计时,一但时间到了便到达下一题,到达新的题目时便重新计时。
b、历史记录
我们每做一道题,不管对错便将题目和正确答案加入到text中,利用字符串的拼接进行历史记录的显示,最后,当我们设定的游戏失败或胜利时,将跳入至历史记录界面进行结束,用户可以看到自己做过的所有题目。
5、设计实现过程
1)函数间和类间关系
除了界面本身的大类和program类,我们一共分为了5个大类,分别为:
- Binarytree(二叉树类,里面其实还有结点,结点同样也有运算符结点和数值结点类)
- build(生成类,这里面包含有生成运算符的大部分函数,而算式的expr数组和长度同样也属于此类)
- ans(答案类,这里面主要是将用户输入答案和正确答案进行比较和格式判定转换的相关函数)
- calculate(计算类,这里将运算符对应的数字和运算对应起来,这里的运算是分数运算)
- num(数值类,将数字用分数表示,并重构加减乘除乘方运算和==判定)
下面这幅图为我们的几乎所有的函数间的大部分联系,共同实现生成一个算式、查重、运算和比对答案的主要功能,这里我将大概的流程说一下:
- 用户通过界面,选择生成模式(是否有乘方、乘方为^还是**)和运算式里的最大数值,将参数传至buildexp函数里,输出一个expr数组,数组中运算符和括号都用数字表示,同时我们利用printexp1和printexp2函数对乘方符号进行两种方式的转换,并存入另一个字符串中。字符串会同时放入至strlist中,便于达到一定数量后一起输入至文件和显示至界面中。
- 由于expr数组存的是中缀表达式,为了便于生成二叉树进行查重和去掉括号,我们利用turntohou函数将表达式转为后缀表达式并更新expr数组。之后利用expr后缀表达式我们生成二叉树,这个二叉树的特点是,只有叶子结点表示的是数值,其他的都是运算符,而且越先算的越靠底部,同时,在构造二叉树的过程中,我们将数字都存成分数形式即num类,在二叉树生成后可以直接进行分数的计算。
- 对于生成的二叉树,我们先利用preordercalc函数进行运算,将运算结果保存在根节点上。之后我们开始查重处理,将二叉树按照制定的规则adjust函数进行调整,调整后将两个二叉树利用comparetree函数进行比较。这里为了避免重复,我们将符合条件的二叉树放入至treelist中,每次生成一个新的二叉树,我们调整后与treelist中的所有二叉树都利用comparetree函数进行比较,如果没有相同的则将此二叉树加入treelist,如果重复则continue,继续生成下一个算式。
- 利用preordercalc函数进行运算后的结果,保存在每棵树的根结点上,我们得到此二叉树的正确答案只需要在getroot().value利用reduction函数进行约分即可,而对应算式也在相同索引的strlist里存放着。最后,当用户输入答案时,点击按钮,调用getresult比较函数将正确答案和用户输入的答案进行比较,正确即得分,错误则不得分,而20s内不输入也不得分。

2)单元测试的设计
我分为两个部分设计了测试用例:
- 第一个部分为计算表达式的部分,这部分我设计了四个测试用例,分别从加减乘除运算、括号和乘方运算、用户输入格式错误处理、用户输入答案错误的处理四个方面进行测定,测试过程中用到了几乎所有类,这里利用的是getresult函数的返回值作为assert中的判定。
- 第二个部分为查重部分,这部分我设计了三个测试用例,分别对68等价于86、3+(2+1)等价于1+2+3、1+2+3不等价与3+2+1三种情况进行测试,测试过程中也涵盖了大部分类,这里利用comparetree函数的返回值作为assert中的判定。
3)单元测试的实例截图
这里我选择将全部放上来,因为这些测试都是很有必要的
a、测试加减乘除的计算正确性
[TestMethod]
public void TestMethod1()
{//测试加减乘除的计算正确性
Build build = new Build();
build.Expr[0] = 1;
build.Expr[1] = 101;
build.Expr[2] = 7;
build.Expr[3] = 102;
build.Expr[4] = 6;
build.Expr[5] = 103;
build.Expr[6] = 3;
build.Expr[7] = 104;
build.Expr[8] = 2;
build.flag = 9;
build.p = 9;
//表达式是1+7-6*3/2
build.TurnToHou();
BinaryTree tree;
tree = build.createtree();//将后缀表达式转换为二叉树
tree.preordercalc(tree.getroot());//计算最终的结果并存在根节点
Ans ans = new Ans();
tree.getroot().value.Reduction();
MyNumber correctAnswer = tree.getroot().value;//这里保存了计算出来的答案
int ansflag = ans.GetResult(correctAnswer, "-1");
Assert.AreEqual(1, ansflag);
}
b、测试括号和乘方的计算正确性
[TestMethod]
public void TestMethod2()
{//测试括号和乘方的计算正确性
Build build = new Build();
build.Expr[0] = 1;
build.Expr[1] = 101;
build.Expr[2] = 106;
build.Expr[3] = 7;
build.Expr[4] = 102;
build.Expr[5] = 6;
build.Expr[6] = 107;
build.Expr[7] = 105;
build.Expr[8] = 3;
build.Expr[9] = 104;
build.Expr[10] = 2;
build.flag = 11;
build.p = 11;
//表达式是1+(7-6)^3/2
build.TurnToHou();
BinaryTree tree;
tree = build.createtree();//将后缀表达式转换为二叉树
tree.preordercalc(tree.getroot());//计算最终的结果并存在根节点
Ans ans = new Ans();
tree.getroot().value.Reduction();
MyNumber correctAnswer = tree.getroot().value;//这里保存了计算出来的答案
int ansflag = ans.GetResult(correctAnswer, "3/2");
Assert.AreEqual(1, ansflag);
}
c、测试输入答案的格式不正确的识别情况
[TestMethod]
public void TestMethod3()
{//测试输入答案的格式不正确的识别情况
Build build = new Build();
build.Expr[0] = 1;
build.Expr[1] = 101;
build.Expr[2] = 7;
build.Expr[3] = 102;
build.Expr[4] = 6;
build.Expr[5] = 103;
build.Expr[6] = 3;
build.Expr[7] = 104;
build.Expr[8] = 2;
build.flag = 9;
build.p = 9;
//表达式是1+7-6*3/2
build.TurnToHou();
BinaryTree tree;
tree = build.createtree();//将后缀表达式转换为二叉树
tree.preordercalc(tree.getroot());//计算最终的结果并存在根节点
Ans ans = new Ans();
tree.getroot().value.Reduction();
MyNumber correctAnswer = tree.getroot().value;//这里保存了计算出来的答案
int ansflag = ans.GetResult(correctAnswer, "-1’");
Assert.AreEqual(2, ansflag);
}
d、测试输入错误答案的识别情况
[TestMethod]
public void TestMethod4()
{//测试输入错误答案的识别情况
Build build = new Build();
build.Expr[0] = 1;
build.Expr[1] = 101;
build.Expr[2] = 7;
build.Expr[3] = 102;
build.Expr[4] = 6;
build.Expr[5] = 103;
build.Expr[6] = 3;
build.Expr[7] = 104;
build.Expr[8] = 2;
build.flag = 9;
build.p = 9;
//表达式是1+7-6*3/2
build.TurnToHou();
BinaryTree tree;
tree = build.createtree();//将后缀表达式转换为二叉树
tree.preordercalc(tree.getroot());//计算最终的结果并存在根节点
Ans ans = new Ans();
tree.getroot().value.Reduction();
MyNumber correctAnswer = tree.getroot().value;//这里保存了计算出来的答案
int ansflag = ans.GetResult(correctAnswer, "-3");
Assert.AreEqual(0, ansflag);
}
e、测试1+2+3等价于3+(2+1)
[TestMethod]
public void TestMethod5()
{//测试1+2+3和3+(2+1)
Build build1 = new Build();
build1.Expr[0] = 1;
build1.Expr[1] = 101;
build1.Expr[2] = 2;
build1.Expr[3] = 101;
build1.Expr[4] = 3;
build1.flag = 5;
build1.p = 5;
//表达式是1+2+3
build1.TurnToHou();
BinaryTree tree1 = build1.createtree();
tree1.preordercalc(tree1.getroot());//计算最终的结果并存在根节点
tree1.adjustTree(tree1.getroot());
Build build2 = new Build();
build2.Expr[0] = 3;
build2.Expr[1] = 101;
build2.Expr[2] = 106;
build2.Expr[3] = 2;
build2.Expr[4] = 101;
build2.Expr[5] = 1;
build2.Expr[6] = 107;
build2.flag = 7;
build2.p = 7;
//表达式是3+(2+1)
build2.TurnToHou();
BinaryTree tree2= build2.createtree();
tree2.preordercalc(tree2.getroot());//计算最终的结果并存在根节点
tree2.adjustTree(tree2.getroot());
int flag = build1.compareTree(tree1, tree2);
Assert.AreEqual(0, flag);
}
f、测试1+2+3不等价于3+2+1
[TestMethod]
public void TestMethod7()
{//测试1+2+3和3+2+1
Build build1 = new Build();
build1.Expr[0] = 1;
build1.Expr[1] = 101;
build1.Expr[2] = 2;
build1.Expr[3] = 101;
build1.Expr[4] = 3;
build1.flag = 5;
build1.p = 5;
//表达式是1+2+3
build1.TurnToHou();
BinaryTree tree1 = build1.createtree();
tree1.preordercalc(tree1.getroot());//计算最终的结果并存在根节点
tree1.adjustTree(tree1.getroot());
Build build2 = new Build();
build2.Expr[0] = 3;
build2.Expr[1] = 101;
build2.Expr[2] = 2;
build2.Expr[3] = 101;
build2.Expr[4] = 1;
build2.flag = 5;
build2.p = 5;
//表达式是3+2+1
build2.TurnToHou();
BinaryTree tree2 = build2.createtree();
tree2.preordercalc(tree2.getroot());//计算最终的结果并存在根节点
tree2.adjustTree(tree2.getroot());
int flag = build1.compareTree(tree1, tree2);
Assert.AreEqual(1, flag);//应该是不同的
}
g、测试68等价于86
[TestMethod]
public void TestMethod6()
{//测试6*8 8*6
Build build1 = new Build();
build1.Expr[0] = 6;
build1.Expr[1] = 103;
build1.Expr[2] = 8;
build1.flag = 3;
build1.p = 3;
//表达式是6*8
build1.TurnToHou();
BinaryTree tree1 = build1.createtree();
tree1.preordercalc(tree1.getroot());//计算最终的结果并存在根节点
tree1.adjustTree(tree1.getroot());
Build build2 = new Build();
build2.Expr[0] = 8;
build2.Expr[1] = 103;
build2.Expr[2] = 6;
build2.flag = 3;
build2.p = 3;
//表达式是8*6
build2.TurnToHou();
BinaryTree tree2 = build2.createtree();
tree2.preordercalc(tree2.getroot());//计算最终的结果并存在根节点
tree2.adjustTree(tree2.getroot());
int flag = build1.compareTree(tree1, tree2);
Assert.AreEqual(0, flag);
}
3)单元测试的结果
对于七个测试用例,实际值与预期值都相同,如图所示:

6. 性能分析与改进
1)性能改进
在刚开始设计图形界面时,我们应用了较多的弹出框提示信息,后来经分析发现MessageBox.Show的cpu花费较多,我们将很多提示信息进行了转化和修改,尽可能少地应用提示框,较为有效地减少了图形界面控件使用部分的时间花费。
同时,我们也对原本最大花费的compareTree函数(比较两棵树是否重复)做了优化,将函数中判定符号的部分由与每个符号相比较改成了给每个符号赋类型值,直接进行简单的符号比较,虽然最后的性能分析表明仍是花费最多的函数,但仍较明显的提升了生成表达式的速度。
2)对图形界面的性能分析
使用生成题目数为4为例(即生成求解部分几乎不占用时间),对图形界面的性能进行分析。从性能分析图可看出,主要的性能花费在图形界面的控件使用上,其中MessageBox.Show花费最多,而button1按钮在button2按钮中的调用花费仅次于MessageBox.Show



- 对生成求解1000个表达式并输出到文件的性能分析
由性能分析图知,生成、判重、求解1000个表达式并将其输出到文件里共花费59ms,其中时间花费最多的函数是对两棵二叉树的是否重复的比较函数。其次是中缀转后缀函数的花费较多。


7、ui界面的动画展示
1)windows窗口应用程序:
a、错误处理和自定义题目全部做完的情况

b、需20s内做完和10次机会用完的情况

2)unity
由于gif太大所以不能穿上来,放几张图吧





8、psp表格—实际花费时间
| psp2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 80 | 110 |
| Estimate | 估计这个任务需要多少时间 | 2875 | 3630 |
| Development | 开发 | 1550 | 2120 |
| Anylasis | 需求分析(包括学习新技术) | 290 | 350 |
| Design Spec | 生成设计文档 | 60 | 80 |
| Design Rewiew | 设计复审(和同事审核设计文档) | 30 | 40 |
| Coding Stantard | 代码规范(为目前的开发制定合适的规范) | 30 | 30 |
| Design | 具体设计 | 210 | 270 |
| Coding | 具体编码 | 1400 | 2000 |
| Code Review | 代码复审 | 170 | 140 |
| Test | 测试(自我测试、修改代码、提交修改) | 270 | 330 |
| Reporting | 报告 | 150 | 130 |
| Test Report | 测试报告 | 100 | 60 |
| Size Measurement | 计算工作量 | 40 | 40 |
| Postmortem Process Improvement Plan | 事后总结,并提出改进计划 | 45 | 50 |
| 合计 | 2875 | 3630 |
这是一个软工结对项目,旨在生成无重复的四则运算计算界面,支持整数和真分数计算。项目分为三个阶段,包括生成计算题、增加乘方运算和设计UI界面。采用二叉树结构避免重复题目,通过单元测试验证正确性和功能。实现了20s倒计时、历史记录和多种表达式设置。

1272

被折叠的 条评论
为什么被折叠?



