结对项目(林鸿永3124004431,唐嘉璇3124004437)

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703
这个作业的目标 小学四则运算题目生成器的设计与实现

1. 结对项目:小学四则运算题目生成器

结对成员:(林鸿永3124004431,唐嘉璇3124004437)
GitHub 项目地址: https://github.com/pupaname/homework3

2. PSP表格(计划阶段)

在开始实现程序之前,我们预估了在程序各个模块开发上预计耗费的时间。

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

3. 效能分析

在初步实现程序后,我们进行了性能测试。在最初版本中,生成1000道题目并查重时,程序出现了卡顿甚至崩溃。我们花费了约1小时进行性能改进。

改进思路:

  1. 查重数据结构优化:最初使用 std::set(基于红黑树)查重,时间复杂度为 O(logN)。改进后换成了 std::unordered_set(基于哈希表),将查重时间复杂度降至平均 O(1)。
  2. 内存管理改进(核心优化):最初在 ExprNode 中手动写 freeTree() 递归释放,导致双重释放(Double Free),程序经常崩溃。后来利用 RAII 机制,直接在 ~ExprNode() 析构函数中递归释放左右子节点,只使用 delete root;,不仅消灭了内存泄漏,还大大提高了运行稳定性。
  3. 整数溢出修复:将 Fraction 的 num 和 den 从 int 改为 long long,防止在 -r 1000 时中间乘法溢出导致无限递归。

4. 设计实现过程

本程序采用面向对象的设计思想,主要包含以下几个类和关键函数:

  • Fraction 类(分数类):封装了分子和分母,实现了分数的加减乘除、化简(约分)、比较运算、以及格式化为带分数或真分数的字符串(toString())。构造函数中自动化简,保证查重的唯一性。
  • ExprNode 结构体(表达式树节点):采用二叉树结构表示数学表达式。叶节点存储 Fraction 操作数,非叶节点存储运算符 + - * / 及左右子树指针。析构函数自动递归释放整棵树的内存。
  • 关键函数:
    • generateExpression(int r, int maxOps):随机生成一个表达式树,运算符不超过3个。
    • normalize(ExprNode* node)(查重核心):递归遍历树,只允许应用交换律(比较左右子树字符串,小的放左边),禁止结合律(不展平,强制保留括号),生成唯一的规范字符串用于查重。
    • parseAndEval(const string& s)(批改核心):使用双栈法解析中缀表达式。支持处理带分数格式(如 2'3/8),并计算分数结果。

5. 代码说明

关键代码1:规范化查重(严格遵守“只应用交换律,不应用结合律”)

string normalize(ExprNode* node) {
    if (node->isLeaf) return node->val.toString();
    string left = normalize(node->left);
    string right = normalize(node->right);
    
    if (node->op == '+' || node->op == '*') {
        // 只应用交换律:比较字典序,保证小的在左边
        if (left > right) swap(left, right);
    }
    // 对于减法和除法,不能交换左右,必须保留原结构并加括号
    return "(" + left + string(1, node->op) + right + ")";
}

关键代码2:中缀表达式解析器(修复连续除法Bug)

// 读取数字时,严格读取一个整数或一个分数,最多只能有一个 '/'
while (j < processed.size() && isdigit(processed[j])) j++; // 读取分子
if (j < processed.size() && processed[j] == '/') { // 遇到分数
    j++;
    while (j < processed.size() && isdigit(processed[j])) j++; // 读取分母
}
string numStr = processed.substr(i, j - i);
// 随后使用双栈法进行优先级比较与计算...

6. 测试运行

为了确保程序的正确性,我们设计了以下10个测试用例:
测试文件也放入了github

测试编号 输入(题目) 预期答案 测试目的
1 1 + 2 = 3 基础整数加法
2 1/2 + 1/3 = 5/6 真分数加法与约分
3 1/6 + 1/8 = 7/24 题目要求的特定分数运算
4 5 - 3 - 1 = 1 连续减法与左结合律
5 9 * 2/3 / 2 = 3 连续除法与分数乘法(此前测试出Bug的场景)
6 3 + 2 * 2 = 7 乘除优先级高于加减
7 ( 1 + 2 ) * 3 = 9 括号改变优先级
8 2'3/8 + 1/8 = 2'1/2 带分数与真分数混合运算
9 1 ÷ 2 = 1/2 除法结果必须是真分数
10 3 - 5 = (拒绝生成) 确保生成过程中不产生负数

正确性说明: 通过大量随机测试,我们发现了连续除法解析的漏洞,并针对性地修复了 parseAndEval 的读取逻辑,最终实现了自我生成、自我批改的 100% 正确率。

7. PSP表格(实际阶段)

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

8. 项目小结

成败得失与经验教训:
本次结对编程项目整体非常成功。我们不仅完成了基础要求,还实现了带分数支持、严格查重(区分交换律和结合律)以及批改功能。

  • 教训1:一开始忽略了 C++ 的内存管理,导致程序在生成大量题目时频繁崩溃。学会了工程中 RAII(资源获取即初始化)思想的重要性。
  • 教训2:在字符串解析方面,一开始过度依赖简单的 find 和 sscanf,导致了 9 * 2/3 / 2 被解析错误。我们明白了工程中的“脏数据”需要极其严谨的边界处理。
  • 闪光点:normalize 去重算法的设计非常巧妙,通过将二叉树递归序列化,完美地解决了“允许交换律但禁止结合律”的苛刻要求。

结对感受与闪光点分享:
两人结对编程(一人导航,一人驾驶)极大地提高了代码质量。当我陷入死胡同(比如死磕内存泄露的 bug)时,搭档能跳出思维定势,提示我检查析构函数;当搭档对解析器的正则处理感到困惑时,我提出了使用双栈加状态机的方式解决。这种1+1>2的效应在Debug阶段体现得淋漓尽致。我们建议未来的结对项目继续保持代码Review的习惯,每一次Review都是一次互相学习的机会。

posted @ 2026-09-21 17:24  pupaname  阅读(9)  评论(0)    收藏  举报