结对项目(刘文俊,林昕旸)

这个作业属于哪个课程 软件工程
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703
这个作业的目标 掌握结对开发的流程

结对成员:
刘文俊 3124004480 Github地址:https://github.com/liuwenjun9/liuwenjun9/tree/master
林昕旸 3124005359 Github地址:https://github.com/1112Chy/Doubleproject

PSP表格:

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 30
· Estimate · 估计这个任务需要多少时间 20 30
Development 开发 425 470
· Analysis · 需求分析 (包括学习新技术) 45 60
· Design Spec · 生成设计文档 40 40
· Design Review · 设计复审 (和同事审核设计文档) 30 25
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 25
· Design · 具体设计 80 75
· Coding · 具体编码 150 160
· Code Review · 代码复审 30 45
· Test · 测试(自我测试,修改代码,提交修改) 30 40
Reporting 报告 100 125
· Test Report · 测试报告 30 40
· Size Measurement · 计算工作量 30 35
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 40 50
合计 545 625

效能分析:

我们在改进程序上花费了45分钟。

改进前的问题及改进方案:

1

每一步加减乘除内部反复调用gcd,四则运算内部重复求最大公约数。

后来我们让所有四则运算返回new Fraction(a,b),只在构造函数执行一次gcd做约分,把gcd调用控制在每次对象新建时仅执行一次。

2

遇到减法负数就丢弃整棵树,会大量丢弃已经递归创建好的ExpressionNode对象,tryBuildTree()调用次数会暴涨几十倍,运行的速度非常的慢。

我们尝试在tryBuildTree()遇到减法负数时不直接丢弃已经递归构建好的左右子树,而是直接交换左右节点,保证减法结果非负,直接复用两棵子树,降低tryBuildTree()的调用总次数,减少大量无效对象的创建与回收。

3

每生成一题,循环遍历全部历史题目List对比字符串,题目越多查重越慢。

我们后来改成使用HashSet做查重,不用暴力遍历列表,减短查重时间。

改进后的性能分析图:

PixPin_2026-09-21_23-34-06

消耗最大的函数:

// 生成规范化字符串,加法和乘法交换左右子树用于查重
public String toCanonicalString() {
if (isLeaf()) {
return value.toString();
}
String leftCanon = left.toCanonicalString();
String rightCanon = right.toCanonicalString();
// 满足交换律时按字典序统一排序
if (op.commutative) {
if (leftCanon.compareTo(rightCanon) > 0) {
String tmp = leftCanon;
leftCanon = rightCanon;
rightCanon = tmp;
}
}
return "(" + leftCanon + " " + op.symbol + " " + rightCanon + ")";
}

设计实现过程:

项目一共有五个Java类

首先是Main类,它主要是作为程序的入口,完成解析命令行参数、区分生成题目和批改作业两种运行模式和完成文件读写。

ExpressionGenerator类完成随机生成批量的不重复四则表达式、递归构建表达式树和做业务约束校验、题目去重的任务。

ExpressionNode类用来表达式二叉树节点、运算符枚举、保存子节点与运算结果和输出中缀表达式、规范化查重字符串。

Fraction类实现分数模型、分数的四则运算、约分、比较、字符串解析与格式化的输出。

Evaluator类的职责是双栈法解析算式字符串、计算表达式的结果、批改作业。

类之间的协作:

程序从Main启动,生成题目模式,调用ExpressionGenerator.generate(),递归创建大量ExpressionNode表达式树节点,节点内部保存Fraction作为运算结果,生成完毕后把表达式输出写入文本文件。

批改作业模式时通过Main读取题目文件,将题目字符串交给Evaluator.evaluate(),Evaluator解析字符串得到Fraction标准答案,与学生答案对比,输出批改结果文件。

Fraction被其余所有类调用,统一完成分数的计算、约分和格式化。

调用流程图:

PixPin_2026-09-21_23-36-26

代码说明:

1

Fraction 分数模型核心构造与运算:

public class Fraction implements Comparable {
private final long numerator; //分子
private final long denominator; //分母,恒保证>0

public Fraction(long num, long den) {
if (den == 0) throw new ArithmeticException("分母不能为 0");
//把负号统一放到分子,分母永远为正数
if(den < 0){
num = -num;
den = -den;
}
long g = gcd(Math.abs(num), den);
this.numerator = num / g;
this.denominator = den / g;
}
//辗转相除法求最大公约数,用于分数约分
private static long gcd(long a, long b) {
while (b != 0) {
long temp = b;
b = a % b;
a = temp;
}
return a;
}
//除法运算,抛出除数为0异常
public Fraction divide(Fraction o) {
if (o.numerator == 0) throw new ArithmeticException("除数不能为 0");
return new Fraction(this.numerator * o.denominator, this.denominator * o.numerator);
}
//判断严格真分数:0 < 分子 < 分母,生成除法题目的约束条件
public boolean isProperFraction() {
return numerator > 0 && numerator < denominator;
}
}
分数的所有运算结果全部通过构造函数完成一次性约分,不在四则方法内部重复求最大公约数,统一将负号置于分子,分母恒大于 0,简化后续比较、判断逻辑,isProperFraction()用于约束除法题目结果必须为真分数。

2

tryBuildTree 递归生成表达式树:

//递归生成一棵子表达式树;opCount:本棵子树还需要多少个运算符
private ExpressionNode tryBuildTree(int opCount) {
//递归终止条件:运算符数量为0,生成叶子数字节点
if (opCount == 0) {
return new ExpressionNode(randomFraction());
}
//将运算符数量拆分给左右两棵子树
int leftOps = random.nextInt(opCount);
int rightOps = opCount - 1 - leftOps;
ExpressionNode left = tryBuildTree(leftOps);
if (left == null) return null;
ExpressionNode right = tryBuildTree(rightOps);
if (right == null) return null;

ExpressionNode.Op[] ops = ExpressionNode.Op.values();
ExpressionNode.Op op = ops[random.nextInt(ops.length)];

//约束1:减法不能产生负数,交换左右子树,复用已生成节点,不直接丢弃
if (op == ExpressionNode.Op.SUB) {
if (left.getResult().compareTo(right.getResult()) < 0) {
ExpressionNode tmp = left;
left = right;
right = tmp;
}
}
//约束2:除法结果必须是严格真分数,不满足直接返回null舍弃该组合
if (op == ExpressionNode.Op.DIV) {
if (right.getResult().getNumerator() == 0) return null;
Fraction divRes;
try {
divRes = left.getResult().divide(right.getResult());
} catch (ArithmeticException e) {
return null;
}
if (!divRes.isProperFraction()) {
return null;
}
}
return new ExpressionNode(op, left, right);
}
递归构建二叉表达式树,遇到减法就通过交换左右节点复用子树,减少对象重建开销,除法无法交换,不满足真分数约束直接返回 null,上层generate()循环调用该函数,结合HashSet和toCanonicalString()完成题目的去重。

3

toCanonicalString:

// 生成规范化字符串,加法和乘法交换左右子树用于查重
public String toCanonicalString() {
if (isLeaf()) {
return value.toString();
}
String leftCanon = left.toCanonicalString();
String rightCanon = right.toCanonicalString();
// 满足交换律时按字典序统一排序
if (op.commutative) {
if (leftCanon.compareTo(rightCanon) > 0) {
String tmp = leftCanon;
leftCanon = rightCanon;
rightCanon = tmp;
}
}
return "(" + leftCanon + " " + op.symbol + " " + rightCanon + ")";
}
递归遍历表达式树,针对加法、乘法交换律,将左右子表达式字典序重排,使1+2和2+1生成完全相同字符串,实现等价题目的去重。

测试运行:

PixPin_2026-09-21_22-13-07

PixPin_2026-09-21_22-08-21

单元测试ExpressionTest一共 10 个测试用例,覆盖了分数约分化简,带分数格式化输出,分母为 0 抛出异常,交换律等价表达式查重,减法不能产生负数,除法结果必须为真分数,四则运算、表达式求值优先级校验。
具体内容:
testCommutativeSim:加法乘法交换律归一查重测试。
testNonCommutativeDistinction:减法、除法不满足交换律区分。
testCommutativeComplex:复杂嵌套表达式交换律查重。
testNegativeSubtracti:减法结果非负约束测试。
testFractionSimplify:分数约分。
testMixedFractionForm:带分数格式化输出。
testEvaluatorPreceden:表达式求值运算符优先级。
testDivisionProperFraction:除法结果为真分数校验。
testFractionAdd:分数加法运算。
testZeroDenominator:分母为 0 异常捕获。

PixPin_2026-09-21_22-12-53

这展示了程序的题目生成模式,一次性生成 10000 条不重复四则运算题目写入Exercises.txt,答案写入Answers.txt中。

PixPin_2026-09-21_22-14-25

这是程序的批改模式,读取Exercises.txt题目文件和学生答案文件,调用Evaluator双栈求值模块自动批改,批改结果输出到Grade.txt中。

项目小结:

我们的项目完成了基本的要求,但还是有许都能改进的地方,像占用了大部分运行时间的toCanonicalString(),还有优化的空间。

我个人的结对感受:

这次和林昕旸同学一起完成的结对项目让我受益匪浅,林昕旸同学有很强的规划能力,制定了详细的规划和作业步骤,也很细心,经常进行代码复审,纠正那些有疏漏或是性能差的地方,结对作业期间我从林昕旸同学那里学到了很多软件开发的知识和能力,我很感谢他。

和刘文俊同学结对非常顺畅。他的动手能力很强,编码节奏快,尤其在写测试用例时考虑得很周全,帮我们提前排查掉了不少边界异常。遇到环境配置报错和性能调优难点时他也特别有耐性,两个人随时讨论、协同排错的效率挺高的。非常感谢他的配合,这次合作很愉快。

posted @ 2026-09-22 21:00  liuwenjun0  阅读(12)  评论(0)    收藏  举报