结对项目

结对项目:四则运算题目生成器(JAVA)

一、Github项目地址(合作人:林泽鸿3118005009、陈宇3118004999)

https://github.com/linzworld/arithmetic-generate

二、项目功能介绍

  1. 使用 -n 参数控制生成题目的个数【完成】

  2. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围【完成】

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

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

  5. 每道题目中出现的运算符个数不超过3个。【完成】

  6. 程序一次运行生成的题目不能重复。【未完成】

  7. 生成的题目存入执行程序的当前目录下的Exercises.txt文件。【完成】

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

  9. 程序应能支持一万道题目的生成。【完成】

  10. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计。【完成】

三、思路介绍

  1. 使用二叉树进行构建表达式,包括生成括号

    生成四则表达式时,通常的是带有括号的表达式,我们这个项目用的是二叉树来进行运算,因为之前在四则运算中都是可以看成两个元素互相作用,加减乘除都是二元运算符,用二叉树比较合理。

  2. 全部采用分数的形式进行存储。

    在存储时,存储分数的分子和分母,若为整数,则将该整数存储到分子上,分母默认是1。

  3. 使用二叉树的结构进行是否重复的检验

    a.当两个二叉树的结构一致时,则两个表达式重复了。

    b.若判断的运算符是+或者*这种可以交换左右子表达式之后结果不变的,这时候就得通过比较第一个二叉树每个节点的左子树和第二个二叉树的右子树是否也相同,第一个二叉树每个节点的右子树和第二个二叉树的左子树是否也相同。不相同则返回不重复。

  4. 负数的处理

    项目要求结果不能出现负数,而进行运算的分式都是整数或者0,不是负数。所以负数的情况只有在小的正数减去大的正数时出现,此时则将左右子树进行互换,变成大减去小,而且这个时候是处于生成表达式的时候,没有其他影响。

  5. n/0的处理

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

  6. 使用HashMap来与队友进行合作沟通

HashMap中存放所有的表达式和答案,分工合作,一人负责生成表达式并且计算结果,另外一个人负责将得到的答案和实际作答进行对比和写入文件。

四、具体实现代码

主要类

-Expresssion.java

负责生成表达式和构建二叉树

-Fraction.java

分数类,进行存储每个分式的分子和分母,以便后续的带分数转化

-GenerateUtils.java

生成随机数和结果集合的工具类

-Node.java

存放操作数的节点

-OperatorNode.java

存放运算符的节点,该节点是Node的子类

-FileUtils.java

进行io的工具类

-Main.java

main函数

代码介绍

Expression

package com.lzh;
​
​
/**
 * @Description 四则运算表达式的生成和构建二叉树
 * @Author 林泽鸿
 * @Date 2020/4/9 17:47
 */
public class Expression {
​
    private static final String ADD = "+";
​
    private static final String SUBTRACT = "-";
​
    private static final String MULTIPLY = "×";
​
    private static final String DIVIDE = "÷";
​
    private static final String LEFT_BRACKETS = "(";
​
    private static final String RIGHT_BRACKETS = ")";
​
    //运算符的种类
    private static final String[] OPERATORS = {ADD, SUBTRACT, MULTIPLY, DIVIDE};
​
    //根节点
    private Node root;
​
    //出现n除以0的情况
    private boolean isDivideForZero = false;
​
    public boolean isDivideForZero() {
        return isDivideForZero;
    }
​
    public void setDivideForZero(boolean divideForZero) {
        isDivideForZero = divideForZero;
    }
​
    //生成答案的范围
    public static int range;
​
    public Node getRoot() {
        return root;
    }
​
    public void setRoot(Node root) {
        this.root = root;
    }
​
    //生成表达式
    public Expression(int operator_number, int answer_range) {
        if (operator_number < 1) {
            throw new RuntimeException("运算符个数必须大于0");
        }
        if (answer_range < 1) {
            throw new RuntimeException("运算结果范围必须大于等于1");
        }
        this.range = answer_range;
​
        if (operator_number == 1) {
            root = generateNode(operator_number);
        } else {
            root = generateNode(GenerateUtils.getRandomInRange(operator_number) + 1);
        }
    }
​

 


    /**
     * 构建生成四则运算表达式的二叉树
     * @Param number 运算符的个数
     * @Return com.lzh.Node 二叉树的头节点
     * @Author 林泽鸿
     * @Date 2020/4/11 17:41
     **/
    public Node generateNode(int number) {
        //如果是0就构造叶子节点
        if (number == 0) {
            return new Node(Fraction.generateFraction(), null, null, 1);
        }
        //其他都是构造符号节点
        OperatorNode parent = new OperatorNode(null, null, OPERATORS[GenerateUtils.getRandomInRange(4)]);
        int left = GenerateUtils.getRandomInRange(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()) {
            Node tmp = parent.left;
            parent.left = parent.right;
            parent.right = tmp;
        }
        parent.result = result;
        //计算树高
        parent.high = Math.max(parent.left.high, parent.right.high) + 1;
        return parent;
    }
​
​
    //进行两个元素的计算
    private Fraction calculate(String operator, Fraction leftFraction, Fraction rightFraction) {
        switch (operator) {
            case ADD:
                return leftFraction.add(rightFraction);
            case SUBTRACT:
                return leftFraction.subtract(rightFraction);
            case MULTIPLY:
                return leftFraction.multiply(rightFraction);
            //可能会出现除以0的情况,即rightFraction可能为0
            case DIVIDE:
                if (rightFraction.getA() == 0) {
                    this.isDivideForZero = true;
                    rightFraction.setA(1);
                }
                return leftFraction.divide(rightFraction);
            default:
                throw new RuntimeException("该操作符不存在");
        }
    }
​
    //打印出中缀表达式,包括括号
    @Override
    public String toString() {
        return print(root);
    }
​
    /**
     * 中序遍历二叉树,左中右
     *
     * @Param localRootNode 当前所在的最高节点,可以不是根节点
     * @Return java.lang.String
     * @Author 林泽鸿
     * @Date 2020/4/11 17:58
     **/
    private String print(Node localRootNode) {
​
        if (localRootNode == null) {
            return "";
        }
        String left = print(localRootNode.left);
        String mid = localRootNode.toString();
        //需要加括号的情况,一个节点的操作符为乘除,其子节点的操作符是加减
        if (localRootNode.left instanceof OperatorNode && localRootNode instanceof OperatorNode) {
            if (leftBrackets(((OperatorNode) localRootNode.left).operator, ((OperatorNode) localRootNode).operator)) {
                left = LEFT_BRACKETS + " " + left + " " + RIGHT_BRACKETS;
            }
        }
        String right = print(localRootNode.right);
        if (localRootNode.right instanceof OperatorNode && localRootNode instanceof OperatorNode) {
            if (rightBrackets(((OperatorNode) localRootNode.right).operator, ((OperatorNode) 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
     * @Author 林泽鸿
     * @Date 2020/4/11 18:10
     **/
    private boolean isAddOrSubtract(String operator) {
        return operator.equals(ADD) || operator.equals(SUBTRACT);
    }
​
    /**
     * 是乘除运算符
     *
     * @Param operator
     * @Return boolean
     * @Author 林泽鸿
     * @Date 2020/4/11 18:10
     **/
    private boolean isMultiplyOrDivide(String operator) {
        return operator.equals(MULTIPLY) || operator.equals(DIVIDE);
    
​
}

 

Fraction

package com.lzh;
​
import com.cy.www.numberUtils;
​
/**
 * @Description 计算数(分数),统一是叶子节点
 * @Author 林泽鸿
 * @Date 2020/4/9 22:20
 */
public class Fraction {
​
    //分子
    private int a;
    //分母,不能为0,默认为1
    private int b = 1;
​
    public int getA() {
        return a;
    }
​
    public void setA(int a) {
        this.a = a;
    }
​
    public int getB() {
        return b;
    }
​
    public void setB(int b) {
        this.b = b;
    }
​
    //设置分子和分母
    public Fraction(int a, int b) {
        setAB(a, b);
    }
​
    //通过表达式得到分子和分母,都未经过简化,分母可能为0
    public Fraction(String result) {
        result.trim();
        int a_index = result.indexOf("/");
        int a1_index = result.indexOf("'");
​
        //不是分式的时候
        if (a_index == -1) {
            a = Integer.valueOf(result);
        }
        //是分式的时候
        else {
            //分母
            b = Integer.valueOf(result.substring(a_index + 1));
            //真分数
            if (a1_index == -1) {
                a = Integer.valueOf(result.substring(0, a_index));
            }
            //带分数
            else {
                int a1 = Integer.valueOf(result.substring(0, a1_index));
                int a0 = Integer.valueOf(result.substring(a1_index + 1, a_index));
                a = a1 * b + a0;
            }
        }
        setAB(a, b);
    }
​
    //将分子分母调整之后,存储到成员变量中
    public void setAB(int a, int b) {
        if (b == 0)
            throw new RuntimeException("分母不能为0");
        //结果默认是正数
        int isNagitiveAB = 1;
        //调整符号,b只能为正数
        if (a * b < 0) {
            isNagitiveAB = -1;
        }
        a = Math.abs(a);
        b = Math.abs(b);
        //最大公因数
        int g = gcd(a, b);
        //化简
        this.a = a * isNagitiveAB / g;
        this.b = b / g;
​
    }
​
​
    /**
     * 生成一个计算数
     *
     * @Return java.lang.String
     * @Author 林泽鸿
     * @Date 2020/4/9 23:35
     **/
    public static Fraction generateFraction() {
        //a.b 都是大于等于0的
        int a = GenerateUtils.getRandomInRange(Expression.range);
        int b = GenerateUtils.getRandomInRange(Expression.range);
        //分母为0
        while (b == 0) {
            b = GenerateUtils.getRandomInRange(Expression.range);
        }
        Fraction result = new Fraction(a, b);
        return result;
    }
​
​
    //加法
    public Fraction add(Fraction right) {
        // a/b+c/d =(ad+bc)/bd
        return new Fraction(
                this.a * right.b + this.b * right.a,
                this.b * right.b
        );
    }
​
    //减法
    public Fraction subtract(Fraction right) {
        // a/b-c/d =(ad-bc)/bd
        return new Fraction(
                this.a * right.b - this.b * right.a,
                this.b * right.b
        );
    }
​
    //乘法
    public Fraction multiply(Fraction right) {
        // a/b * c/d = ac / bd
        return new Fraction(
                this.a * right.a,
                this.b * right.b
        );
    }
​
    //乘法
    public Fraction divide(Fraction right) {
        // a/b  /  c/d = ad / bc
        return new Fraction(
                this.a * right.b,
                this.b * right.a
        );
    }
​
    //
//求最大公因数,辗转相除法
    private int gcd(int a, int b) {
        int big = a;
        if (big == 0)
            return 1;
        int small = b;
        //让a成为最大的
        if (a < b) {
            big = b;
            small = a;
        }
        int mod = big % small;
        return mod == 0 ? small : gcd(small, mod);
    }
​
​
    //看当前分数是否为负数
    boolean isNegative() {
        //结果默认是正数
        boolean isNagitiveAB = false;
        if (a * b < 0) {
            isNagitiveAB = true;
        }
        return isNagitiveAB;
    }
​
​
    //将a,b转化为表达式
    @Override
    public String toString() {
        //不是分式
        if (b == 1)
            return String.valueOf(a);
            //真分式
        else {
            int i = a / b;
            //余数
            int j = a % b;
            if (i != 0) {
                return String.format("%d'%d/%d", i, j, b);
            } else {
                return String.format("%d/%d", a, b);
            }
        }
    }
​
​
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
​
        Fraction fraction = (Fraction) o;
​
        if (a != fraction.a) return false;
        return b == fraction.b;
    }
​
    //根据分子和分母
    @Override
    public int hashCode() {
        int result = 31 * a + b;
        return result;
    }
}

 

 

GenerateUtils

package com.lzh;
​
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
​
/**
 * @Description 生成四则表达式
 * @Author 林泽鸿
 * @Date 2020/4/7 18:31
 */
public class GenerateUtils {
​
    /**
     * 获得范围内的随机整数
     *
     * @Param range  范围
     * @Return int 随机数
     * @Author 林泽鸿
     * @Date 2020/4/9 22:11
     **/
    public static int getRandomInRange(int range) {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return random.nextInt(range);
    }
​
​
    //生成题目和答案的映射关系
    public static HashMap<String, String> generateMap(int exam_number, int answer_range) {
        if (exam_number < 1) {
            throw new RuntimeException("生成题目的个数必须大于0");
        }
        if (answer_range < 1) {
            throw new RuntimeException("运算结果范围必须大于等于1");
        }
        HashMap<String, String> hashMap = new HashMap<>();
​
        for (int i = 1; hashMap.size() < exam_number; ) {
            //因为在运算的过程中会出现n÷0的情况,这时候就会抛异常
            Expression expression = new Expression(3, answer_range);
            if ((hashMap.get(expression.toString()) != null || !"".equals(expression.toString()))
                    &&
                    !expression.isDivideForZero()) {
                hashMap.put(expression.toString(), expression.getRoot().result.toString());
                i++;
            }
        }
        return hashMap;
    }
}

 

 

Node

package com.lzh;
​
/**
 * @Description 定义一个存放操作数(分数)的节点
 * @Author 林泽鸿
 * @Date 2020/4/9 21:21
 */
public class Node {
    //存储当前节点以下的计算结果
    public Fraction result;
    public Node left;
    public Node right;
    public int high;
​
    public Node() {
​
    }
​
    public Node(Fraction result, Node left, Node 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;
​
        Node node = (Node) 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;
    }
​
}

 

 

OpercatorNode

package com.lzh;
​
/**
 * @Description 一个存放运算符的节点
 * @Author 林泽鸿
 * @Date 2020/4/9 21:22
 */
public class OperatorNode extends Node {
​
    //运算符
    public String operator;
​
    public OperatorNode(Node left, Node right, String operator) {
        //父类中无用的常量设置为null
        super(null, left, right, 0);
        this.operator = operator;
    }
​
​
    //中间节点存放运算符,需空格隔开
    @Override
    public String toString() {
        return " " + operator + " ";
    }
}
​
​
​
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++;
        }
    }
    
   //对比练习和答案,并将结果写入文件
    public static void compare(File answerFile,File exerciseFile) throws IOException {
        if (!exerciseFile.exists()) {
            System.out.println("练习答案文件不存在");
            return;
        }
        if (!answerFile.exists()) {
            System.out.println("答案文件不存在");
            return;
        }
        //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]);
        }
        //存储标准答案
        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()+"(");
        for (int str: rightRsult) {
            printWriter.print(str+",");
        }
        printWriter.println(")");
        printWriter.print("Wrong:错误题数:"+errorRsult.size()+"(");
        for (int str: errorRsult) {
            printWriter.print(str+",");
        }
        printWriter.print(")");
        printWriter.flush();
        fileWriter.flush();
        printWriter.close();
        fileWriter.close();
    }
​
}

 




public class Main {
    public static void main(String[] args) throws IOException {
        //数字范围大小
        int range=0;
        //题目数量
        int number=0;
        //接收参数
        while (true) {
            Scanner sc = new Scanner(System.in);
            String string = sc.nextLine();
            args = string.split("\\s+");
            //判断参数是否正确
            if (args.length < 2) {
                System.out.println("请重新输入正确的参数...");
            }
            else {
                break;
            }
        }
        //获取参数
        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;
            }
        }
        //判断是否生成题目,如果不是生成题目,则是对照答案
        //Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
        if(range==0&&number==0){
            String answerFileName;
            String execiseFileName;
            if ("-e".equals(args[0])){
                execiseFileName=args[1];
                answerFileName=args[3];
            }else {
                execiseFileName=args[3];
                answerFileName=args[1];
            }
            File answerFile=new File(answerFileName);
            File exerciseFile=new File(execiseFileName);
            FileUtils.compare(answerFile,exerciseFile);
        }
​
        else {
            HashMap<String, String> map=GenerateUtils.generateMap(number,range);
            File file=new File("Exercises.txt");
            File answerFile=new File("answerfile.txt");
            try {
                FileWriter fileWriter=new FileWriter(file,true);
                PrintWriter printWriter=new PrintWriter(fileWriter);
                FileUtils.writeTitle(printWriter,map);
                printWriter.flush();
                fileWriter.flush();
                printWriter.close();
                fileWriter.close();
            } 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();
            } catch (IOException e) {
                e.printStackTrace();
            }
​
        }
​
    }

 




 

五、测试

题目文件

 

 

 

答案文件

 

 

练习文件

 

 

对比文件

 

 

六、PSP

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

七、项目总结

在这个项目中,学到了各种表达式的知识,也对二叉树的结构有了进一步的理解。前期在对接的时候,由于写的可能是两个比较有关联性的代码,所以后续我们各自分工,同时也加快了各自的编码效率。而在实际敲代码的时候,尤其是用二叉树表示有括号的表达式时,那个地方思考了比较久,在什么时候加上括号的条件,后来我是通过画出表达式的二叉树结构,并且对运算符进行进一步的分类,最后解决了这个难题。在这个过程中我们培养了团队协作的能力和与他人交际的能力,同时也使我们的编程能力不断提高。

 

posted @ 2020-04-13 17:46  hrzgj  阅读(328)  评论(0编辑  收藏  举报