结对项目 - 小学四则运算题目生成器
结对项目 - 小学四则运算题目生成器
项目成员:罗晟 3124004103、梁译航 3124004101
GitHub 地址:https://github.com/3124004103ls/3124004103ls
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 45 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 45 |
| Development | 开发 | 475 | 540 |
| · Analysis | · 需求分析(包括学习新技术) | 45 | 60 |
| · Design Spec | · 生成设计文档 | 30 | 45 |
| · Design Review | · 设计复审 | 20 | 30 |
| · Coding Standard | · 代码规范 | 20 | 30 |
| · Design | · 具体设计 | 60 | 90 |
| · Coding | · 具体编码 | 180 | 120 |
| · Code Review | · 代码复审 | 30 | 45 |
| · Test | · 测试 | 90 | 120 |
| Reporting | 报告 | 80 | 120 |
| · Test Report | · 测试报告 | 30 | 45 |
| · Size Measurement | · 计算工作量 | 20 | 30 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 30 | 45 |
| 合计 | 585 | 705 |
二、效能分析
1. 性能测试结果
用 PerformanceTest 测试 10000 道题的生成、计算、规范化耗时:
| 测试 | 10000 道题耗时 |
|---|---|
| 生成 | 16 ms |
| 计算 | 2 ms |
| 规范化 | 5 ms |
结论:性能瓶颈在生成环节,因为生成时要反复构建表达式树、检查合法性、去重。
2. 性能改进
改进点:ExpressionGenerator.generate() 里,把合法性检查和重复检查合并成一次 if,减少 continue 次数:
// 改进前:两个 continue
if (!isValid(expr)) continue;
if (seen.contains(key)) continue;
// 改进后:一个 continue
if (!isValid(expr) || seen.contains(key)) continue;
改进效果:生成 10000 道题耗时稳定在 16ms 左右。
三、设计实现过程
1. 代码组织
| 类 | 职责 |
|---|---|
| Main | 程序入口,解析命令行参数 |
| Fraction | 分数类,支持加减乘除、化简、比较 |
| Expression | 表达式树节点 |
| ExpressionGenerator | 题目生成器 |
| ExpressionNormalizer | 去重 |
| ExpressionParser | 字符串转表达式树 |
| ExpressionPrinter | 表达式树转字符串 |
| FileIO | 文件读写 |
| Grader | 判分器 |
2. 类之间的关系
Main
├── ExpressionGenerator
│ ├── Expression
│ ├── Fraction
│ └── ExpressionNormalizer
├── FileIO
│ ├── ExpressionPrinter
│ └── Expression
└── Grader
├── ExpressionParser
├── Expression
└── Fraction
3. 关键算法
题目生成:
- 随机决定运算符个数(1-3 个)
- 递归构建表达式树
- 检查合法性:
- 减法:左 ≥ 右(不能出现负数)
- 除法:除数 ≠ 0,结果必须是真分数
- 乘法:两个数都不能是 0
- 检查重复:用 ExpressionNormalizer 规范化后查 HashSet
- 合法且不重复则返回,否则重新生成
去重算法:
- 加法、乘法满足交换律和结合律:a + b 和 b + a 视为相同,a + (b + c) 和 (a + b) + c 展平后排序
- 减法、除法不满足交换律,顺序固定
- 用规范化后的字符串作为唯一标识
四、代码说明
1. Fraction 核心方法
public Fraction add(Fraction other) {
int num = this.numerator * other.denominator + other.numerator * this.denominator;
int den = this.denominator * other.denominator;
return new Fraction(num, den);
}
分数加法:通分后相加,构造函数里自动化简。
2. ExpressionGenerator 合法性检查
private boolean isValid(Expression expr) {
if (expr.isNumber()) return true;
if (!isValid(expr.getLeft()) || !isValid(expr.getRight())) return false;
Fraction l = expr.getLeft().evaluate();
Fraction r = expr.getRight().evaluate();
switch (expr.getType()) {
case MULTIPLY:
if (l.getNumerator() == 0 || r.getNumerator() == 0) return false;
break;
case DIVIDE:
if (r.getNumerator() == 0) return false;
if (l.getNumerator() == 0) return false;
Fraction result = l.divide(r);
if (result.getNumerator() >= result.getDenominator()) return false;
break;
case SUBTRACT:
if (l.compareTo(r) < 0) return false;
break;
default:
break;
}
return true;
}
3. ExpressionNormalizer 去重
private static String normalizeAssociative(String op, String left, String right) {
List<String> operands = new ArrayList<>();
collectOperands(op, left, operands);
collectOperands(op, right, operands);
operands.sort(String::compareTo);
return "(" + String.join(op, operands) + ")";
}
把加法和乘法的操作数收集起来,排序后拼接,保证 23 + 45 和 45 + 23 规范化后相同。
五、测试运行
1. 测试用例
| 测试类 | 测试数 | 内容 |
|---|---|---|
| FractionTest | 8 | 加减乘除、化简、toString、compareTo |
| ExpressionGeneratorTest | 3 | 生成、不重复、无负数 |
| GraderTest | 2 | 全部正确、部分正确 |
| PerformanceTest | 3 | 生成、计算、规范化性能 |
| 合计 | 16 |
2. 测试结果
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
3. 测试说明
- FractionTest:验证分数运算的正确性,覆盖化简、带分数转换等边界情况
- ExpressionGeneratorTest:验证生成的题目不重复、无负数、合法性
- GraderTest:验证判分器能正确区分对错
- PerformanceTest:验证性能满足 10000 道题的要求
六、Code Quality Analysis
使用 SonarQube for IDE 对项目做代码质量分析。
主要修复:
| 规则 | 说明 |
|---|---|
| S106 | System.out 改用 Logger |
| S1118 | 工具类加私有构造函数 |
| S2629 | LOGGER.info 用占位符 |
| S5786 | 测试类去掉多余 public |
| S5976 | 合并类似测试为参数化 |
最终状态:No SonarQube issues to display.
七、项目小结
结对感受
合作顺利的地方:
- 两人对需求理解基本一致,算法设计阶段沟通顺畅。
- 代码复审时互相发现了几个边界问题(如 0 参与乘除法、除法结果是整数等)。
遇到的困难:
- 中文编码问题:之前设置了打开的文件自动以 GBK 形式保存,导致“×”,“÷” 以及中文显示乱码,哪怕使用Unicode转义字符都没用。在这一部分浪费了很多时间,最终没办法了,只能改用 “*”,“/” 替代,特此说明。
![b25ed9e588f7b1aafc4d5d91b896e411]()
然后“÷”改成“/”后,程序识别不了类似9 / 1/3(正常应该是9÷1/3)这个式子导致答案出错,这个问题目前我们还没解决,所以最后判断正误还有3.7%的错误率。 - 去重算法:一开始没考虑结合律,后来改用「展平 + 排序」的方式解决。
罗晟心得体会:在AI的辅助下终于艰难的完成了这个项目的大部分。由于基础薄弱,导致即便有AI帮忙写代码有几行还是不懂就像System.out在SonarQube里为什么要改成LOGGER才能消除警告,而且调试的时候出错还要在Ai的帮助下才能弄懂这里为什么会错,以及要怎么修改,甚至做到最后生成10000题后判断正误,错误率还是高达3.7%。如果一开始“×”,“÷” 如果没出错我应该能省下挺多时间的。

梁译航心得体会:通过这次结对,我意识到测试并不是简单地跑一遍程序,而是要主动去构造一些“刁钻”的输入,比如0参与乘法、除法结果是整数这些情况,才能真正发现代码里的漏洞。虽然最后判分还有3.7%的误差没有彻底解决,但整体上我们两个人配合得还不错,他负责算法和核心逻辑,我负责测试和复审,互相补充了各自的短板。
八、附录
运行示例
生成 10 道题:
java -jar main.jar -n 10 -r 10
生成 10000 道题:
java -jar main.jar -n 10000 -r 10
判分:
java -jar main.jar -e Exercises.txt -a Answers.txt
输出文件示例
Exercises.txt:
1. 1/2 * 5/7 =
2. 3/7 * 8/9 * (9 * 3) =
...
Answers.txt:
1. 1/2 * 5/7 = 5/14
2. 3/7 * 8/9 * (9 * 3) = 72/7
...
Grade.txt:
Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Wrong: 0 ()


浙公网安备 33010602011771号