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

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Networkengineering1834
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Networkengineering1834/homework/11148
这个作业的目标 队友之间相互协作,实现一个自动生成小学四则运算题目的命令行程序

一、结对编程

1.项目成员

陈智超(3118005320),陈燕(3218005352)

2.Github项目地址

https://github.com/chenzhichaohh/ChenZC-ChenY-Pair-Project.git

3.commit记录

4. 实现的需求

4.1 使用 -n 参数控制生成题目的个数

4.2 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围

4.3 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。

4.4 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。

4.5 每道题目中出现的运算符个数不超过3个。

4.6 程序一次运行生成的题目不能重复。

4.7 生成的题目存入执行程序的当前目录下的Exercises.txt文件。
4.8 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。

4.9 程序应能支持一万道题目的生成。

4.10 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计。

4.psp

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

二、程序设计

1.流程图

1.1项目执行流程

2.项目架构

2.1 工程架构

2.2 关键类的UML类图

3.效能分析

3.1 算法改进之前:

3.2 算法改进之后:

3.3 改进过程

可以看到,最耗费内存的就是生成题目的方法,第一次的算法,套用了两层循环,时间复杂度太高,借鉴快排的思想,达到条件之后马上跳出循环,这样时间复杂度就能够降低了。

4.算法逻辑

4.1随机数生成算法流程

4.2利用二叉树构造四则运算表达式并生成运算结果流程图

4.3去掉重复题目算法流程

5.设计思路(12个关键点)

5.1 采用二叉树的方式构建运算表达式,非叶子结点存储运算符,叶子结点存储数值。

5.2 运算符号:存储在一个枚举类中,包括加减乘除和括号。

5.3 运算数值:采用随机生成的方法。

5.4 分数的处理:因为java不存在可以处理分数运算的类,采用面向对象的思想,我们的处理方式是将整数与分数作为一个Fracton对象来处理,即整数就是分母为1的分数,即所有参与运算的数都可以看成是分数。

5.5 二叉树的详细说明:二叉树的节点分为两种,一种BiTreeNode(有一个Fraction类的result属性,存储当前节点以下的计算结果),存储参与运算的数字类,一种是OperatorCharNode(继承自BiTreeNode),存储运算符。

5.6 构建运算表达式: 存储在Expression类中,内含BitreeNode属性,指向一棵二叉树的根节点,中序遍历这棵二叉树,便可以转化成直观的运算表达式了,也就是中缀表达式。

5.7 括号的处理:需要加括号的情况,一个节点的操作符为乘除,其子节点的操作符是加减。

5.8 表达式的计算结果:Expression中有一个Fraction类的result属性,存储这个表达式的计算结果,result属性中含有BiTreeNode类的属性,用来存储这棵树的根节点,遍历这棵儿二叉树才能出结果。

5.9 负数的处理:由于参与运算的结果都是分数或整数,而且,分数的分子和分母都是自然数,因此,如果出现负数,则肯定是 “较小的数 – 较大的数“ 造成的。这里想到了两种处理方法,第一种是出现负数则抛异常,不要这道题目,继续循环生成题目。第二种是交换左右两棵子树,这样就变成了 “较大的数 – 减去较小的数”,就不会出现负数了,本项目采用第二种方法处理。

5.10 n/0的处理:每个表达式都设置一个是否舍弃当前表达式的标志,若出现n/0的情况,则将n/0中的0设置为1,防止其出现异常。并且将表达式设置为舍弃,后续的获取表达式时,则不放进生成的题目中。

5.11 判断重复的题目:由于乘法和加法满足交换律,因此,可以借助二叉树同构的方法,判断题目是否重复,如果非叶子节点是“*” 或者 “+”,则可以交换左右子树,如果能满足同构,则这两道题目是一样的。

5.12 采用Map的数据结构存储题目,key为表达式,value值为答案。

6.关键代码说明

6.1 运算符枚举类

package com.bichoncode.bean;

/**
 * 操作符枚举类
 * 包括 + - * / ( )
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/09
 */
public enum OperationalCharEnum {

    PlUS("+"),
    SUBTRACT("-"),
    MULTIPLY("*"),
    DIVIDE("/"),
    LEFT_BRACKETS("("),
    RIGHT_BRACKETS(")");
    // 操作字符
    private String valueChar;

    OperationalCharEnum(String valueChar) {
        this.valueChar = valueChar;
    }

    public String getValueChar() {
        return valueChar;
    }

    public void setValueChar(String valueChar) {
        this.valueChar = valueChar;
    }
}

6.2 二叉树节点类

package com.bichoncode.bean;

/**
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/10
 */
public class BiTreeNode {
    // 存储当前节点以下的计算结果
    public Fraction result;
    public BiTreeNode left;
    public BiTreeNode right;
    public int high;

    public BiTreeNode() {
    }

    public BiTreeNode(Fraction result, BiTreeNode left, BiTreeNode right, int high) {
        this.result = result;
        this.left = left;
        this.right = right;
        this.high = high;
    }

    // 打印出表达式
    @Override
    public String toString() {
        return result.toString();
    }


    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass()) return false;

        BiTreeNode node = (BiTreeNode) o;

        if (result != null ? !result.equals(node.result) : node.result != null)
            return false;
        if (right != null ? !right.equals(node.right) : node.right != null)
            return false;
        return left != null ? left.equals(node.left) : node.left == null;
    }

    // 递归
    @Override
    public int hashCode() {
        int result1 = result != null ? result.hashCode() : 0;
        result1 = 31 * result1 + (right != null ? right.hashCode() : 0);
        result1 = 31 * result1 + (left != null ? left.hashCode() : 0);
        return result1;
    }

}

6.3二叉树存储字符的节点类

package com.bichoncode.bean;


/**
 * 操作运算符节点
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/10
 */
public class OperatorCharNode extends BiTreeNode {

    // 运算符
    public String operator;

    public OperatorCharNode(BiTreeNode left, BiTreeNode right, String operator) {
        // 父类中无用的常量设置为null
        super(null, left, right, 0);
        this.operator = operator;
    }


    // 中间节点存放运算符,按需求,用空格隔开
    @Override
    public String toString() {
        return " " + operator + " ";
    }
}

6.4 分数类

package com.bichoncode.bean;

import com.bichoncode.utils.RandomUtils;

/**
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/10
 */
public class Fraction {

    // 分子
    private int numerator;
    // 分母,不能为0,默认为1
    private int denominator = 1;

    public int getNumerator() {
        return numerator;
    }

    public void setNumerator(int numerator) {
        this.numerator = numerator;
    }

    public int getDenominator() {
        return denominator;
    }

    public void setDenominator(int denominator) {
        this.denominator = denominator;
    }

    // 设置分子和分母
    public Fraction(int numerator, int denominator) {
        setFactionValue(numerator, denominator);
    }

    // 通过表达式得到分子和分母,都未经过简化,分母可能为0
    public Fraction(String result) {
        result.trim();
        int numerator_index = result.indexOf("/");
        int numerator1_index = result.indexOf("'");

        // 不是分式的时候
        if (numerator_index == -1) {
            numerator = Integer.valueOf(result);
        }
        // 是分式的时候
        else {
            // 分母
            denominator = Integer.valueOf(result.substring(numerator_index + 1));
            // 真分数
            if (numerator1_index == -1) {
                numerator = Integer.valueOf(result.substring(0, numerator_index));
            }
            // 带分数
            else {
                int numerator1 = Integer.valueOf(result.substring(0, numerator1_index));
                int numerator0 = Integer.valueOf(result.substring(numerator1_index + 1, numerator_index));
                numerator = numerator1 * denominator + numerator0;
            }
        }
        setFactionValue(numerator, denominator);
    }

    // 将分子分母调整之后,存储到成员变量中
    public void setFactionValue(int numerator, int denominator) {
        if (denominator == 0)
            throw new RuntimeException("分母不能为0");
        // 结果默认是正数
        int isNagitiveAB = 1;
        // 调整符号,b只能为正数
        if (numerator * denominator < 0) {
            isNagitiveAB = -1;
        }
        numerator = Math.abs(numerator);
        denominator = Math.abs(denominator);
        // 最大公因数
        int g = gcd(numerator, denominator);
        // 化简
        this.numerator = numerator * isNagitiveAB / g;
        this.denominator = denominator / g;

    }



    public static Fraction generateFraction() {
        // 分子分母 都是大于等于0的
        int numerator = RandomUtils.getARandom(Expression.range);
        int denominator = RandomUtils.getARandom(Expression.range);
        // 分母为0
        while (denominator == 0) {
            denominator = RandomUtils.getARandom(Expression.range);
        }
        Fraction result = new Fraction(numerator, denominator);
        return result;
    }


    // 加法
    public Fraction plus(Fraction right) {
        // a/b+c/d =(ad+bc)/bd
        return new Fraction(
                this.numerator * right.denominator + this.denominator * right.numerator,
                this.denominator * right.denominator
        );
    }

    // 减法
    public Fraction subtract(Fraction right) {
        // a/b-c/d =(ad-bc)/bd
        return new Fraction(
                this.numerator * right.denominator - this.denominator * right.numerator,
                this.denominator * right.denominator
        );
    }

    // 乘法
    public Fraction multiply(Fraction right) {
        // a/b * c/d = ac / bd
        return new Fraction(
                this.numerator * right.numerator,
                this.denominator * right.denominator
        );
    }

    // 除法
    public Fraction divide(Fraction right) {
        // a/b  /  c/d = ad / bc
        return new Fraction(
                this.numerator * right.denominator,
                this.denominator * right.numerator
        );
    }


    // 辗转相除法,求最大公约数
    private int gcd(int numerator, int denominator) {
        int big = numerator;
        if (big == 0)
            return 1;
        int small = denominator;
        //让a成为最大的
        if (numerator < denominator) {
            big = denominator;
            small = numerator;
        }
        int mod = big % small;
        return mod == 0 ? small : gcd(small, mod);
    }


    // 看当前分数是否为负数
    boolean isNegative() {
        // 结果默认是正数
        boolean isNagitiveFraction = false;
        if (numerator * denominator < 0) {
            isNagitiveFraction = true;
        }
        return isNagitiveFraction;
    }


    // 将分子分分母转化成手写形式
    @Override
    public String toString() {
        //不是分式
        if (denominator == 1)
            return String.valueOf(numerator);
            //真分式
        else {
            int i = numerator / denominator;
            //余数
            int j = numerator % denominator;

            // 分母为0则直接返回0
            if (numerator == 0) {
                return String.format("%d", 0);
            }
            if (i != 0) {
                return String.format("%d'%d/%d", i, j, denominator);
            } else {
                return String.format("%d/%d", numerator, denominator);
            }
        }
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Fraction fraction = (Fraction) o;

        if (numerator != fraction.numerator) return false;
        return denominator == fraction.denominator;
    }

    // 重写hashcode
    @Override
    public int hashCode() {
        int result = 31 * numerator + denominator;
        return result;
    }



}


6.5四则运算表达式类

package com.bichoncode.bean;


import com.bichoncode.exception.CommonException;
import com.bichoncode.utils.RandomUtils;

/**
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/10
 */
public class Expression {

    private String PLUS = OperationalCharEnum.PlUS.getValueChar();

    private String SUBTRACT = OperationalCharEnum.SUBTRACT.getValueChar();

    private String MULTIPLY = OperationalCharEnum.MULTIPLY.getValueChar();

    private String DIVIDE = OperationalCharEnum.DIVIDE.getValueChar();

    private String LEFT_BRACKETS = OperationalCharEnum.LEFT_BRACKETS.getValueChar();

    private String RIGHT_BRACKETS = OperationalCharEnum.RIGHT_BRACKETS.getValueChar();

    // 运算符的种类
    private String[] OPERATORS = {PLUS, SUBTRACT, MULTIPLY, DIVIDE};

    // 二叉树的根节点
    private BiTreeNode root;

    // 是否出现除数为0的情况
    private boolean divisorIsZero = false;

    public boolean divisorIsZero() {
        return divisorIsZero;
    }

    public void setdivisorIsZero(boolean divisorIsZero) {
        divisorIsZero = divisorIsZero;
    }

    // 生成答案的范围
    public static int range;

    public BiTreeNode getRoot() {
        return root;
    }

    public void setRoot(BiTreeNode root) {
        this.root = root;
    }

    // 生成四则运算表达式
    public Expression(int operator_number, int answer_range) {
        if (operator_number < 1) {
            throw new CommonException("运算符个数必须大于0");
        }
        if (answer_range < 1) {
            throw new RuntimeException("运算结果范围必须大于等于1");
        }
        this.range = answer_range;

        if (operator_number == 1) {
            root = generateNode(operator_number);
        } else {
            root = generateNode(RandomUtils.getARandom(operator_number) + 1);
        }
    }


    /**
     * 构建生成四则运算表达式的二叉树
     *
     * @Param number 运算符的个数
     **/
    public BiTreeNode generateNode(int number) {
        // 如果是0就构造叶子节点
        if (number == 0) {
            return new BiTreeNode(Fraction.generateFraction(), null, null, 1);
        }
        // 其他都是构造符号节点
        OperatorCharNode parent = new OperatorCharNode(null, null, OPERATORS[RandomUtils.getARandom(4)]);
        int left = RandomUtils.getARandom(number);
        // 递归下去构造左孩子和右孩子
        parent.left = generateNode(left);
        // 总数要减去当前已经构建出来的这一个节点
        parent.right = generateNode(number - 1 - left);

        // 然后计算结果
        Fraction result = calculate(parent.operator, parent.left.result, parent.right.result);
        // 如果是负数,就是出现小的减去大的情况,这时候交换左右孩子
        if (result.isNegative()) {
            BiTreeNode tmp = parent.left;
            parent.left = parent.right;
            parent.right = tmp;
        }
        // 重新计算结果
        parent.result = calculate(parent.operator, parent.left.result, parent.right.result);
        // 计算树高
        parent.high = Math.max(parent.left.high, parent.right.high) + 1;
        return parent;
    }


    // 进行两个元素的计算
    private Fraction calculate(String operator, Fraction leftFraction, Fraction rightFraction) {
        if (operator.equals(PLUS))
            return leftFraction.plus(rightFraction);
        else if (operator.equals(SUBTRACT))
            return leftFraction.subtract(rightFraction);
        else if (operator.equals(MULTIPLY))
            return leftFraction.multiply(rightFraction);
        else if (operator.equals(DIVIDE)) {
            //可能会出现除以0的情况,即rightFraction可能为0
            if (rightFraction.getNumerator() == 0) {
                this.divisorIsZero = true;
                rightFraction.setNumerator(1);
            }
            return leftFraction.divide(rightFraction);
        } else
            throw new RuntimeException("该操作符不存在");

    }

    // 打印四则运算表达式
    @Override
    public String toString() {
        return print(root);
    }

    /**
     * 中序遍历二叉树,左中右
     *
     * @Param localRootNode 当前所在的最高节点,可以不是根节点
     * @Return java.lang.String
     **/
    private String print(BiTreeNode localRootNode) {

        if (localRootNode == null) {
            return "";
        }
        String left = print(localRootNode.left);
        String mid = localRootNode.toString();
        // 需要加括号的情况,一个节点的操作符为乘除,其子节点的操作符是加减
        if (localRootNode.left instanceof OperatorCharNode && localRootNode instanceof OperatorCharNode) {
            if (leftBrackets(((OperatorCharNode) localRootNode.left).operator, ((OperatorCharNode) localRootNode).operator)) {
                left = LEFT_BRACKETS + " " + left + " " + RIGHT_BRACKETS;
            }
        }
        String right = print(localRootNode.right);
        if (localRootNode.right instanceof OperatorCharNode && localRootNode instanceof OperatorCharNode) {
            if (rightBrackets(((OperatorCharNode) localRootNode.right).operator, ((OperatorCharNode) localRootNode).operator)) {
                right = LEFT_BRACKETS + " " + right + " " + RIGHT_BRACKETS;
            }
        }
        return left + mid + right;
    }


    // 向左遍历时,需要括号
    private boolean leftBrackets(String left, String mid) {
        return (isAddOrSubtract(left) && isMultiplyOrDivide(mid));
    }

    // 向右遍历时,需要括号
    private boolean rightBrackets(String right, String mid) {
        //有可能出现2*3 /( 4*5 )的情况,所以不用加括号只有当
        return !(isAddOrSubtract(mid) && isMultiplyOrDivide(right));
    }

    /**
     * 是加减运算符
     *
     * @Param operator
     * @Return boolean
     **/
    private boolean isAddOrSubtract(String operator) {
        return operator.equals(PLUS) || operator.equals(SUBTRACT);
    }

    /**
     * 是乘除运算符
     *
     * @Param operator
     * @Return boolean
     **/
    private boolean isMultiplyOrDivide(String operator) {
        return operator.equals(MULTIPLY) || operator.equals(DIVIDE);
    }


}


6.6 异常处理类

package com.bichoncode.utils;

import com.bichoncode.bean.BiTreeNode;


/**
 * 去重工具类
 * @author ChenYan
 */
public class DistincOperatorUtils {

    /**
     * 使用递归判断两科二叉树是否同构
     * @param root1
     * @param root2
     * @return
     */
    public static boolean isomorphism(BiTreeNode root1, BiTreeNode root2) {
        if (root1 == null && root2 == null)
            return true;
        if (root1 == null && root2 != null) {
            return false;
        }
        if (root1 != null && root2 == null) {
            return false;
        }
        if (!root1.result.equals(root2.result) )
            return false;
        return isomorphism(root1.left, root2.left) && isomorphism(root1.right, root2.right)
                ||isomorphism(root1.left, root2.right) && isomorphism(root1.right, root2.left);

    }


}

6.7文件读写工具类

package com.bichoncode.utils;


import java.io.*;
import java.util.*;

/**
 * @author ChenYan
 * @create 2020/10/11
 */
public class FileUtils {

    public  static void writeTitle(PrintWriter printWriter, Map<String,String> map){
        Set<String> titles=map.keySet();
        int i=1;
        for(String title:titles){
           printWriter.println(i+":"+title);
           i++;
        }

    }


    // 将答案写入文件中
    public static void  writeAnswer(PrintWriter printWriter,Map<String,String> map){
        Set<String> answer=map.keySet();
        int i=1;
        for (String key :answer){
            String value=map.get(key);
            printWriter.println(i+":"+value);
            i++;
        }
    }




}

6.8随机数生成工具类

/**
 * @author ChenYan
 * @create 2020/10/10
 */
public class RandomUtils {
    public static int getARandom(int range){
        ThreadLocalRandom random = ThreadLocalRandom.current();
        // 产生一个包括上限不包括下陷的值
        return random.nextInt(range);
    }
}

6.9答案处理工具类

package com.bichoncode.utils;

import com.bichoncode.bean.BiTreeNode;
import com.bichoncode.exception.CommonException;
import com.bichoncode.bean.Expression;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/11
 */
public class AnswerUtils {



    // 生成题目和答案的映射关系
    public static HashMap<String, String> generateMap(int exam_number, int answer_range) {
        if (exam_number < 1) {
            throw new CommonException("生成题目的个数必须大于0");
        }
        if (answer_range < 1) {
            throw new CommonException("运算数字的范围必须大于等于1");
        }
        // 存储去重之后的题目
        HashMap<String, String> hashMap = new HashMap<>();

       // 存储刚生成的题目(此时未去重)
        HashMap<Expression, String> hashMap2 = new HashMap<>();

        while (hashMap.size() < exam_number) {
            for (int i = 1; hashMap2.size() < exam_number + 2000; ) {
                // 因为在运算的过程中会出现n/0的情况,这时候就会抛异常
                Expression expression = new Expression(3, answer_range);
                if ((hashMap.get(expression.toString()) != null || !"".equals(expression.toString()))
                        &&
                        !expression.divisorIsZero()) {
                    hashMap2.put(expression, expression.getRoot().result.toString());
                    i++;
                }
            }
            // 去重
            HashMap<Expression, String> distincMap = distinc(hashMap2);
            hashMap2.clear();
            hashMap2 = distincMap;


            for (Expression expression : distincMap.keySet()) {
                hashMap.put(expression.toString(), expression.getRoot().result.toString());
                if (hashMap.size() == exam_number) {
                    break;
                }
            }

        }

        return hashMap;
    }
    
	// 比对答案和练习题
    public static void compare(File answerFile, File exerciseFile) throws IOException {
        if (!exerciseFile.exists()) {
            throw new CommonException("练习答案文件不存在");
        }
        if (!answerFile.exists()) {
            throw new CommonException("答案文件不存在");
        }
        // key是题号,value是答案
        Map<Integer, String> exerciseMap = new HashMap<>();
        Map<Integer, String> answerMap = new HashMap<>();
        // 对比的结果
        List<Integer> rightRsult=new LinkedList<>();
        List<Integer>  errorRsult=new LinkedList<>();
        InputStreamReader exerciseIn = new InputStreamReader(new FileInputStream(exerciseFile.getAbsolutePath()), StandardCharsets.UTF_8);
        InputStreamReader answerIn = new InputStreamReader(new FileInputStream(answerFile.getAbsolutePath()), StandardCharsets.UTF_8);
        BufferedReader exerciseReader = new BufferedReader(exerciseIn);
        BufferedReader answerReader = new BufferedReader(answerIn);
        String string = null;
        // 存储的练习答案
        while ((string = exerciseReader.readLine()) != null) {
            string = string.replaceAll(" +", "");
            string = string.replaceAll("\uFEFF", "");
            String TEXT=string.split("[:]")[0];
            exerciseMap.put(Integer.valueOf(string.split("[:]")[0]), string.split(":")[1].split("=")[1]);
        }
        while ((string = answerReader.readLine()) != null) {
            string = string.replaceAll(" +", "");
            string = string.replaceAll("\uFEFF", "");
            answerMap.put(Integer.valueOf(string.split("[:]")[0]), string.split(":")[1]);
        }
        exerciseReader.close();
        answerReader.close();

        // 比较答案
        for (int i = 1; i <= answerMap.size(); i++){
            if(exerciseMap.containsKey(i)){
                if(exerciseMap.get(i).equals(answerMap.get(i))){
                    rightRsult.add(i);
                }else {
                    errorRsult.add(i);
                }
            }
        }
        // 将比较结果存储到文件中
        File file=new File("Grade.txt");
        FileWriter fileWriter = new FileWriter(file, true);
        PrintWriter printWriter = new PrintWriter(fileWriter);
        printWriter.println(" ");
        printWriter.print("Correct:正确题数:"+rightRsult.size()+"(");
        System.out.print("Correct:正确题数:"+rightRsult.size()+"(");
        for (int str: rightRsult) {
            printWriter.print(str+",");
            System.out.print(str+",");
        }
        printWriter.println(")");
        System.out.println(")");
        printWriter.print("Wrong:错误题数:"+errorRsult.size()+"(");
        System.out.print("Wrong:错误题数:"+errorRsult.size()+"(");
        for (int str: errorRsult) {
            printWriter.print(str+",");
            System.out.print(str+",");
        }
        printWriter.print(")");
        System.out.print(")");
        printWriter.flush();
        fileWriter.flush();
        printWriter.close();
        fileWriter.close();
        System.out.println("比较完成,");
    }

    /**
     * 去除重复的题目
     * @param map
     * @return
     */
    public static HashMap<Expression, String> distinc(HashMap<Expression, String> map){
        HashMap<Expression, String> distincMap = new HashMap<>();
        List<Expression> repeatList = new ArrayList<>();
        for (String key : map.values()) {
            List<Expression> keyList = getRepratKeys(map, key);
            if (keyList.size() > 1) {
                // 获取两个表达式的根节点
                BiTreeNode root1 = keyList.get(0).getRoot();
                for (int i = 1; i < keyList.size(); i++){
                    BiTreeNode root2 = keyList.get(i).getRoot();
                    boolean isomorphism = DistincOperatorUtils.isomorphism(root1, root2);
                    if (isomorphism) {
                        // 如果题目重复,则移除第二道
                        //map.remove(keyList.get(i));
                        repeatList.add(keyList.get(i));
                    }
                }

            }
        }
        for (Expression expression : map.keySet()) {
            if (!repeatList.contains(expression)) {
                distincMap.put(expression, expression.getRoot().result.toString());
            }
        }
        return distincMap;
    }


    public static List<Expression> getRepratKeys(Map<Expression, String> map, String value){
        Set set = map.entrySet(); //通过entrySet()方法把map中的每个键值对变成对应成Set集合中的一个对象
        Iterator<Map.Entry<Expression, String>> iterator = set.iterator();
        ArrayList<Expression> arrayList = new ArrayList();
        while(iterator.hasNext()){
            //Map.Entry是一种类型,指向map中的一个键值对组成的对象
            Map.Entry<Expression, String> entry = iterator.next();
            if(entry.getValue().equals(value)){
                arrayList.add(entry.getKey());
            }
        }
        return arrayList;
    }

}

6.10 main方法

package com.bichoncode;

import com.bichoncode.exception.CommonException;
import com.bichoncode.utils.FileUtils;
import com.bichoncode.utils.AnswerUtils;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Scanner;

/**
 * @author BichonCode
 * @mail chenzhichaohh@163.com
 * @create 2020/10/10
 */
public class Main {
    public static void main(String[] args) throws IOException {
        // 运算数字的范围
        int range = 0;
        // 题目数量
        int number = 0;

        System.out.println("1.生成题目请输入: -n 题目树龄 -r 题目数字的范围。例如:-n 5 -r 5");
        System.out.println("2.对照答案请输入: -e:练习题的绝对路径 -a: 答案的绝对路径。例如: -e F:exercises.txt -a F:answerfile.txt");
        System.out.println("请输入");
        Scanner sc = new Scanner(System.in);
        String string = sc.nextLine();
        args = string.split("\\s+");

        // 判断参数是否正确
        if (args.length < 4) {
            throw new CommonException("请检查参数是否正确");
        }

        // 获取参数
        for (int i = 0; i < args.length; i++) {
            if ("-n".equals(args[i])) {
                number = Integer.parseInt(args[i + 1]);
                i++;
            } else if ("-r".equals(args[i])) {
                range = Integer.parseInt(args[i + 1]);
                i++;
            } else {
                break;
            }
        }
        // 判断是否生成题目,如果不是生成题目,则是对照答案
        // range == 0 && number == 0意味着没有输入这两个参数的值,则是对照答案
        // 生成题目和答案: -n 5 -r 5
        // 对照答案:-e <exercisefile>.txt -a <answerfile>.txt

        if (range == 0 && number == 0) { // 对照答案 -e F:exercises.txt -a F:answerfile.txt
            String answerFileName;
            String execiseFileName;
            execiseFileName = args[1];
            answerFileName = args[3];

            File answerFile = new File(answerFileName);
            File exerciseFile = new File(execiseFileName);
            AnswerUtils.compare(answerFile, exerciseFile);
        } else {
            // -n 5 -r 5
            // 出题和生成答案
            HashMap<String, String> map = AnswerUtils.generateMap(number, range);
            File file = new File("F:exercises.txt");
            //File file = new File(args[1]);
            File answerFile = new File("F:answerfile.txt");
            //File answerFile = new File(args[3]);
            try {
                // 将生成的题目写入文件中
                FileWriter fileWriter = new FileWriter(file, true);
                PrintWriter printWriter = new PrintWriter(fileWriter);
                FileUtils.writeTitle(printWriter, map);
                printWriter.flush();
                fileWriter.flush();
                printWriter.close();
                fileWriter.close();
                System.out.println("题目已生成,文件路径为F:exercises.txt");
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                // 将答案写入文件中
                FileWriter fileWriter = new FileWriter(answerFile, true);
                PrintWriter printWriter = new PrintWriter(fileWriter);
                FileUtils.writeAnswer(printWriter, map);
                printWriter.flush();
                fileWriter.flush();
                printWriter.close();
                fileWriter.close();
                System.out.println("答案已生成,文件路径为F:answerfile.txt");
            } catch (IOException e) {
                e.printStackTrace();
            }


        }


    }
}

7.运行结果

7.1 测试题目的生成

生成的练习题:

生成的答案:

7.2测试答案校对

提交的练习答案:

正确答案:

校对结果:

7.3 10000道题目的生成

三、项目小结

  1. 经过这个结对项目,我们不再只是局限于原先的个人项目的各自开发,而是作为一个团队来一块开发这个项目。结对编程让我们的工作更有动力,能够集思广益减少犯错误的几率,但是对两个人的工作有顺序和时间前后的约束,容易从双线程退化成单线程。
    本次编程的分析和设计,是经历一起讨论之后,综合各自的想法,最终得出的一个设计,比自己考虑的更加周全详细。编码过程各自负责各自的模块,以及运用到git,大大提高了我们的工作的效率。测试方面,编写这部分的人负责做单元测试,然而其他成员也可以在这个基础上,进行一些黑盒测试。
    总之,通过这次结对编程项目,不光锻炼巩固了面向对象开发的流程,还体验了在一个团队中进行交流、合作进行开发。
  2. 建议分享: 结对项目中,每个人需要负责不同的模块,需要协调好分工,但是又不能仅仅负责自己的模块,还需要看懂对方的代码,双方之间可以通过CodeReview的方式审核代码。
posted @ 2020-10-12 13:47  BichonCode  阅读(370)  评论(1编辑  收藏  举报