软工作业3:自动生成小学四则运算题目的命令行程序
这个作业属于哪个课程 | 计科21级1 2班 |
---|---|
这个作业要求在哪里 | 结对项目 |
这个作业的目标 | 熟悉合作开发流程 |
项目Github | 点击这里 |
团队成员 | |
---|---|
姓名 | 学号 |
石云欣 | 3221004809 |
沈纪康 | 3121004750 |
PSP表
PSP2.1 | Personal Software Process | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 10 |
Development | 开发 | 880 | 840 |
· Analysis | · 需求分析 (包括学习新技术) | 220 | 250 |
· Design Spec | · 生成设计文档 | 50 | 30 |
· Design Review | · 设计复审 | 30 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 25 |
· Design | · 具体设计 | 50 | 30 |
· Coding | · 具体编码 | 200 | 205 |
· Code Review | · 代码复审 | 80 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 150 |
Reporting | 报告 | 90 | 90 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 20 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 50 | 60 |
· 合计 | 990 | 940 |
运行环境
VisualStudio 2022
效能分析
程序中耗时最长的函数为 Generation:: IOsystem
,该函数集成了生成算式与存储至对应文件两个功能,主要性能瓶颈在于生成随机数与判断算式是否符合规范,即计算过程中不出现负数。
void Generation::IOsystem()
{
Arithmetics A;
std::ofstream outFile("exercisefile.txt");
std::ofstream outFile_answer("Answer.txt");
if (!outFile) {
std::cerr << "无法打开输出文件." << std::endl;
return;
}
if (!outFile_answer) {
std::cerr << "无法打开输出文件." << std::endl;
return;
}
for (int i = 0; i < request; i++){
std::vector<int>numList = NumGeneration(numLimit);
std::vector<char>operaList = OperatorGeneration();
std::string expression = Combination(numList, operaList);
if (expression.size()>3){
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 1);
int isParentheses = dist(gen); // 随机生成 0 或 1
if (isParentheses == 1){
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 3);
int parenthesesPosition = dist(gen) * 2;
while (parenthesesPosition > expression.size() - 2)
{
parenthesesPosition = parenthesesPosition - 2;
}
expression.insert(parenthesesPosition,1, '(');
expression.insert(parenthesesPosition + 4, 1, ')');
}
}
A.string_without_space_to_equation(expression);
std::string res = A.get_result().to_string();
if (A.get_result().numerator > 729||A.get_result().denominator>729) {
i--;
A.clear();
continue;
}
outFile << i + 1 << ". " << expression << "\n";
outFile_answer << i + 1 << ". " << res << "\n";
A.clear();
}
outFile.close();
outFile_answer.close();
}
改进方案:
- 批量生成随机数:批量生成一些随机数,然后在循环中使用这些随机数。这可以减少生成器调用的次数。
- 多线程生成:使用多线程来并行生成随机数以提高生成的效率。
- 硬件随机数生成器:计算机系统提供的硬件随机数生成器速度更快。
设计实现过程
四则运算的实现原理为使用后缀表达式对算式进行求解。
在类Arithmetic
中内置了三个函数
void string_to_equation(std::string str);
void string_with_space_to_equation(std::string str);
void string_without_space_to_equation(std::string str);
用来实现将字符串格式的算式直接转换为Arithmetic
类型,即将数字与算术符分别存入成员 std::vector<Number> nums std::vector<Operator> operators
以进行下一步操作。
- 初始化一个操作符栈
opr_stack
和一个结果栈num_stack
。 - 从左到右遍历中缀表达式的每个字符:
- 如果是操作数(数字),直接压入结果栈
num_stack
。 - 如果是操作符:
- 检查操作符栈
opr_stack
的栈顶元素,如果栈顶操作符的优先级高于或等于当前操作符的优先级,并且栈顶元素不是左括号,就将栈顶操作符弹出并压入结果栈,直到不满足条件。 - 将当前操作符压入操作符栈
opr_stack
。
- 检查操作符栈
- 如果是左括号
(
,将其压入操作符栈opr_stack
。 - 如果是右括号
)
,执行以下步骤:- 从操作符栈
opr_stack
中弹出元素并压入结果栈,直到遇到左括号(
,但不要将左括号压入结果栈。 - 弹出左括号
(
,但不压入结果栈。
- 从操作符栈
- 如果是操作数(数字),直接压入结果栈
- 遍历完成后,将操作符栈中的所有剩余操作符弹出并压入结果栈。
- 最终,结果栈
num_stack
中的元素就是后缀表达式。
计算后缀表达式的过程(以3 4 + 5 *
为例) 过程如下:
- 从左到右扫描表达式。
- 遇到算数符时,将其推入栈中。
- 遇到运算符时,从栈中弹出相应数量的操作数进行运算,然后将结果推回栈中。
- 最终,栈中的唯一元素就是表达式的结果。
在上述示例中,计算过程如下:
- 遇到
3
,将其推入栈中:栈[3]
- 遇到
4
,将其推入栈中:栈[3, 4]
- 遇到
+
,从栈中弹出两个操作数3
和4
,计算结果7
,将结果推回栈中:栈[7]
- 遇到
5
,将其推入栈中:栈[7, 5]
- 遇到
*
,从栈中弹出两个操作数7
和5
,计算结果35
,将结果推回栈中:栈[35]
最终,栈中的元素 35
就是表达式 3 4 + 5 *
的计算结果。
主函数流程图如下:
代码说明
Arithmetics::calculate
Number Arithmetics::calculate()
{
if (nums.empty()) return Number{ 0 };
auto it_num = nums.begin();
auto it_opr = operators.begin();
std::stack<Number> num_stack;
std::stack<Operator> opr_stack;
while (it_num != nums.end())
{
if (it_opr != operators.end() && it_opr->get_type() == LeftBrace)
{
num_stack.push(calculate_in_brace(it_num, ++it_opr));
}
else
{
if (it_num == nums.end()) break;
num_stack.push(*it_num);
++it_num;
}
if (it_opr == operators.end()) break;
// if the operator stack is not empty and next operator's priority is less equal than previous operator's
while (!opr_stack.empty() && it_opr->get_priority() <= opr_stack.top().get_priority())
{
Number& b = num_stack.top();
num_stack.pop();
Number& a = num_stack.top();
num_stack.pop();
num_stack.push(opr_stack.top().func(a, b));
opr_stack.pop();
}
opr_stack.push(*it_opr);
++it_opr;
}
if (num_stack.empty())
{
std::cerr << "ERROR : Empty result";
return Number{ 0, 0 };
}
while (!opr_stack.empty())
{
Number& b = num_stack.top();
num_stack.pop();
Number& a = num_stack.top();
num_stack.pop();
num_stack.push(opr_stack.top().func(a, b));
opr_stack.pop();
}
return num_stack.top();
}
这段代码的目标是实现一个数学表达式的计算器,它通过遍历表达式中的数值和运算符,使用两个栈(num_stack 和 opr_stack)来帮助我们完成计算。
代码的工作原理如下:
- 我们将数值压入 num_stack,将运算符压入 opr_stack。
- 当遇到左括号时,我们会调用 calculate_in_brace 函数来计算括号内的表达式,并将结果压入 num_stack。
- 当遇到右括号或者运算符优先级低于 opr_stack 栈顶运算符的优先级时,我们会从 num_stack 弹出两个数值和 opr_stack 弹出一个运算符,然后进行相应的运算,并将结果压入 num_stack。我们会一直重复这个过程,直到 opr_stack 为空或者遇到左括号。
- 最后,如果我们已经遍历完了 nums 和 operators,但 opr_stack 仍然不为空,那么我们会继续弹出 num_stack 的两个数值和 opr_stack 的一个运算符,进行相应的运算,并将结果压入 num_stack。这个过程会一直重复,直到 opr_stack 为空。
- 最终,num_stack 的栈顶元素就是整个表达式的计算结果,将其返回。
测试代码
控制台输入如下:
exercisefile.txt所存储的生成题目如下:
Answer.txt所存储的答案如下:
用户输入答案至answerfile.txt如下(将第十题答案修改为错误的值后):
成绩输入至Grade.txt如下:
项目小结
经验:
- 密切的合作: 每天都保持沟通,确保项目进展顺利。
- 技能互补: 拥有不同的技能和背景,我们能够相互补充,提供更全面的解决方案。
- 任务分工明确: 减少重复工作和提高效率。
教训和挑战:
- 时间管理: 在项目初期我们没有提前规划时间,导致项目的开工时间一再推迟。
- 沟通障碍: 缺少面对面沟通,很多时候聊了半天才发现说的不是同一个模块。
- 技术难题: 在生成算式的模块上,花费了较多时间。
结对感受:
我们在项目中面对了许多挑战,但通过相互的协作和努力,我们克服了这些问题,从中学到了很多。这个合作经验也让我更好地理解了团队合作的重要性。
结对闪光点和建议:
- 沈纪康:我认为我们的合作非常成功,但我们可以进一步改进时间管理,以确保更好地满足项目截止日期。此外,我们可以更频繁地检查并确认项目细节,以减少误解。
- 石云欣:同意,我们可以在时间管理方面做得更好。此外,我建议我们在项目的早期阶段进行更明确的分工,以便更好地理解彼此的工作内容。
总结:
这个结对项目对我们来说是一次宝贵的经验,我们在项目中不仅学到了新知识,还提高了团队合作和问题解决的能力。我们将这些经验应用到未来的项目中,继续共同努力,共同成长。