第三次作业
小学四则运算题目生成器 - 结对项目总结
姓名1: 库徳来提·迪里夏提 学号1: 3124004059
姓名2: 蔡小宇 学号2: 3224004190
GitHub项目地址: https://github.com/K-D-R-T/arithmetic-exercise
一、PSP 2.1 表格
在开始实现程序之前,我们进行了需求分析并估算了各个模块的开发时间;实现完成后,记录了实际的耗时。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 20 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
| Development | 开发 | 300 | 400 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 | 30 |
| · Design Spec | · 生成设计文档 | 20 | 20 |
| · Design Review | · 设计复审 | 10 | 10 |
| · Coding Standard | · 代码规范 | 10 | 10 |
| · Design | · 具体设计 | 30 | 40 |
| · Coding | · 具体编码 | 180 | 200 |
| · Code Review | · 代码复审 | 20 | 30 |
| · Test | · 测试(自我测试,修改代码) | 30 | 40 |
| Reporting | 报告 | 30 | 30 |
| · Test Report | · 测试报告 | 10 | 10 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结 | 10 | 10 |
| 合计 | 400 | 450 |
二、效能分析
为了测试程序的性能,我们使用 -n 10000 -r 100 参数生成了一万道不重复的四则运算题目。程序采用了动态秒级计时提示,出题结束后自动覆写为最终耗时。


性能改进思路与耗时分析:
- 去重优化:初版代码使用
List.contains判重,在生成一万道题时速度非常慢。我们改用了HashSet结合表达式的规范化字符串(toCanonicalString)进行去重,时间复杂度从 O(N²) 降至 O(N),一万道题的去重时间几乎可以忽略不计。 - 防数值膨胀优化:在初版代码中,随机连续嵌套的乘除法导致最终答案出现了
177’8/15、88’1/3这种极其庞大的分数,完全不符合“小学四则运算”的场景。我们在生成器中加入了“结果过滤机制”,当运算结果超过规定范围(r * 2)时直接丢弃重试。虽然这导致生成时间从 6 秒增加到了 15 秒,但牺牲了部分性能换取了题目质量的大幅提升,是非常值得的工程权衡。 - 程序中消耗最大的函数:通过逻辑分析,程序中消耗最大的函数是
ExpressionGenerator.generate()中的重试逻辑,以及Fraction类的gcd()约分函数。由于要保证减法非负、除法结果真分数以及过滤超纲题,生成器会频繁递归试错,这是性能消耗的主要来源。
三、设计实现过程
本程序采用面向对象设计,核心为表达式树(Expression Tree)模式,各个类各司其职,通过组合关系实现了完整的功能闭环。

核心类设计说明:
Fraction类:独立封装分数的表示与加减乘除运算,自动约分,统一处理自然数、真分数、带分数的内部逻辑。Expression抽象基类:派生出NumberExpression(叶子节点,数字)和BinaryExpression(二元运算节点,包含左、右子树和运算符)。ExpressionGenerator:递归生成表达式树,并在生成过程中保证减法非负、除法结果为真分数、运算符不超过 3 个。遇到乘除法时自动缩小数值范围,防止分数膨胀。ExpressionParser:对题目文件进行解析,兼容中文右单引号’和英文单引号'。Grader:读取题目和答案文件,逐行比对,统计正误并写入Grade.txt。Main:命令行入口,负责参数解析、去重校验以及控制台动态计时。
四、代码说明
1. 分数类的核心运算(Fraction.java)
public Fraction add(Fraction other) {
return new Fraction(numerator * other.denominator + other.numerator * denominator, denominator * other.denominator);
}
public Fraction divide(Fraction other) {
if (other.numerator == 0) throw new ArithmeticException("除数不能为 0");
return new Fraction(numerator * other.denominator, denominator * other.numerator);
}
说明:利用交叉相乘进行分数运算,并在构造函数中调用 gcd 进行约分,保证所有分数以最简形式存储。
2. 题目去重逻辑(BinaryExpression.java
@Override
public String toCanonicalString() {
String leftCanon = left.toCanonicalString();
String rightCanon = right.toCanonicalString();
// 满足交换律的运算符,交换左右子树进行规范化
if (operator == '+' || operator == '×') {
if (leftCanon.compareTo(rightCanon) > 0) {
String temp = leftCanon;
leftCanon = rightCanon;
rightCanon = temp;
}
}
return "(" + leftCanon + " " + operator + " " + rightCanon + ")";
}
说明:利用二叉树的规范化,对于符合交换律的 + 和 ×,将左右子树按字典序排列,完美解决了 1+2 和 2+1 重复的问题。
3. 动态秒级计时与超纲过滤(Main.java)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(...);
scheduler.scheduleAtFixedRate(() -> {
if (finished[0]) return;
long elapsed = (System.currentTimeMillis() - startTime) / 1000;
System.out.print("\r正在出题,请稍等... 已用 " + elapsed + " 秒");
}, 1, 1, TimeUnit.SECONDS);
说明:使用后台线程每秒刷新控制台提示,并在出题结束后原地覆写耗时结果,提升了命令行工具的交互体验。同时加入了结果过滤,超过 r * 2 的题会被丢弃重试,保证题目难度符合小学标准。
五、测试运行
我们对程序进行了全面的测试,以下为部分关键测试用例(10个):
正常生成:-n 10 -r 10,正常生成,包含自然数、真分数、带分数。



边界范围:-n 5 -r 1,程序正常运行。

参数缺失:不输入 -r,程序报错并给出帮助信息。

性能测试:-n 10000 -r 100,成功生成一万道不重复题(耗时15秒)。

减数非负验证:检查生成文件,无负数结果。

除法真分数验证:检查所有除法题,结果均小于 1。


去重验证:无 1+2 和 2+1 等重复题。

带分数格式验证:输出均使用中文右单引号 ’。

判题全对:-e Exercises.txt -a Answers.txt,输出 Correct: 10 (...)。

判题纠错:改错一题后,输出 Correct: 9 (...) Wrong: 1 (6)。

正确性确信说明:以上测试覆盖了正常生成、边界情况、参数校验、性能测试、去重验证、格式验证以及判题全对和纠错。通过白盒测试和黑盒测试结合,证明了程序的健壮性和正确性。
六、项目小结
本次结对项目让我们深刻体会到了分工协作的优势。
在开发初期,我们一起讨论确定了面向对象的设计方案(表达式树 + 分数类),这为后续编码打下了坚实基础。在编码阶段,库徳来提·迪里夏提主要负责核心生成逻辑、分数类与表达式类的实现;蔡小宇主要负责解析器、判题器以及测试用例的设计。
我们遇到的最大挑战是数值膨胀问题。最初一万道题的答案里出现了极其巨大的分数,我们发现单纯限制初始范围是不够的,最终通过引入“结果大小过滤机制”和“乘除法范围压缩”成功解决。在这个过程中,我们互相 review 代码,发现了许多边界条件的漏洞(如除数为 0、括号的优先级、中文引号兼容等)。
通过这次项目,我们不仅巩固了 Java 面向对象、数据结构(二叉树、HashSet)的知识,还学会了如何用 ScheduledExecutorService 优化命令行交互。结对编程让我们明白:好的代码不是一个人写出来的,而是两个人不断碰撞、审查、优化出来的。
浙公网安备 33010602011771号