结对项目

结对项目

软件工程 班级网址
作业要求 作业要求代码
作业目标 积累结对项目经验,提高交流和合作能力

合作者

黄博晓 信息安全 (1) 班 学号 : 3118005363

凌文宇 信息安全 (1) 班 学号 : 3118005372

作业代码链接

github 网址 : https://github.com/bxxiao/bxxiao/tree/master/ArithmeticGeneratorProject

PSP 表格

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

1. 设计实现过程

包结构:

image-20201012120622839
  • expression包:一个Expression对象表示一个四则表达式,其中提供了随机创建表达式、检验表达式、获取表达式运算结果和返回表达式字符串形式等方法。一个四则表达式封装为一个二叉树,TreeNode是二叉树的结点,每个结点保存一个运算符或运算数。

  • util包:

    • Calculator:主要提供了针对整数、分数的加减乘除运算、以及获取分数的最简形式的静态方法。运算结果以字符串形式返回。返回形式有整数和分数(如1/3,2'5/6)形式。

    • Ran:用于生成随机值。包括随机生成运算符、随机生成运算数(整数和分数)以及随机生成运算符在二叉树中的位置。

    • CommandResolver:命令解析器。解析main方法的args参数,获取其中的有用参数。若输入形式不符合要求,会打印提示信息。若输入形式符合要求,则获取其中指定的题目数和最大值并保存。

    • FilteUtil:用于将生成的题目跟答案写入文件。写入的格式如下:

      1. exp1 = or answer1
      2. exp2 = or answer2
      3. exp3 = or answer3
      ...
      
  • generator包:

    • Generator:提供了一个generate()方法,使用CommandResolver解析输入的命令,获取题目数和最大值,根据这两个值创建表达式和对应的答案,并使用FilteUtil将题目和答案写入当前目录下的expFile.txt和answers.txt文件(若不存在会创建)。在创建每个表达式之前,先随机生成一个1-3的整数,作为运算符个数。
    • Main:包含main方法。
  • test包:包含一个测试类。(使用Junit)

2. 代码说明

2.1 表达式的字符串形式

一个表达式对应的二叉树是一个完全二叉树,其中,叶子结点都是运算数,父结点都是运算符。对这样的二叉树进行中序遍历可得到一个运算表达式,且每次返回父结点都会带上括号,如:

image-20201012193135414

遍历结果是((1+2)-(3x4))

再通过以下原则去除多余的括号

假设待去括号的表达式为(m1 op1 n1) op (m2 op2 n2)这里m1、n1、m2、m2可能自身就是个表达式,也可能是数字,op、op1、op2为运算符

1、若op为'÷',则 (m2 op2 n2)的括号必须保留;

2、若op为'x'或'-',如果op2为'+'或'-',则(m2 op2 n2)的括号必须保留;

3、若op为'x'或'÷',如果op1为'+'或'-',则(m1 op1 n1)的括号必须保留;

4、 除此之外,去掉括号不影响表达式的计算顺序。

如上面例子去掉多余括号后变为:1+2-3x4

将当前二叉树按以上规则转换为字符串形式的方法在TreeNode中:

public String toString(){
    String leftExp = "";//左表达式
    String rightExp = "";//右表达式
    String result = "";//运算符

    //若当前结点有孩子
    if(hasChild()){
        //右子树如果有孩子,说明右子树是一个表达式,而不是数字节点。
        if(getRchild().hasChild()){
            //判断左邻括号的运算符是否为'/'
            if(str.equals("÷")){
                //获取右子树的表达式,保留括号
                rightExp = getRchild().toString();
            }
            //判断左邻括号的运算符是否为'x'或'-'
            else if(str.equals("x") || str.equals("-")){
                //判断op是否为'+'或'-'
                if(getRchild().str.equals("+") || getRchild().str.equals("-")){
                    rightExp = getRchild().toString();
                }
                else{
                    //获取右子树的表达式,并且去括号
                    rightExp = getRchild().toString().substring(1, getRchild().toString().length()-1);
                }
            }
            else{
                //右子树除此之外都是可以去括号的。
                rightExp = getRchild().toString().substring(1, getRchild().toString().length()-1);
            }
        }
        else{
            rightExp = getRchild().str;
        }

        //左子树的情况同右子树类似
        if(getLchild().hasChild()){
            if(str.equals("x") || str.equals("÷")){
                if(getLchild().str.equals("+") || getLchild().str.equals("-")){
                    leftExp = getLchild().toString();
                }
                else{
                    leftExp = getLchild().toString().substring(1, getLchild().toString().length()-1);
                }
            }
            else{
                leftExp = getLchild().toString().substring(1, getLchild().toString().length()-1);
            }
        }
        else{
            leftExp = getLchild().str;
        }
        //获取当前的运算式,并加上括号
        result = "(" + leftExp + " " + this.str + " " + rightExp + ")";
    }
    else{
        //若没有孩子,说明是数字节点,直接返回数字
        result = this.str;
    }
    return result;
}

2.2 随机生成表达式

2.2.1 随机创建表达式 

随机生成二叉树的思路:

  • 先随机生成一个运算符作为根结点。若只有一个运算符,直接生成两个运算数作为根节点的左右孩子。
  • 若有2或3个运算符:
    • 先根据运算符个数随机获取运算符结点位置的boolean数组(见下文getChildPlace())。
    • 遍历该boolean数组,若为true,则要创建一个表达式子树(运算符为父结点,运算数为其左右孩子);若为false,则创建一个运算数子树(只有一个结点)。
    • 在上述遍历中设置一个遍历索引i,若i为偶数,则创建的子树作为"当前结点"的左孩子,否则为右孩子。且每创建一个作为左孩子的表达式子树,则该子树的根节点作为“当前结点”(当前结点从第一步生成的根节点开始)...
    • 这种方式创建子树能保证最后生成的二叉树是完全二叉树。
  • 创建完表达式,调用CalAndVal()对表达式校验。
public void createExpression(){
    TreeNode lchild, rchild, lnode, rnode;

    //只有一个运算符
    if(opCounts == 1){
        lchild = new TreeNode(Ran.getNumber(max), null, null);
        rchild = new TreeNode(Ran.getNumber(max), null, null);
        root = new TreeNode(Ran.getOperator(), lchild, rchild);
    }
    else{
        int num1 = 0;
        boolean[] place = Ran.getChildPlace(opCounts);
        root = new TreeNode(Ran.getOperator(), null, null);
        opeList.add(root);

        for(int i = 0; i < place.length; i++){
            //place[i]为true,生成一个运算符,并为该运算符生成两个左右孩子运算数
            if(place[i]){
                lnode  = new TreeNode(Ran.getNumber(max), null, null);
                rnode  = new TreeNode(Ran.getNumber(max), null, null);
                //i为偶数(从0开始的),将新创建的运算符作为当前结点(运算符)的左孩子;为奇,则作为右孩子
                if(i%2 == 0){
                    lchild = new TreeNode(Ran.getOperator(), lnode, rnode);
                    opeList.add(lchild);
                    opeList.get(num1).setLchild(lchild);
                }
                else{
                    rchild = new TreeNode(Ran.getOperator(), lnode, rnode);
                    opeList.add(rchild);
                    opeList.get(num1).setRchild(rchild);
                }
            }
            //place[i]为false,则只生成一个运算数,组装到当前运算符的左右孩子
            else{
                if(i%2 == 0){
                    lchild = new TreeNode(Ran.getNumber(max), null, null);
                    opeList.get(num1).setLchild(lchild);
                }
                else{

                    rchild = new TreeNode(Ran.getNumber(max), null, null);
                    opeList.get(num1).setRchild(rchild);
                }
            }
            //num1为偶数,说明当前结点还未生成有孩子,num1不变;若是奇数,则+1
            num1 = num1 + i%2;
        }
    }
    CalAndVal();//生成完二叉树进行校验
}

2.2.2 随机生成运算符结点位置

Ran类中包含一个根据运算符个数随机生成运算符结点位置的方法,其实现思路:

  • 方法返回返回一个长度为2的boolean数组,true元素的个数表示要新创建的运算符个数。
    • 在随机生成表达式的方法中会使用到该方法,true表示在当前结点中,创建一个以运算符为父结点,两个运算数为叶子结点的子二叉树,作为当前结点的左或右孩子;false则表示创建一个运算数,作为当前结点的左或右孩子。
  • 若运算符个数为2,则需要再创建一个运算符,所以true,false各一个。
  • 运算符个数为3,则需要再创建两个运算符。元素都是true。
public static boolean[] getChildPlace(int num){
    boolean[] result = new boolean[]{true, true};

    if(num==2){
        Random random = new Random();
        if(random.nextBoolean()){
            result[0] = false;
        }else {
            result[1] = false;
        }
    }

    return result;
}

2.2.3 校验表达式

校验表达式包括对除数为0,减法运算结果为负的情况的处理。

  • 若除数为0,替换除号为其他运算符。
  • 若减法结果为负,交换被减数和减数的位置。
  • CalAndVal()实际调用了getResult()方法,该方法获取当前结点对应的运算结果并按以上规则校验。

public String getResult(){
    //若有子节点,说明当前结点是运算符
    if(hasChild()){
        switch(str){
            case "+":
                return Calculator.add(getLchild().getResult(), getRchild().getResult());
            case "-":
                String subResult = Calculator.sub(getLchild().getResult(), getRchild().getResult());
                //若返回值为-1,交换左右孩子
                if(subResult.contains("-")){
                    swapChild();
                    return Calculator.sub(getLchild().getResult(), getRchild().getResult());
                }
                else {
                    return subResult;
                }
            case "x":
                return Calculator.mul(getLchild().getResult(), getRchild().getResult());
            case "÷":
                String divResult = Calculator.division(getLchild().getResult(), getRchild().getResult());
                //返回结果为-1,表示除数为0,替换运算符
                if(divResult.contains("-")){
                    while(str.equals("÷")){
                        //将当前运算符转换为其他运算符
                        str = Ran.getOperator();
                    }
                    return this.getResult();
                }
                else {
                    return divResult;
                }
        }
    }
    //无子节点,说明当前结点是叶子结点,直接返回结点值
    return str;
}

3. 测试

输入命令-n 6 -r 10(生成6道题目,最大值为10(不包括10)),生成的题目和答案如下:

题目:(在当前目录下的expFile.txt中)

image-20201012164901696

答案:(在当前目录下的answer.txt中)

image-20201012164924487

4. 小结

4.1 黄博晓

  • 跟别人合作开发要注意沟通好。

  • 数据结构很重要。

  • 使用搜索引擎有时可以极大的提高效率...

4.2 凌文宇

  • 基础真的很重要,以后要继续努力才能更好的和队友合作

  • 全程大佬带飞,以后要继续加油,争取不拖后腿

  • 事先的沟通与安排是必不可少的

参考链接

https://www.cnblogs.com/echoing/p/7878954.html

https://blog.csdn.net/kuku713/article/details/12684509

posted @ 2020-10-12 19:58  tfls  阅读(211)  评论(0编辑  收藏  举报