结对项目
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 合作结对实现一个自动生成小学四则运算题目的命令行程序 |
一、项目信息
| 队员 | 学号 | github仓库 |
|---|---|---|
| 程源 | 3124004203 | https://github.com/dinnernumber1/3124004203/homework3 |
| 李彦赋 | 3124004211 | https://github.com/Alittlenoob693/3124004211 |
开发语言:java
版本:v1.0
二、开发耗时预估
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| ・Planning | ・计划 | 10 |
| ・Estimate | ・估计这个任务需要多少时间 | 10 |
| ・Development | ・开发 | 20 |
| ・Analysis | ・需求分析 (包括学习新技术) | 10 |
| ・Design Spec | ・生成设计文档 | 10 |
| ・Design Review | ・设计复审 (和同事审核设计文档) | 10 |
| ・Coding Standard | ・代码规范 (为目前的开发制定合适的规范) | 10 |
| ・Design | ・具体设计 | 10 |
| ・Coding | ・具体编码 | 30 |
| ・Code Review | ・代码复审 | 10 |
| ・Test | ・测试(自我测试,修改代码,提交修改) | 10 |
| ・Reporting | ・报告 | 20 |
| ・Test Report | ・测试报告 | 10 |
| ・Size Measurement | ・计算工作量 | 10 |
| ・Postmortem & Process Improvement Plan | ・事后总结,并提出过程改进计划 | 10 |
| 合计 | 190 |
三、效能分析
-
改进思路与花费时间
我们在项目初期实现了基本功能,但在测试生成10000道题目时,程序耗时长达数分钟,且判卷模块频繁出现“误判”现象。我们总共花费了约 3.5小时 进行性能分析和重构优化。
第一版问题:
判卷模块(最大败笔):最初为了省事,使用正则表达式将带分数 2'3/8 替换为 (2+3/8) 再计算。这导致正则解析开销巨大,且一旦遇到复杂的括号嵌套,替换逻辑就会破坏原有运算优先级,导致正确答案被误判为错误。
查重逻辑:使用简单的字符串拼接存入 Set,导致 1+2+3 和 3+(2+1) 被识别为不同题目,反复生成直到超时。
除法与减法约束:采用“先盲目生成,再检查丢弃”的策略,导致无效递归过多,产生大量废弃的 ExpressionNode 对象,引发频繁的 GC(垃圾回收)。 -
改进方案与成效
判卷解析器重写:彻底废弃正则替换,手写了一个轻量级的“递归下降解析器”,原生支持 ' 字符。它直接读取带分数并转换为 Fraction 对象,极大提升了判卷速度和准确率(现在自己判自己必然满分)。
去重算法优化:在 ExpressionNode 中实现 getCanonicalString。对于 + 和 *,递归展平子树的所有操作数,排序后再拼接成规范字符串。这完美解决了等价表达式的查重问题。
约束前置:在构建内部节点时(generateNode),直接计算左右子树的值,一旦发现 左 < 右(减法负数)或 除法结果为整数,立即抛出异常并回退,避免生成无效树。 -
性能分析图与最大消耗函数
![image]()
消耗最大的函数是:Generator.generateExpression()
原因:由于题目严格要求“减法不产生负数、除法必须是真分数(非整数)”,当数值范围 -r 设置得较小时(如 -r 10),随机生成合法表达式的概率极低,导致 while 循环重试次数指数级上升。
四、设计实现过程
1. 代码组织结构
项目共包含 4 个核心类,均组织在 Myapp.java 中(为方便提交),逻辑高度解耦:
Fraction(分数类):底层数据支撑。包含分子、分母,负责分数的加减乘除、约分、比较,以及重写 toString() 实现真分数和带分数(2'3/8)的格式化输出。
ExpressionNode(表达式树节点类):核心数据结构。分为叶子节点(数值)和内部节点(运算符)。包含 evaluate()(递归求值)、toExpressionString()(中缀表达式生成,智能添加括号)和 getCanonicalString()(查重标准化)。
Generator(题目生成器):负责业务逻辑。包含随机数/分数生成(randomFraction),以及递归生成表达式树(generateNode)。同时维护一个 Set
Myapp(主类):负责命令行参数解析(-n, -r, -e, -a)、文件读写(Exercises.txt, Answers.txt, Grade.txt)以及判卷逻辑(包含独立手写的 parseExpression 解析器)。
2. 函数关系与流程图
Myapp 解析参数后,若为生成模式,则实例化 Generator。Generator 递归构建 ExpressionNode 树。树构建完成后,调用 evaluate() 计算 Fraction 结果,最后 Myapp 负责将表达式字符串和答案写入文件。
若为判卷模式,Myapp 读取文件,将题目字符串交给 parseExpression 解析成 Fraction,然后与答案文件中的 Fraction 进行 equals 比对。
关键函数流程图:

五、代码说明
1. 核心去重逻辑(ExpressionNode.getCanonicalString)
思路:题目要求 1+2+3、3+2+1、3+(2+1) 视为同一题。我们利用加法和乘法的交换律与结合律,将 + 和 * 的操作数全部提取、排序后重组。
// 核心代码节选
private String canonicalize(ExpressionNode node) {
if (node.isLeaf) return node.value.toString();
if (node.operator == '+' || node.operator == '*') {
List
collectOperands(node, node.operator, operands); // 递归展平
Collections.sort(operands); // 字典序排序
return "(" + String.join(" " + node.operator + " ", operands) + ")";
} else {
// 减法和除法不满足交换律,保持原样
return "(" + canonicalize(node.left) + " " + node.operator + " " + canonicalize(node.right) + ")";
}
}
2. 判卷模块的带分数解析(Myapp.parseExpression)
// 核心代码节选
Fraction parseFactor() {
if (eat('(')) { Fraction x = parseExpression(); eat(')'); return x; }
int startPos = this.pos;
if (ch >= '0' && ch <= '9') {
while (ch >= '0' && ch <= '9') nextChar();
// 关键:处理带分数 2'3/8
if (ch == ''') {
int intPart = Integer.parseInt(cleanExpr.substring(startPos, this.pos));
nextChar(); // 跳过 '
int fracStart = this.pos;
while (ch >= '0' && ch <= '9' || ch == '/') nextChar();
String[] parts = cleanExpr.substring(fracStart, this.pos).split("/");
Fraction fraction = new Fraction(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
return new Fraction(intPart, 1).add(fraction); // 整数部分与分数部分相加
}
// ... (处理普通分数和纯整数)
}
}
六、测试运行
(1)无参数运行:java Myapp -> 结果:正确输出帮助信息,提示需要 -r 参数。

(2)缺少 -r 参数:java Myapp -n 10 -> 结果:报错“必须给定 -r 参数控制数值范围”。

(3)正常生成:java Myapp -n 10 -r 10

(4)极限范围测试:java Myapp -n 10000 -r 10

(5)较大范围:java Myapp -n 50 -r 100

(6)自判卷测试:java Myapp -e Exercises.txt -a Answers.txt


(7)错误答案测试:

(8)带分数判卷测试:
无问题
(9)文件格式验证:

(10)真分数格式验证:

(11)除零保护测试:
无问题
七、实际开发时间
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| ・Planning | ・计划 | 10 |
| ・Estimate | ・估计这个任务需要多少时间 | 10 |
| ・Development | ・开发 | 20 |
| ・Analysis | ・需求分析 (包括学习新技术) | 10 |
| ・Design Spec | ・生成设计文档 | 10 |
| ・Design Review | ・设计复审 (和同事审核设计文档) | 10 |
| ・Coding Standard | ・代码规范 (为目前的开发制定合适的规范) | 10 |
| ・Design | ・具体设计 | 10 |
| ・Coding | ・具体编码 | 40 |
| ・Code Review | ・代码复审 | 10 |
| ・Test | ・测试(自我测试,修改代码,提交修改) | 20 |
| ・Reporting | ・报告 | 40 |
| ・Test Report | ・测试报告 | 20 |
| ・Size Measurement | ・计算工作量 | 10 |
| ・Postmortem & Process Improvement Plan | ・事后总结,并提出过程改进计划 | 10 |
| 合计 | 240 |
八、项目小结
1. 成败得失与经验教训
成功之处:我们完美实现了所有需求,特别是复杂的三层运算符嵌套、括号的智能生成、以及高难度的等价表达式去重(1+2+3 等价于 3+(2+1))。
失败与教训:项目初期贪图方便,在判卷模块使用了“正则表达式替换带分数”的捷径,导致后期出现严重的误判 Bug。我们花了大量时间排查,最终重写了解析器才解决。这告诉我们:在涉及复杂逻辑时,不要试图用正则表达式走捷径,老老实实写词法/语法解析器才是最稳健的方案。
2. 结对感受与闪光点
结对感受:结对编程让我们体会到了“一个人写代码容易钻牛角尖,两个人讨论能快速找到破局点”。我们在遇到除法真分数约束导致死循环时,一人提出思路,一人负责实现和测试,效率远高于单打独斗。通过 Git 进行版本控制,我们学会了如何解决代码冲突。
队友闪光点:队友在解决去重问题时提出了“展平+排序”的 getCanonicalString 方案,极具创造力,直接优雅地解决了最复杂的查重难点。另外,队友在代码规范上非常严谨,坚持对每个核心方法写 Javadoc 注释,极大地提高了代码的可读性。
对彼此的建议:建议队友在未来的项目中能更早地编写单元测试(如 JUnit),这样我们在重构判卷解析器时,就能更有底气,而不是每次都手动改答案去测试。同时,我也需要向队友学习,在编码前多画流程图,理清思路再动手。

浙公网安备 33010602011771号