• 四则运算题目生成程序


    个人作业1——四则运算题目生成程序(基于控制台)

    项目已提交到Coding.net:arith-exercise

    需求分析

    • 使用 -n 参数控制生成题目的个数
    • 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
    • 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
    • 每道题目中出现的运算符个数不超过3个。
    • 其中真分数在输入输出时,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
    • 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
    • 程序应能支持一万道题目的生成 [spoiler]然而并没有规定时间哈哈哈[/spoiler]。
    • 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,并会输出所有题目中重复的题目。

    功能设计

    • 根据命令行参数种类的不同来分别进入两种模式:算式生成和算式检查
    • 设计一个数字类,用来封装分数和四则运算操作
    • 题目没有给出算式生成模式时输出的答案文件名的命令行参数,所以默认为“Answer.txt”

    设计实现

    采用Java开发

    项目结构

    • arith
      • Arith              封装了数学方法,比如算式校验和算式计算
      • Checker          检查测验文件,进行算式查重,并将答案与答案文件里的比对,输出结果到成绩文件
      • Creator          生成算式
    • model
      • Expression     表达式
      • Number             数字,封装了四则运算和约分等操作
    • Config                    各种配置,比如-n、-r等
    • Main                       程序入口,读取参数并进入相应模式

    Model类

    这里列出两个model类——数字和表达式,因为篇幅原因省略掉了很多方法和方法的实现,具体可以到Coding.net里查看

    public class Number {
        // 把每个数字都视为分数(可能是假分数), 如果是整数的话就把分母设为1
        private int mNumerator;
        private int mDenominator;
    
        // 通过整数、分子和分母、格式化的数字字符串三种方式来构造
        public Number(int value) { }
        public Number(int numerator, int denominator) { }
        public Number(String number) { }
    
        // 根据运算符来执行对应四则运算
        public Number operate(String operator, Number number) { }
    
        // 封装的四则运算操作
        public Number add(Number number) { }
        public Number sub(Number number) { }
        public Number mul(Number number) { }
        public Number div(Number number) { }
    
        // 约分分数,用到了网上找到的最大公约数算法
        // 每次运算后都会调用一次该函数
        public void reduce() {
            if (mNumerator == 0) // 不需要约分
                return;
    
            // 计算最大公约数
            // Ref: http://blog.csdn.net/iwm_next/article/details/7450424
            int a = Math.abs(mNumerator);
            int b = mDenominator;
            while (a != b)
                if (a > b)
                    a = a - b;
                else
                    b = b - a;
    
            mNumerator /= a;
            mDenominator /= a;
        }
    }
    
    // 表达式是一个字符串数组,其中每个元素都是格式化的数字或者运算符,比如
    // (1+2/3)*4'5/6 => ["(", "1", "+", "2/3", ")", "*", "4'5/6"]
    public class Expression extends ArrayList<String> {
        // 解析一个字符串,转化为表达式类型
        public static Expression fromString(String src) { }
    }
    

    代码说明

    对于表达式的计算,参考了这篇博客的算法,但是他的算法有个地方有点问题:

    else { // 优先级小于栈顶运算符,则弹出
        tmp = stack.pop();
    
        // 这里不应该把element加入suffix里,应该把它压入栈中
        // suffix.append(tmp).append(" ").append(element).append(" ");
        suffix.append(tmp).append(" ");
        stack.push(element);
    }
    
    

    生成算式的方法

    public static void create(Config config, boolean putAnswer) {
        if (!isConfigValid(config))
            return;
    
        int number = config.number;
        int range = config.range;
        List<Expression> expressions = new ArrayList<>(number);
        List<Number> answers = new ArrayList<>(number);
    
        for (int i = 0; i < number; i++) {
            try {
                Expression exp = createExpression(range);
                Number ans = Arith.evaluate(exp);
    
                // 检查是否存在重复的算式,先检查答案是否重复再检查算式本身
                if (Checker.hasSameAnswer(answers, ans) && Checker.findDuplicate(expressions, exp) != -1)
                    throw new ArithmeticException("Expression duplicated: " + exp);
    
                expressions.add(exp);
                answers.add(ans);
            } catch (ArithmeticException e) {
                // 如果生成失败了就回退,再试一次
                i--;
            }
        }
    
        // 保存结果到文件
        output(config.output, expressions, answers, putAnswer);
    }
    

    生成随机数字的方法

    private static Number randomNumber(int numberMax) {
        if (Math.random() < PR_INTEGER) // PR_INTEGER=0.8,是生成一个整数的概率
            // 生成一个整数
            return new Number((int) (Math.random() * numberMax - 1) + 1);
        else
            // 生成一个分数
            return new Number((int) (Math.random() * numberMax - 1) + 1, (int) (Math.random() * numberMax - 1) + 1);
    }
    

    在原算式的基础上添加一个运算的方法,随机选取一个数字,比如将1+2*3里的2替换为2-4,或者带括号的(2-4)

    private static void addOperation(Expression exp, int numberMax) {
        int size = exp.size();
        int position = 0;
        int loops = 0;
    
        // 随机选择一个数字
        while (true) {
            if (Arith.isNum(exp.get(position).charAt(0)) && Math.random() > 0.5)
                break;
    
            // 如果搜索结束了还没有选中数字,就回退到起点然后重新搜索
            if (++position == size)
                position = 0;
    
            if (loops++ == 50) {
                System.out.println("Oops, something went wrong...?");
                return;
            }
        }
    
        boolean addBrackets = Math.random() > PR_BRACKET; // PR_BRACKET=0.5,是插入一个括号的概率
        if (addBrackets)
            exp.add(position++, "(");
    
        exp.add(++position, randomOperator());
        exp.add(++position, randomNumber(numberMax).toString());
    
        if (addBrackets)
            exp.add(++position, ")");
    }
    

    从文件读取并检查算式结果的算法

    // 示例:
    // Exercise.txt >       1. 1+2*3=7
    // Answer.txt >         1. 7
    
    while ((expLine = exerciseReader.readLine()) != null) {
        separator = expLine.indexOf('=');
        exp = Expression.fromString(expLine.substring(expLine.indexOf('.') + 2, separator));
    
        ansLine = answerReader.readLine();
        rightAnsStr = ansLine.substring(ansLine.indexOf('.') + 2);
    
        // 检查答案
        if (separator + 1 < expLine.length() // 填有答案
                && rightAnsStr.equals(expLine.substring(separator + 1))) // 等于正确答案
            corrects.add("" + index);
        else
            wrongs.add("" + index);
    
        // 检查重复
        rightAns = new Number(rightAnsStr);
        if (hasSameAnswer(rightAnswers, rightAns) && (duplicate = findDuplicate(expressions, exp)) != -1)
            // 暂存重复的两个表达式的下标和对象
            repeats.add(new ExpressionPair(duplicate + 1, index, expressions.get(duplicate), exp));
    
        expressions.add(exp);
        rightAnswers.add(rightAns);
    
        index++;
    }
    

    测试运行

    运行截图,耗时还是比较长……

    生成10000个算式,最大数字10

    这里因为÷号不是ascii所以读取出来的是乱码,需要加上 -Dfile.encoding=utf-8 参数

    检查算式

    算式、答案、成绩文件

    现在的查重还是做不到检测交换顺序后的重复,只能检测到数字顺序一样并且运算符和优先级一致的算式,具体来说就是去掉或加上括号的那种重复

    PSP

    PSP2.1 Personal Software Process Stages Time Predicted Time
    Planning 计划 5 5
    · Estimate 估计这个任务需要多少时间 5 5
    Development 开发 420 973
    · Analysis 需求分析 (包括学习新技术) 10 15
    · Design Spec 生成设计文档 - -
    · Design Review 设计复审 - -
    · Coding Standard 代码规范 - -
    · Design 具体设计 - -
    · Coding 具体编码 200 491
    · Code Review 代码复审 10 270
    · Test 测试(自我测试,修改代码,提交修改) 200 197
    Reporting 报告 100 94
    . 测试报告 90 94
    . 计算工作量 10 -
    . 并提出过程改进计划 - -

    做这个项目还是花了很多时间的,虽然明明可以把功能完成得差不多就得了,但是为什么还要这么拼呢,对啊为什么呢……
    大概是因为完美主义吧,就像我玩游戏一定要做全成就一样

    这个表格的时间我也算得比较严格,误差应该在30分钟之内

    具体编码这部分虽然一开始就觉得会花很久,结果最后花的时间还是比预期的要多

    测试这部分虽然没有超出估计值,但还是比我之前做项目时花的时间多得多,也是第一次用了单元测试,感觉太棒了,非常好用啊

    后来还花了3个半小时来写文档和注释(大部分时间在查单词- -),不知道该归类在哪,就写到代码复审里了

    还有一些步骤我没做或者不知道哪些属于它,就没写时间了

    另外,最烦人的部分是git,带着各种匪夷所思的错误,为了git的一个push花了3个小时,还是在寂寞的半夜

    最后写博客花的时间我没记,不过加起来也得有3、4个小时吧

    小结

    这个项目虽然耗费了我大量娱乐时间,但也让我学到了很多有用的东西

    比如JUnit,以前我都懒得写测试,但现在我第一次体会到了单元测试的好处,再也不用为了测一段代码就得反复把整个程序跑起来,还各种模拟操作了

    再比如javadoc,这是我第一次这么认真地写javadoc,也基本了解了它的语法和一些表达习惯

    还有git,虽然第一次配置配得我头都快秃了,但配完之后用起来还是很好用的,对代码管理也是大有帮助

  • 相关阅读:
    java学习网址大全
    Js 提示框
    api帮助文档及常见IT学习网站
    传参给 jsp
    jsp>action
    二级横菜单显示+sitemesh母板应用
    request,session
    map>json
    清空image画布并改变大小填充背景色
    取汉字首字母方法
  • 原文地址:https://www.cnblogs.com/plab/p/7538164.html
Copyright © 2020-2023  润新知