结对项目:实现自动生成小学四则运算题目

发布时间 2023-09-25 12:45:16作者: 冥羌
这个作业属于哪个课程 计科21级12班
这个作业要求在哪里 结对项目
这个作业的目标 熟悉结对项目的的合作方法与模式

团队成员信息

姓名 学号
刘晋延 3121004832
张建文 3121004845

PSP表格

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

接口的设计和实现

1、设计思路

该次项目实现了全部需求1~9,根据分析,整体功能总共需要实现三个方面的功能:生成四则运算式,计算结果,文件读写。

  • 生成四则运算式
    • 运算式需要考虑是否重复,是否出现除数为0和运算过程出现负数的情况,均利用二叉树来检查。
    • 分数需要单独写一个方法进行随机。
  • 计算结果
    • 采用后缀表达式计算
    • 因为生成的四则运算式为中缀表达式,故还需要先转化为后缀表达式。
  • 文件读写
    • 文件读写均调用包里的接口,比较简单

2、整体流程

需要根据输入的参数分辩具体实现的功能。
-n:指定生成题目数量
-r:指定题目中数字范围
-e:指定输入的题目文件路径
-a:指定输入的答案文件路径

3、类

  • 生成四则运算式
    • BinaryTree.java :主要用于二叉树的操作,如建树等;基于二叉树判断重复的算法也在此处。
    • Generate.java :主要用于四则运算式的生成,其内包含生成分数,以及生成整合四则运算式及答案的方法。
    • TreeNode.java :定义了二叉树的节点,包含具体检测减法除法以及由树转换后缀表达式的方法。
  • 计算结果
    • ComputeFourRule.java:含有结果的计算和中缀表达式转换后缀表达式的方法。
    • Fraction.java:该类主要对分数进行操作,分割分子与分母,以及标准化分数的格式。
  • 文件读写
    • Determine.java:读入题目与答案,并输出对错结果。
    • read.java:文件读写
  • 其他
    • Main.java:main方法所在的类
    • MyException.java:自定义异常类,用于抛出异常信息

4、关键代码

1、判断四则运算式重复

主要通过后缀表达式所构建的二叉树判断,根据两个式子的二叉树是否能通过交换加与乘节点的左右子树变成同一个二叉树来判断。代码如下:

    public static boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null && q == null) {
            return true;
        } else if (p == null || q == null) {
            return false;
        } else if (!p.getName().equals(q.getName())) {
            return false;
        } else {
            if(p.getName().equals("+") || p.getName().equals("*")){//加法与乘法可交换节点
                return (isSameTree(p.getLeft(), q.getLeft()) && isSameTree(p.getRight(), q.getRight()))||isSameTree(p.getLeft(),q.getRight() ) && isSameTree(p.getRight(),q.getLeft() );
            }
            return isSameTree(p.getLeft(), q.getLeft()) && isSameTree(p.getRight(), q.getRight());
        }
    }

2、判断除法与减法

除法与减法的判断逻辑相似,都是遍历二叉树,找到对应除法与减法的节点,根据其左右子树判断。代码如下:

public boolean postOrderDivide(){
        //遍历检查除法
        boolean  statel=true;
        boolean stater=true;
        if(this.left!=null){
           statel= this.left.postOrderDivide();
        }
        if(this.right!=null){
            stater= this.right.postOrderDivide();
        }
        if(!(statel&&stater)) return false;
        if(this.name.equals("÷")){//除法判断右子树对应式子值是否会为0
            List<String> Right=new ArrayList<>();
            this.right.postOrder(Right);
            return !ComputeFourRule.Compute(Right).equals("0");
        }
        else{
            return true;
        }


    }
public boolean postOrderSubtraction(){
        //遍历检查减法
        boolean  statel=true;
        boolean stater=true;
        if(this.left!=null){
            statel=this.left.postOrderSubtraction();
        }
        if(this.right!=null){
            stater=this.right.postOrderSubtraction();
        }
        if(!(statel&&stater)) return false;
        if(this.name.equals("-")){//计算减法左右子树对应式子的值比较大小
            List<String> Left=new ArrayList<>();
            List<String> Right=new ArrayList<>();
            this.left.postOrder(Left);
            this.right.postOrder(Right);
            String leftnumber=ComputeFourRule.Compute(Left);
            String rightnumber=ComputeFourRule.Compute(Right);
            int[] number1=new Fraction().SplitNumber(leftnumber);
            int[] number2=new Fraction().SplitNumber(rightnumber);
            return number1[0] * number2[1] >= number2[0] * number1[1];
        }
        else return true;
    }

3、中缀表达式转为后缀表达式

  • 主要流程为:
    • 从左到右读入中缀表达式
    • 读入操作数直接输入后缀表达式
    • 读入左括号直接入栈
    • 读入右括号直接出栈,并将出栈运算符送入后缀表达式,直到左括号出栈
    • 读入运算符,若栈空,直接入栈。
      若栈非空,判断栈顶操作符,若栈顶操作符优先级低于该操作符,该操作符入栈;否则一直出栈,并将出栈字符依次送入后缀表达式,直到栈空或栈顶操作符优先级低于该操作符,该操作符再入栈。
public static List<String> SuffExpression(List<String> list){
        //转换为后缀表达式
        List<String> suffexpression= new ArrayList<>();
        Stack<String> stack= new Stack<>();
        for(int i=0;i<list.size();i++){
            String suff=list.get(i);
            if(suff.equals("(")) {
                stack.push(suff);
            }else if(suff.equals(")")){
                while (!stack.peek().equals("("))
                    suffexpression.add(stack.pop());
                stack.pop();
            }else if(suff.equals("*")||suff.equals("÷")){
                if(!stack.empty()){
                    while (stack.peek().equals("*")||stack.peek().equals("÷")) {
                        suffexpression.add(stack.pop());
                        if(stack.empty())
                            break;
                    }
                }
                stack.push(suff);
            }else if(suff.equals("+")||suff.equals("-")){
                if(!stack.empty()){
                    while (stack.peek().equals("+")||stack.peek().equals("-")||stack.peek().equals("*")||stack.peek().equals("÷")){
                        suffexpression.add(stack.pop());
                        if(stack.empty())
                            break;
                    }
                }
                stack.push(suff);
            }else {
                suffexpression.add(suff);
            }
        }
        while (!stack.empty()){
            suffexpression.add(stack.pop());
        }
        return suffexpression;
    }

4、计算结果

从左到右遍历后缀表达式的每个数字和符号,遇到是操作数就进栈,遇到是符号,就将处于栈顶两个操作数出栈,进行运算,运算结果进栈,一直到最终获得结果。因为存在分数,故计算需要单独处理。

public static String Compute(List<String> list){
        String result;
        Stack<String> stack1= new Stack<>();
        int leftnumber;
        int rightnumber;
        int[] number1=new int[2];
        int[] number2=new int[2];
            for(int i=0;i<list.size();i++){
                String suff=list.get(i);
                if (suff.equals("+")) {
                    number2=new Fraction().SplitNumber(stack1.pop());
                    number1=new Fraction().SplitNumber(stack1.pop());
                    leftnumber=number1[0]*number2[1]+number2[0]*number1[1];
                    rightnumber=number1[1]*number2[1];
                    stack1.push(leftnumber+"/"+rightnumber);
                } else if (suff.equals("-")) {
                    number2=new Fraction().SplitNumber(stack1.pop());
                    number1=new Fraction().SplitNumber(stack1.pop());
                    leftnumber=number1[0]*number2[1]-number2[0]*number1[1];
                    rightnumber=number1[1]*number2[1];
                    stack1.push(leftnumber+"/"+rightnumber);
                } else if (suff.equals("*")) {
                    number2=new Fraction().SplitNumber(stack1.pop());
                    number1=new Fraction().SplitNumber(stack1.pop());
                    leftnumber=number1[0]*number2[0];
                    rightnumber=number1[1]*number2[1];
                    stack1.push(leftnumber+"/"+rightnumber);
                }else if(suff.equals("÷")){
                    number2=new Fraction().SplitNumber(stack1.pop());
                    number1=new Fraction().SplitNumber(stack1.pop());
                    leftnumber=number1[0]*number2[1];
                    rightnumber=number1[1]*number2[0];
                    stack1.push(leftnumber+"/"+rightnumber);
                }else {
                    stack1.push(suff);
                }
            }
            result=stack1.pop();
        result=new Fraction().SimplestFraction(result);
        return result;
    }

测试运行

1、BinaryTree类

  • 主要测试
    • 检查四则式的重复功能
    • 检查除法的除数是否为0的方法
    • 检查减法是否会产生负数
      测试代码如下:
@Test
   void checkRepetition() {
       //检查四则式的重复功能
       List<String> a= Arrays.asList("1","2","+","3","+");
       List<String> b=Arrays.asList("3","2","1","+","+");
       List<String> c=Arrays.asList("3","2","+","1","+");
       System.out.println(BinaryTree.CheckRepetition(a,b));
       System.out.println(BinaryTree.CheckRepetition(a,c));
   }

   @Test
   void checkdivide(){
       //检查除法的除数是否为0的方法
       List<String> a=Arrays.asList("(","8","*","7",")","÷","(","5","-","4",")");
       List<String> b=Arrays.asList("(","8","*","7",")","÷","(","5","-","5",")");
       System.out.println(BinaryTree.CheckDivide(ComputeFourRule.SuffExpression(a)));
       System.out.println(BinaryTree.CheckDivide(ComputeFourRule.SuffExpression(b)));
   }
   @Test
   void checksubtraction(){
       //检查减法是否会产生负数
       List<String> a=Arrays.asList("(","8","*","7",")","÷","(","5","-","6",")");
       List<String> b=Arrays.asList("(","8","*","7",")","÷","(","5","-","4",")");
       System.out.println(BinaryTree.CheckDivide(ComputeFourRule.SuffExpression(a)));
       System.out.println(BinaryTree.CheckDivide(ComputeFourRule.SuffExpression(b)));
   }
  • 测试结果

  • 代码覆盖率

2、ComputeFourRule类

  • 主要测试
    • 计算结果
    • 转换为后缀表达式
 void suffExpression() {
        //转换为后缀表达式
        List<String> a=Arrays.asList("1","+","2","+","3");
        List<String> b=Arrays.asList("(","8","*","7",")","÷","(","5","-","4",")");
        a=ComputeFourRule.SuffExpression(a);
        b=ComputeFourRule.SuffExpression(b);
        System.out.println(a);
    }

    @Test
    void compute() {
        //计算结果
        List<String> a= Arrays.asList("1","÷","6","+","2'7/10","*","7");
        a=ComputeFourRule.SuffExpression(a);
        System.out.println(ComputeFourRule.Compute(a));
    }
  • 测试结果

  • 代码覆盖率

该处代码覆盖率主要跟所涉及的运算种类有关

3、Fraction类

  • 主要测试
    • 分割分子分母
    • 转换分数格式
 @Test
    void splitNumber() {
        //测试分割分子分母
        int[] a=new Fraction().SplitNumber("2");
        System.out.println("分子:"+a[0]+" 分母:"+a[1]);
        int[] b=new Fraction().SplitNumber("2'2/3");
        System.out.println("分子:"+b[0]+" 分母:"+b[1]);
        int[] c=new Fraction().SplitNumber("7/8");
        System.out.println("分子:"+c[0]+" 分母:"+c[1]);
    }

    @Test
    void simplestFraction() {
        //测试转换分数格式
        System.out.println(new Fraction().SimplestFraction("10/3"));

    }
  • 测试结果

  • 代码覆盖率

4、Generate类

  • 主要测试
    • 式子与分数的生成
    • 整合后的多个式子生成与计算
@org.junit.jupiter.api.Test
    void creat() {
        System.out.println(String.join(" ",Generate.Creat(10)));
    }

    @org.junit.jupiter.api.Test
    void creatFraction() {
        System.out.println(Generate.CreatFraction(10));
    }

    @Test
    void EquationCreateTest(){
        //生成试题
        List<List<String>> a=Generate.EquationCreate(10,10);
       for (List<String> b:a){
            System.out.println(String.join(" ",b));
        }
    }
    @Test
    void AnswerTest(){
        //计算答案
        List<List<String>> a=Generate.EquationCreate(10,10);
        List<String> b=Generate.Answer(a);
        for (List<String> c:a){
            System.out.println(String.join(" ",c));
        }
        for(String d:b){
            System.out.println(d);
        }
    }
  • 测试结果
  • 代码覆盖率

5、参数异常输入测试


如图,当参数输入存在错误时会给出提示。

6、正常功能测试

  • 1、生成10道范围为10的题目

    结果如下:
  • 2、传入文件计算对错
    传入文件

    结果如下

效能分析


如图,占用最大的内存为TreeNode类,此为二叉树节点类,主要原因:生成四则运算需要遍历所有题目生成二叉树来判断重复,故当题目数量增大时,该类占用内存会增大

项目小结

  • 项目分工
    • 刘晋延:实现四则运算式的生成与运算式重复检验,以及博客的整理。
    • 张建文:实现四则运算式的计算与文件的读写,以及需求9中对真错的判断与输出。