四则运算系统

这个作业属于哪个课程 软件工程2班
这个作业要求在哪里 结对项目
这个作业的目标 团队共同实现一个自动生成小学四则运算题目的命令行程序

合作人员

  • 朱雅子 3223004823
  • 黄海怡 3223004296

项目地址Github链接

一、PSP表格

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

二、项目简介

实现功能

开发一个具有图形界面的四则运算题目生成工具,支持自定义题目数量、数值范围和真分数选项,能够自动生成题目文件(Exercises.txt)和答案文件(Answers.txt),并提供答案批改功能(生成Grade.txt)。

使用方法

点击main函数运行,弹出窗口。点击是否要真分数,输入生成的式子数量和范围。点击生成按钮,就可以在Exercises.txt看见式子,在Answers.txt看见答案。(注:如果弹出提示范围不够大需要重新输入范围)生成后会弹出窗口提示。可以点击批改按钮选择要批改的txt文件。(测试,可以把Answers.txt的内容复制粘贴到要批改的答案文档,修改要测试的答案号码来测试批改功能)批改完成后会弹出提示窗口,批改结果可以在Grade.txt中查看。

项目结构

MathGeneratorGUI
├── init() # 初始化GUI窗口
├── create_widgets() # 创建界面组件(输入框、按钮等)
├── generate_expression() # 递归生成表达式树
├── normalize_expression()# 规范化表达式(排序操作数)
├── generate_number() # 生成数字(自然数或真分数)
├── calculate() # 计算表达式值
├── validate_input() # 验证用户输入合法性
├── generate() # 主生成流程(写入文件)
├── grade_answers() # 批改答案功能
└── 辅助方法
├── _format_answer() # 格式化答案输出
├── _parse_answer() # 解析答案字符串
└── _read_answer_file()# 读取答案文件

三、效能分析

性能的改进

思路

考虑到以下优点,将运算式子的生成模式改进为采用二叉树的递归算法,从有想法到实现耗时一小时。

  • 二叉树具有天然的层级优势,可控制剩余运算符数量
  • 将随机数与随机运算符分成两部分

效果


分析:根据代码分析,耗时最大的函数是generate_expression及其调用的normalize_expression。
改进方法

  1. 优化除法条件判断
    问题:允许分数时,除法要求结果不能为整数,导致多次重试
    改进:允许除法结果为整数,简化条件判断
  2. 优化查重数据结构
    问题:字符串集合查重效率低
    改进:使用数值哈希(如表达式结果)辅助查重

四、代码设计

函数设计

流程图

程序主流程

核心函数流程

1、generate_expression

2、normalize_expression

3、Generate

五、核心代码

generate_expression递归生成四则运算表达式,确保数学合法性并避免重复。

def generate_expression(self, remaining_ops, range_limit, allow_fraction, generated):
    MAX_RETRIES = 100
    for _ in range(MAX_RETRIES):
        try:
            if remaining_ops == 0:
                # 生成原子表达式(单个数字)
                # 递归终止条件(最低一层,都是数字)
                num = self.generate_number(range_limit, allow_fraction)
                expr = str(num)
                if expr in generated:
                    continue
                generated.add(expr)
                return expr, num

            op = random.choice(['+', '-', '×', '÷'])
            left_ops = random.randint(0, remaining_ops - 1)
            right_ops = remaining_ops - 1 - left_ops

            left_exp, left_val = self.generate_expression(left_ops, range_limit, allow_fraction, generated)
            right_exp, right_val = self.generate_expression(right_ops, range_limit, allow_fraction, generated)

            # 生成左右子树立即规范化
            left_exp = self.normalize_expression(left_exp)
            right_exp = self.normalize_expression(right_exp)

            if op == '-' and left_val < right_val:
                left_exp, right_exp = right_exp, left_exp
                left_val, right_val = right_val, left_val

            if op == '÷':
                for _ in range(MAX_RETRIES):
                    if right_val != 0 and not (allow_fraction and (left_val / right_val).denominator == 1):
                        break
                    right_exp, right_val = self.generate_expression(right_ops, range_limit, allow_fraction,
                                                                    generated)
                else:
                    continue

            # 按数值排序操作数
            if op in ['+', '×']:
                if left_val > right_val:
                    left_exp, right_exp = right_exp, left_exp
                    left_val, right_val = right_val, left_val
                expr = f"{left_exp} {op} {right_exp}"
            else:
                expr = f"{left_exp} {op} {right_exp}"

            if remaining_ops > 1:
                expr = f"({expr})"

            # 直接返回规范化后的表达式
            if expr in generated:
                continue
            generated.add(expr)

            value = self.calculate(op, left_val, right_val)
            return expr, value

        except (ValueError, ZeroDivisionError):
            continue
    raise RuntimeError(f"无法生成合法表达式,建议扩大数值范围(当前r={range_limit})")

normalize_expression规范化表达式结构,消除交换律导致的重复。

def normalize_expression(self, expr): #让所有表达式规范化,便于查重
    # 移除所有括号
    expr = expr.replace('(', '').replace(')', '')

    def sort_sub_expressions(e):
        # 递归拆分表达式并排序
        if '×' in e or '÷' in e:
            # 处理乘除优先
            parts = []
            current = []
            ops = []
            for c in e:
                if c in ['×', '÷']:
                    parts.append(''.join(current).strip())
                    ops.append(c)
                    current = []
                else:
                    current.append(c)
            parts.append(''.join(current).strip())
            # 对乘法项排序(数值从小到大)
            for i in range(len(parts)):
                if i < len(ops) and ops[i] == '×':
                    parts[i] = self.normalize_expression(parts[i])
                    parts[i + 1] = self.normalize_expression(parts[i + 1])
                    # 按数值排序
                    left = self._parse_number(parts[i])
                    right = self._parse_number(parts[i + 1])
                    if left > right:
                        parts[i], parts[i + 1] = parts[i + 1], parts[i]
            # 重组表达式
            sorted_expr = parts[0]
            for i in range(len(ops)):
                sorted_expr += f' {ops[i]} {parts[i + 1]}'
            return sorted_expr
        else:
            # 处理加法
            add_parts = e.split('+')
            # 转换为数值排序
            add_nums = [self._parse_number(p) for p in add_parts]
            sorted_parts = sorted(zip(add_nums, add_parts), key=lambda x: x[0])
            return '+'.join([p[1] for p in sorted_parts])

    return sort_sub_expressions(expr)

六、测试

以下是以测试代码形式重组的10个关键测试用例,附带正确性验证说明。所有测试基于unittest框架,需与主程序文件main.py配合使用

 # 测试用例1:边界值输入验证
    def test_min_valid_input(self):
        with patch.object(self.gui, 'n_entry') as mock_n, \
             patch.object(self.gui, 'r_entry') as mock_r:
            mock_n.get.return_value = "1"
            mock_r.get.return_value = "10"
            self.assertEqual(self.gui.validate_input(), (1, 10))

    # 测试用例2:非法字符输入检测
    def test_invalid_character_input(self):
        with patch.object(self.gui, 'n_entry') as mock_n, \
             patch.object(self.gui, 'r_entry') as mock_r:
            mock_n.get.return_value = "5a"
            mock_r.get.return_value = "20"
            self.assertIsNone(self.gui.validate_input())

    # 测试用例3:带分数运算逻辑
    def test_mixed_number_calculation(self):
        result = self.gui.calculate('÷', Fraction(7, 2), Fraction(1, 2))
        self.assertEqual(result, Fraction(7))

    # 测试用例4:表达式去重验证
    def test_expression_normalization(self):
        expr1 = self.gui.normalize_expression("3+5")
        expr2 = self.gui.normalize_expression("5+3")
        self.assertEqual(expr1, expr2)

    # 测试用例5:除零保护测试
    def test_division_by_zero_protection(self):
        with self.assertRaises(ValueError):
            self.gui.calculate('÷', Fraction(3, 4), Fraction(0))

    # 测试用例6:文件格式验证
    def test_output_file_formatting(self):
        test_exercises = {
            1: "3+2",
            2: "1/2 × 4",
            3: "(5-2) ÷ 3"
        }
        with patch.object(self.gui, 'generate_exercises', return_value=test_exercises):
            self.gui.generate()
            with open("Exercises.txt", "r") as f:
                lines = f.readlines()
                self.assertTrue(lines[0].startswith("[1] "))

    # 测试用例7:括号优先级验证
    def test_bracket_priority(self):
        expr = "(1/2 + 3) × 4"
        normalized = self.gui.normalize_expression(expr)
        self.assertIn("×(4)", normalized)  # 验证乘法符号位置

    # 测试用例8:答案格式容错
    def test_answer_format_parsing(self):
        with self.assertRaises(ValueError):
            self.gui._parse_answer("2’3/4")  # 错误的中文撇号

    # 测试用例9:大数据量压力测试
    def test_large_scale_generation(self):
        with patch.object(self.gui, 'n_entry') as mock_n, \
             patch.object(self.gui, 'r_entry') as mock_r:
            mock_n.get.return_value = "10000"
            mock_r.get.return_value = "100"
            self.gui.generate()
            self.assertEqual(len(self.gui.exercises), 10000)

    # 测试用例10:运算符分布验证
    def test_operator_distribution(self):
        operators = set()
        for _ in range(100):
            expr = self.gui.generate_expression(10, True)
            ops = {c for c in expr if c in '+-×÷'}
            operators.update(ops)
        self.assertEqual(operators, {'+', '-', '×', '÷'})

    def tearDown(self):
        self.test_dir.cleanup()

if __name__ == "__main__":
unittest.main(verbosity=2)

思路:

七、项目小结

项目亮点

1、数学规则严谨性(确保减法结果非负、除法分母合法)
2、高效查重机制(通过规范化表达式实现O(1)时间复杂度的重复判断)
3、用户友好性(图形界面操作简单,支持错误提示和状态反馈)
4、数据结果高效性(通过递归生成和规范化机制,本项目能够高效生成符合数学规则的题目,适用于小学数学教育场景)

通过项目积累的经验

这个项目不仅让我们巩固了二叉树、递归这样的技术方法,也让我们对一整套开发流程有了更深刻的理解。开发不仅仅是一个人的事,也是团队里的每个人都要参与讨论编写的,从程序到测试到文档,我们应当合理分工、用心完成。

结对感受

黄海怡to朱雅子:非常给力的队友,执行力强,思维缜密,代码水平高,教会了我许多!
朱雅子to黄海怡:两个人工作减少了工作量,可以专注在自己想做的领域。

posted @ 2025-03-17 12:42  hihuang  阅读(54)  评论(0)    收藏  举报