本项目由:张永祥(学号3122004546),黄俊杰(学号3122004525)
项目的GitHub链接如下:
正式版:
TimeP1ayer/SimpleArithmeticProgram: 自动生成小学四则运算题目的命令行程序 (github.com)
测试版:
测试版
测试版中包含了我们的所有代码
PSP | Persional Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 5 |
- Estimate | - 估计这个任务需要多少时间 | 10 | 5 |
Development | 开发 | 920 | 915 |
- Analysis | - 需求分析 | 10 | 5 |
- Design Spec | - 生成设计文档 | 100 | 60 |
- Design Review | - 设计复审(和同事审核设计文档) | 20 | 15 |
- Coding Standard | - 代码规范(为目前的开发指定合适的规范) | 10 | 10 |
- Design | - 具体设计 | 300 | 360 |
- Coding | - 具体编码 | 400 | 360 |
- Code Review | - 代码复审 | 20 | 30 |
- Test | - 测试(自测、修改代码、提交修改) | 60 | 75 |
Reporting | 报告 | 40 | 30 |
- Test Report | - 测试报告 | 20 | 10 |
- Size Measurement | - 计算工作量 | 10 | 10 |
- Postmortem & Process Improvment Plan | - 事后总结,并提出过程改进计划 | 10 | 10 |
总时间 | 930 | 920 |
需求分析
本此需求分析在题目里已经有了明确的说明,已经详细到不能再详细了。
该项目中,最为重要的就是查重模块了,为什么?
我们用不同的视角来思考一下。
如果我们的用户是老师:
同一张试卷中遇到两道完全相同的试题,学生本人是十分兴奋的,因为一个解法拿双倍的分。但这样的话学生的能力就得不到锻炼
对于老师来说,在同一张试卷中出现相同的试题,那就是这个老师出题水平不行,这可能导致我们的程序对老师产生一种不利影响。
所以,考虑老师的使用体验,查重功能是必要的。
如果我们的用户的学生,他们通常渴望着刷题,所以寻求一种能快速生成大量题目的程序:
同一张卷子里,如果程序给的试题中,有10%的题目是重复出现的,那学生本人会对这个生成的题目不满意
因为我本来可以练更多的,为什么你一直让我刷重复的题?
当然也不是说不能完全不重复,有时候隔很远出现一次重复,这对学生来说是很好的复习。但这样的功能,我们觉得也必须建立在程序本身具有查重能力的基础上。
所以综上,我们决定:我们的软件中必须要有查重题目的功能。
生成设计文档
这一次将系统设计成三个模块:
- 解释命令行参数模块:负责将接收到的命令行参数转化成实际程序可用的参数,并且会告诉使用该模块的人,什么功能由于收集到足够的参数,因而可以正常运转。
- 随机表达式生成模块:给它要生成的题目数
n
和数值的范围r
,它就能给你生成一个Map
,这里的键Key
是表达式,Value
是表达式对应的答案。 - 输出和读取模块:负责在选择生成表达式功能时,将表达式和对应的结果按某种方式输出到
Exercises.txt
和Answers.txt
中。
具体设计
解释命令行参数模块
对于参数-n,-r,采用正则表达式来判断其后面的参数是否为纯数字。
对于参数-e,-a,采用判断文件路径是否存在的方式来防止出现读取错误。
实现方式比较简单。
随机表达式生成模块
因为算法解释起来太长了,于是写了另外一篇博客,用来解释我们随机表达式生成模块的运行。这是一个最让人疯狂的模块好吧,花了我们两天的时间把查重的各个方面都考虑了一遍。
关于四则运算的一些想法 - onezhan - 博客园 (cnblogs.com)
大概就是利用表达式树进行表达式的生成,并利用算法规定好连续的乘法或加法下,数据序列的唯一。
接下来解释一下 Expression
应该是什么样的构成。我们的算法将利用类继承的多态性
也就是说,一棵表达式树,会有很多的结点,这些结点都是 Expression
的子类
表达式中数据的单元 Fraction
分数类,不论是整数还是分数,在本项目中全看做分数
Fraction
类负责管理实数的加减乘除。
这个类将提供一个方法,会根据分数对象自身的数据情况,选择输出分数或整数。不用担心分数会输出成假分数,因为这个方法会保证不管分数是真分数还是假分数,都输出真分数。
有了这个类,我们就可以从分数等数据运算中解脱出来,只用关心:怎么样搭建一棵数学意义和计算机意义一样的表达式树。
Add()
、Sub()
、Mul()
、Div()
这三个方法帮助我们进行分数的运算,外界不用关心他们是否需要简化分数,类内会帮外界完成这些琐碎的事。
Great()
、Equal()
提供了分数之间比较的方法
由于生成表达式的过程中,可能会出现除法的分母表达式结果为零的情况,所以提供了判断这个分数对象是不是零的方法 isZero()
ToString()
会将将根据自身的数据状况,输出符合需求的数值(可能是整数、也可能是真分数)
基类 Expression
属性:
- left(Expression):用于存储左表达式子树
- right(Expression):用于存储右表达式子树
- maxLeaf(Fraction):本表达式树的最大叶子节点,这个东西算法需要
- result(Fraction):这棵表达式树的结果,是一个分数
- opType(枚举值):提供一个判断结点类型的依据,这是算法所需要的
方法:
- getResult():String -> 返回一个结果的字符串形式,实际上就是调用了result分数对象的
ToString()
- getExp():String -> 将左子树和右子树的表达式用 “operator” 按一定的算法拼接。本方法设定上是虚方法,需要子类的具体实现
方法我们只介绍重要的,其他的都是外界获取对象内部数据的一种方法,没什么好讲的。
在 Expression
的子类实现中,我们的打算很简单,重写类的构造函数和getExp()
就可以满足项目的需求了
下面我们只挑出 SubExpression
这个比较简单的来介绍,更复杂的其他类,请了解我们上面给出的文章。
SubExpression类
构造函数
SubExpression(Expression left, Expression right);
对该类的构造函数进行如下规范:在传入的left,right中,选择一棵result大的表达式树作为左子树。
这样就做的话就能保证用 -
号连接在一起的表达式,一定是正数。
getExp()函数
取得左子树的表达式 leftExpression.getExp()
并根据我们的算法决定加括号还是不加括号
右子树在某些情况下必须加括号。
输入输出模块
答案的读取:
文本读取采用UTF_8格式。
采用正则表达式读取作答的答案与原题目答案进行对比,将结果输出到一个ArrayList里,再交由其他模块进行结果的写入。
写入文件时均采用覆写方式。
最终软件实现的效果
对原来的乘法符号,和加法符号进行了修改,并且调整的一下表达式符号之间的间隙。
同优先级下实现了括号的随机加入。
能在一定程度上对表达式进行数学意义上的查重。
但我们项目的表达式生成功能在r
过小,n
过大时候会有问题:这样可能会触及排列组合的极限,导致我们程序的生成表达式循环一直到达不了用户的要求。
(也就是说,一直在随机生成已经有了的表达式,表达式的数量增长不了,满足不了用户的要求,因此先入死循环)
这个极限大概是这样的:
r=2
时,约为 960
r=3
时,约为 4255
r=4
时,超过了 12000
(后面增长一个表达式所要时间非常长,所以就选择了一个算法能到达的值)
这就是查重要付出的代价,但对于试题来说是必要的,因为考试的试题中,老师是不希望有两道解题方式基本一样的题目的。
所以,当 r<4
请谨慎选择您要生成的题目数量,您当然可以选择一个非常大的 n
算法也能正常工作,但效率会极低。
因此我们会在循环超过一定次数后,强制结束循环。
为了简单说明,我们只用一个简单了例子说明对比答案模块的功能
其实就是将答案文件的答案提取,然后将练习文件所写答案提取出来
最后对比两个提取出来的字符串就完了
单元测试(使用JUnit4)
我们为每一个单元尽最大努力模拟了用户可能的输入
命令行解释模块
public class CommandUtilTest {
@Test
public void getParameter() {
CommandUtil c = new CommandUtil();
//正常输入参数
String args[] = { "-r","70" , "-n","80" ,"-a","-el","-e","-xp"};
c.getParameter(args);
System.out.println("A:"+c.getA());
System.out.println("E:"+c.getE());
System.out.println("R:"+c.getR());
System.out.println("N:"+c.getN());
System.out.println("-----------");
//缺少不必要参数
CommandUtil c1 = new CommandUtil();
String args1[] = {"-n","80","-r","70","-e","-xp"};
c1.getParameter(args1);
System.out.println("A:"+c1.getA());
System.out.println("E:"+c1.getE());
System.out.println("R:"+c1.getR());
System.out.println("N:"+c1.getN());
System.out.println("-----------");
//缺少必要参数
CommandUtil c2 = new CommandUtil();
String args2[] = {"-n","80","-e","-xp"};
c2.getParameter(args2);
System.out.println("A:"+c2.getA());
System.out.println("E:"+c2.getE());
System.out.println("R:"+c2.getR());
System.out.println("N:"+c2.getN());
System.out.println("-----------");
//参数缺少
CommandUtil c4 = new CommandUtil();
String args4[] = { "-r", "-n" , "-a","D:\\Users\\Desktop\\exp.txt" , "-e","-xp" };
c4.getParameter(args4);
System.out.println("A:"+c4.getA());
System.out.println("E:"+c4.getE());
System.out.println("R:"+c4.getR());
System.out.println("N:"+c4.getN());
System.out.println("-----------");
}
}
随机表达式生成模块
public class RandomExpressionTest {
@Test
public void ToStringTest() {
// 测试整数
Fraction f1 = new Fraction(2, 1);
System.out.println(f1.ToString());
// 测试真分数
Fraction f2 = new Fraction(1, 6);
System.out.println(f2.ToString());
// 测试假分数
Fraction f3 = new Fraction(16, 6);
System.out.println(f3.ToString());
// 分子和分母相等
Fraction f4 = new Fraction(3, 3);
System.out.println(f4.ToString());
// 测试负数
Fraction f5 = new Fraction(-1, 3);
System.out.println(f5.ToString());
Fraction f6 = new Fraction(1, -3);
System.out.println(f6.ToString());
Fraction f7 = new Fraction(3, -2);
System.out.println(f7.ToString());
// 测试分母为零
try {
Fraction zero = new Fraction(1, 0);
}
catch (Exception e) {
System.out.println(e.toString());
}
// 对分数加法的测试
// 正数的加法
Fraction a1 = new Fraction(2, 1);
Fraction a2 = new Fraction(1, 4);
System.out.println(a1.Add(a2).ToString());
// 存在负数的加减法
a1 = new Fraction(-1, 2);
a2 = new Fraction(2, 1);
System.out.println(a1.Add(a2).ToString());
// 对分数的减法测试
a1 = new Fraction(1, 4);
a2 = new Fraction(2, 1);
System.out.println(a1.Sub(a2).ToString());
System.out.println(a2.Sub(a1).ToString());
// 对分数的乘法进行测试
a1 = new Fraction(3, 4);
a2 = new Fraction(-4, 3);
System.out.println(a1.Mul(a2).ToString());
a2 = new Fraction(4, 3);
System.out.println(a1.Mul(a2).ToString());
// 对分数的除法进行测试
a1 = new Fraction(3, 4);
a2 = new Fraction(2, 5);
System.out.println(a1.Div(a2).ToString());
a1 = new Fraction(2, -5);
System.out.println(a1.Div(a2).ToString());
}
@Test
public void CreateExpressionTest() {
CreateExpression a = new CreateExpression(30, 2);
Map<String, String> tmp = a.getExpressionAndResult();
for(String exp : tmp.keySet()) {
System.out.println(exp);
}
}
}
输入输出模块
public class ReadUtilTest {
@Test
public void countLine() {
System.out.println("line:"+ ReadUtil.CountLine("D:\\Users\\Desktop\\exp.txt"));
}
@Test
public void getResult() {
System.out.println(ReadUtil.GetResult("D:\\Users\\Desktop\\exp.txt",3,"."));
System.out.println(ReadUtil.GetResult("D:\\Users\\Desktop\\exp.txt",5,"="));
}
@Test
public void ResultCompare(){
ArrayList<Integer>[] GetCompareResult = ReadUtil.ResultCompare("D:\\Users\\Desktop\\result.txt","D:\\Users\\Desktop\\result2.txt");
ArrayList<Integer>Correct = GetCompareResult[0];
ArrayList<Integer>Wrong = GetCompareResult[1];
System.out.print("Correct:");
for (int num:Correct){
System.out.print(num+" ");
}
System.out.println();
System.out.print("Wrong:");
for(int n:Wrong){
System.out.print(n+" ");
}
}
@Test
public void writeExpression() {
CreateExpression a = new CreateExpression(10,10);
Map<String, String> tmp = a.getExpressionAndResult();
WriteUtil.WriteExpression(tmp,"D:\\Users\\Desktop\\exp.txt","D:\\Users\\Desktop\\result.txt");
}
@Test
public void CompareResult(){
WriteUtil.WriteCompareResult(ReadUtil.ResultCompare("D:\\Users\\Desktop\\result.txt","D:\\Users\\Desktop\\result2.txt"),"D:\\Users\\Desktop\\Compareresult.txt");
}
}
性能测试
给了这么一串参数,性能测试的结果如下:
String[] args0 = {"-n", "100000000", "-r", "1000"};
耗费最多的就是 Fraction
,Expression
及其子类,这是生成大量表达式以及查重模块所带来的代价。当生成的题目数量越多时,Expression
生成的表达式树就会越多,为了进行查重,存起来的表达式树就会越来越多,因此当数据规模足够大时,该算法对内存的消耗量也是非常大的。
但考虑到我们只要求一万道题目的数量,而我们所做测试是一千万道题目的数量,所以可以想象大概是本次性能测试的所有资源消耗量,都几乎会下降一千倍,那这样的性能消耗,对于我们来说就是可以接受的。
总结
张永祥
对本次项目进行总结:
(1)项目的成功之处:
-
高效沟通:在结对编程中,我们保持了高效的沟通,及时解决了编程过程中遇到的问题。
-
互相学习:通过互相监督,我们互相学习了对方的编程技巧和思维方式,提高了自身的编程水平
(2)不足之处:
- 没有足够严格地按照结对编程的方法解决问题
- 因为前期没有定义好相关规范、任务,导致开发过程中有种撕裂感,有种在完成单人项目的感觉。
(3)经验分享
- 在团队中,合理的系统设计文档,能有效组织人员进行开发,让每个人的能力得到充分利用。
黄俊杰
对本次项目进行总结:
(1)项目的成功之处:
- 高效沟通:在结对编程中,我们保持了高效的沟通,及时解决了编程过程中遇到的问题。
- 互相学习:通过互相监督,我们互相学习了对方的编程技巧和思维方式,提高了自身的编程水平
- 使用新方式:第一次使用git进行多人项目的管理,代码版本管理良好
(2)不足之处:
- 没有足够严格地按照结对编程的方法解决问题
- 对git的使用方法不熟练,个人代码版本更新有可能因为交流滞后,造成代码冲突,在提交和merge时出现问题
- 我比较像摸鱼,对算法设计基本没有贡献
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南