结对项目

这个作业属于哪个课程 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

三、效能分析

  1. 改进思路与花费时间
    我们在项目初期实现了基本功能,但在测试生成10000道题目时,程序耗时长达数分钟,且判卷模块频繁出现“误判”现象。我们总共花费了约 3.5小时 进行性能分析和重构优化。
    第一版问题:
    判卷模块(最大败笔):最初为了省事,使用正则表达式将带分数 2'3/8 替换为 (2+3/8) 再计算。这导致正则解析开销巨大,且一旦遇到复杂的括号嵌套,替换逻辑就会破坏原有运算优先级,导致正确答案被误判为错误。
    查重逻辑:使用简单的字符串拼接存入 Set,导致 1+2+3 和 3+(2+1) 被识别为不同题目,反复生成直到超时。
    除法与减法约束:采用“先盲目生成,再检查丢弃”的策略,导致无效递归过多,产生大量废弃的 ExpressionNode 对象,引发频繁的 GC(垃圾回收)。

  2. 改进方案与成效
    判卷解析器重写:彻底废弃正则替换,手写了一个轻量级的“递归下降解析器”,原生支持 ' 字符。它直接读取带分数并转换为 Fraction 对象,极大提升了判卷速度和准确率(现在自己判自己必然满分)。
    去重算法优化:在 ExpressionNode 中实现 getCanonicalString。对于 + 和 *,递归展平子树的所有操作数,排序后再拼接成规范字符串。这完美解决了等价表达式的查重问题。
    约束前置:在构建内部节点时(generateNode),直接计算左右子树的值,一旦发现 左 < 右(减法负数)或 除法结果为整数,立即抛出异常并回退,避免生成无效树。

  3. 性能分析图与最大消耗函数
    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 比对。
关键函数流程图
image

五、代码说明

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 operands = new ArrayList<>();
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 参数。
image
(2)缺少 -r 参数:java Myapp -n 10 -> 结果:报错“必须给定 -r 参数控制数值范围”。
image
(3)正常生成:java Myapp -n 10 -r 10
image
(4)极限范围测试:java Myapp -n 10000 -r 10
image
(5)较大范围:java Myapp -n 50 -r 100
image
(6)自判卷测试:java Myapp -e Exercises.txt -a Answers.txt
image
image
(7)错误答案测试:
image
(8)带分数判卷测试:
无问题
(9)文件格式验证:
image
(10)真分数格式验证:
image
(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),这样我们在重构判卷解析器时,就能更有底气,而不是每次都手动改答案去测试。同时,我也需要向队友学习,在编码前多画流程图,理清思路再动手。

posted @ 2026-09-20 20:19  JJ123456789  阅读(10)  评论(0)    收藏  举报