结对作业

项目 内容
这个作业属于哪个课程 班级连接
作业要求 作业要求
作业目标 四则运算生成器+生成应用程序+结对
成员1 黄奕威 3119005415 Github地址
成员2 刘淑婷 3219005448 Github地址

代码链接

https://github.com/lll1023/teamwork

使用说明

详见仓库README.md文件。

设计实现过程

本项目主要采用MVC模式进行开发,其中后端语言为Java,使用springboot框架;前端语言为HTML、CSS、JavaScript。

Model

Model主要分为四大类:

1. Fraction类

1.1 属性
  • int molecular:分子
  • int denominator:分母
1.2 接口
  • Fraction(String num):将分数的字符串形式转换成Fraction对象。
  • parseFraction(int num):将整数转换成Fraction对象。
  • newRandomFraction(int max):随机生成不超过max的数。
  • addition(Fraction other):加法运算。
  • subtraction(Fraction other):减法运算。
  • multiplication(Fraction other):乘法运算。
  • division(Fraction other):除法运算。
  • toString():重写toString方法,将Fraction对象以真分数形式输出。
  • equals(Object o):重写equals方法,由于分数对象一开始就没有化成最简形式,因此这里两个分数相等的时候就有两种情况。

2. BinaryTree类

2.1 属性
  • BinaryTree left : 左子树
  • BinaryTree right : 右子树
  • Fraction value : 节点值
  • String symbol :当前节点的符号
2.2 接口解释
  • equals(BinaryTree other) : 用来判断两棵二叉树是否一致,防止出现相同的表达式。
  • isNeedBracket(String s1, String s2, boolean leftOrRight) : 判断是否需要添加括号,用于中序遍历的顺序判断。
  • midTraverse() : 中序遍历,用来遍历整棵二叉树,形成中缀表达式。

3. Expression类

3.1 属性
  • int MAX : 符号的最大数量
  • BinaryTree root : 表达式树的根节点
  • String expression : 根据随机生成的树生成的中缀表达式
  • Fraction value : 根据树计算的值
3.2 接口
  • Expression(int maxNum) : 表达式的构造函数,通过传入最大值来随机生成表达式,其中的内部成员变量包括表达式的二叉树,表达式的中缀表示,以及表达式的计算结果。
  • generateBinaryTree(int maxNum, int maxSymbol) : 随机生成二叉树,限定最大值和最大符号数,构造出一棵二叉树。
  • getResult(BinaryTree root) : 根据传入的二叉树来计算最后的结果。
  • operate(Fraction left, Fraction right, String symbol) : 用于计算,根据传入的符号来判断left和right的运算逻辑。
  • expressionToPostfix(String expression) : 将中缀表达式转为后缀表达式(逆波兰),便于后续的逆波兰表达式计算。
  • getPostfixResult(String postfix) : 根据后缀表达式计算结果。
  • isOperator(String c) : 判断当前字符串是否为符号,用于计算后缀表达式。
  • priority(String symbol) : 判断符号的优先级,用来计算后缀表达式。

4. ResultInfo类

4.1 属性
  • int code:表示一次请求的成功与否的情况。
  • String message:存储提示信息。
  • Object result:用于存储返回给前端的值。

View

View主要采用原生JavaScript开发,实现与后台接口的对接。

根据接口主要分为两大模块:

  • 生成器模块:需要用户输入生成表达式的数量以及每个数的最大值,然后返回生成的题目和答案。
  • 校验器模块:需要用户上传题目文件和自己写的答案文件,传入到后台后返回计算结果。

页面设计如下:

image-20211022154657419

Controller

Controller是用于接收前端的请求并返回结果。

主要对应着view的两个模块,一个用于生成表达式和结果,一个用于校对给定的表达式及结果的对错情况。

关键算法代码说明

midTraverse

这是将二叉树转换成中序表达式的关键函数。

算法流程图如下:

image-20211022155824919

代码实现:

public String midTraverse() {
        BinaryTree node = this;
        StringBuilder expression = new StringBuilder();
        if(node.symbol != null) {
            String symbol = node.symbol;
            String left = node.left.midTraverse();
            String right = node.right.midTraverse();
            if(isNeedBracket(symbol,node.left.symbol,true)){
                expression.append("( ").append(left+" ) ").append(symbol + " ");
            }else {
                expression.append(left+" ").append(symbol+" ");
            }

            if(isNeedBracket(symbol,node.right.symbol,false)) {
                expression.append("( ").append(right + " )");
            }else {
                expression.append(right);
            }
            return expression.toString();
        }else {
            return  node.value.toString();
        }
    }

expressionToPostfix

这个函数用来将中缀表达式转换成后缀表达式(逆波兰表达式),方便后续计算。

算法流程图如下:

image-20211022161401576

代码实现:

    public static String expressionToPostfix(String expression) {
        Stack<String> stack = new Stack<>();
        StringBuilder queue = new StringBuilder();
        int index = 0;
        String[] s = expression.split(" ");
        while (index < s.length) {
            String c = s[index];
            if (!isOperator(c) && !c.equals("(") && !c.equals(")")) {
                queue.append(s[index] + " ");
            } else if (c.equals("(")) {
                stack.push(c);
            // 如果是右括号,就弹出栈中的元素,直到遇到左括号为止。左右括号均不入队列
            } else if (c.equals(")")) {
                while (!"(".equals(stack.peek())) {
                    queue.append(stack.pop() + " ");
                }
                stack.pop();
            } else if (isOperator(c)) {
                if (stack.isEmpty()) {
                    stack.push(c);
                } else if ("C".equals(stack.peek())) {
                    stack.push(c);
                } else if (priority(c) > priority(stack.peek())) {
                    stack.push(c);
                    // 如果此时遍历的运算符的优先级小于等于此时符号栈栈顶的运算符的优先级,
                    // 则将符号栈的栈顶元素弹出并且放到队列中,并且将正在遍历的符号压入符号栈
                } else if (priority(c) <= priority(stack.peek())) {
                    queue.append(stack.pop() + " ");
                    stack.push(c);
                }
            }

            index++;
        }

        // 遍历完后,将栈中元素全部弹出到队列中
        while (!stack.isEmpty()) {
            queue.append(stack.pop() + " ");
        }
        return queue.toString();
    }

getPostfixResult

根据后缀表达式计算结果。

算法流程图:

image-20211022161856259

代码实现:

public static String getPostfixResult(String postfix) {
        String[] items = postfix.split(" ");
        Stack<String> stack = new Stack();
        for(int i=0;i<items.length;i++) {
            if(isOperator(items[i])) {
                Fraction a = new Fraction(stack.pop());
                Fraction b = new Fraction(stack.pop());
                switch (items[i]) {
                    case "+" : {
                        a.addition(b);
                        stack.push(a.toString());
                        break;
                    }
                    case "-" : {
                        b.subtraction(a);
                        stack.push(b.toString());
                        break;
                    }
                    case "*" : {
                        a.multiplication(b);
                        stack.push(a.toString());
                        break;
                    }
                    case "÷"  :{
                        b.division(a);
                        stack.push(b.toString());
                        break;
                    }
                    default: {
                        System.out.println("表达式有误,请检查");
                    }
                }
                System.out.println(a);
            }else {
                stack.push(items[i]);
            }
        }
        return stack.pop();
    }

测试运行

这里展示一下10个测试用例。

  1. 随机生成数,用于生成四则运算。

    /**
     * 随机生成数
     */
    @Test
    void test1(){
        for (int i=0;i<10;i++){
            Fraction fraction = Fraction.newRandomFraction(10);
            System.out.println(fraction);
        }
    }
    
  2. 将真分数的字符串形式转换为Fraction对象,用于计算给定四则运算的结果。

    /**
    * 将真分数的字符串形式转换为Fraction对象
    */
    @Test
    void test2(){
        System.out.println(new Fraction("1’1/2"));
    }
    
  3. 判断两个数是否相等,以Fraction对象的形式判断两个数是否相等。

    /**
     * 判断两个数是否相等
     */
    @Test
    void test3(){
        Fraction fraction = new Fraction("1’1/2");
        System.out.println(fraction.equals(new Fraction("1’2/4")));
        System.out.println(fraction.equals(new Fraction("1’1/2")));
        System.out.println(fraction.equals("-1’1/2"));
    }
    
  4. 生成后缀表达式,用于根据四则运算表达式的中缀形式生成后缀表达式。

    /**
     * 生成后缀表达式
     */
    @Test
    void test4(){
        Expression expression = new Expression(10);
        System.out.println(Expression.expressionToPostfix(expression.getExpression()));
    }
    
  5. 随机生成四则表达式,这里随机生成10000个表达式。

    /**
     * 随机生成四则表达式
     */
    @Test
    void test5(){
        for (int i=0;i<10000;i++){
            System.out.println(new Expression(10).getExpression());
        }
    }
    
  6. 判断生成的表达式类的结果和直接用表达式的字符串形式生成的结果是否一致,用于后期判断给定的答案是否是题目的正确答案。

    /**
     * 判断生成的表达式类的结果和直接用表达式的字符串形式生成的结果是否一致
     */
    @Test
    void test6(){
        for (int i=0;i<1000;i++){
            Expression expression = new Expression(10);
            String s = expression.getExpression();
            //后缀表达式
            String toPostfix = Expression.expressionToPostfix(s.substring(0,s.indexOf("=")));
            //根据后缀表达式生成答案
            String result = Expression.getPostfixResult(toPostfix);
            //比对前后答案是否一样
            System.out.println(expression.getValue().equals(new Fraction(result)));
        }
    }
    
  7. 两数相加

    /**
     * 两数相加
     */
    @Test
    void test7(){
        Fraction fraction1 = Fraction.newRandomFraction(10);
        Fraction fraction2 = Fraction.newRandomFraction(10);
        System.out.println(fraction1);
        System.out.println(fraction2);
        fraction1.addition(fraction2);
        System.out.println(fraction1);
    }
    
  8. 两数相减

    /**
     * 两数相减
     */
    @Test
    void test8(){
        Fraction fraction1 = Fraction.newRandomFraction(10);
        Fraction fraction2 = Fraction.newRandomFraction(10);
        System.out.println(fraction1);
        System.out.println(fraction2);
        fraction1.subtraction(fraction2);
        System.out.println(fraction1);
    }
    
  9. 两数相乘

    /**
     * 两数相乘
     */
    @Test
    void test9(){
        Fraction fraction1 = Fraction.newRandomFraction(10);
        Fraction fraction2 = Fraction.newRandomFraction(10);
        System.out.println(fraction1);
        System.out.println(fraction2);
        fraction1.multiplication(fraction2);
        System.out.println(fraction1);
    }
    
  10. 两数相除

    /**
     * 两数相除
     */
    @Test
    void test10(){
        Fraction fraction1 = Fraction.newRandomFraction(10);
        Fraction fraction2 = Fraction.newRandomFraction(10);
        System.out.println(fraction1);
        System.out.println(fraction2);
        fraction1.division(fraction2);
        System.out.println(fraction1);
    }
    

以上10个测试用例的运行情况如下:

image-20211023144851408

从以上的图片可以看出,总共用时475ms。

性能分析

image-20211023150302495

PSP表格

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

项目小结

黄奕威:本次项目与之前的项目不同,终于有了合作伙伴进行开发,在开发初期分析需求时,我和小伙伴两人出现了许多不同的意见,包括对整个类的设计、接口的设计等,最后通过讨论才总结出一份开发计划出来,基本符合两人的预期。一开始,先由小伙伴实现了基础的分数类,在这基础上我再构建二叉树类和表达式类来对分数类进行连接,连接过程中我体会到了阅读别人源码的痛苦,因此注释是非常有必要的,当有些执行结果产生歧义的时候,我会自己测试,尝试找出是自己的问题还是小伙伴的问题,如果是小伙伴的便反馈给她,让她去debug。还有就是命名标准的规范问题,需要提前做好统一,不然会显得整个项目非常的不耦合。当我们把代码合并并debug后,心中终于长舒了一口气。感谢队友的信任,让这个项目最终能够顺利完成。

刘淑婷:本次项目是一个结对项目。在开启这个项目之前,我与小伙伴讨论了很久,应该以什么形式去生成并存储四则运算表达式。我们阅读了很多资料,也在网上找了很多博客,参考别人是怎么做的。最终,我们是以二叉树的形式来生成四则运算表达式。一开始,先由我写基础的分数类,再由小伙伴写生成表达式和计算结果的类。最后再由他写一个界面用于展示,而我提供接口,进行对接。在整个过程中,我们不断debug,如果发现是对方的问题就反馈给对方,让对方解决。感谢队友的信任,让这个项目最终能够顺利完成。

posted on 2021-10-25 21:31  Lsutin  阅读(73)  评论(1编辑  收藏  举报