结对项目(林鸿永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小时进行性能改进。
改进思路:
- 查重数据结构优化:最初使用
std::set(基于红黑树)查重,时间复杂度为O(logN)。改进后换成了std::unordered_set(基于哈希表),将查重时间复杂度降至平均O(1)。 - 内存管理改进(核心优化):最初在
ExprNode中手动写freeTree()递归释放,导致双重释放(Double Free),程序经常崩溃。后来利用 RAII 机制,直接在~ExprNode()析构函数中递归释放左右子节点,只使用delete root;,不仅消灭了内存泄漏,还大大提高了运行稳定性。 - 整数溢出修复:将
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都是一次互相学习的机会。

浙公网安备 33010602011771号