2016012032小学四则运算练习软件项目报告

Coding.net源码仓库地址:https://git.coding.net/wanghz499/2016012032week2-2.git

测试步骤:

1.进入src文件夹

2.在命令行输入javac -encoding utf-8 Main.java

3.回车再输入java Main 20

4.回车,将会在根目录下(与src同级)产生result.txt

一、需求分析

    通过对题目要求的分析,我共提取出以下7个需求(实现带括号和真分数的附加功能):

    1.程序可从命令行接收一个输入参数n,然后随机产生n道加减乘除练习题。

    2.每个数字在 0 和 100 之间,运算符在3个到5个之间。

    3.每个练习题至少要包含2种运算符。

    4.所出的练习题在运算过程中不得出现负数与非整数。

    5.将学号与生成的n道练习题及其对应的正确答案输出到文件“result.txt”中。

    6.支持有括号的运算式,包括出题与求解正确答案。算式中存在的括号必须大于2个,且不得超过运算符的个数。

    7.支持真分数的加减法,并且每个分数都化到最简

二、功能设计

    能够根据用户输入的参数n随机产生n道符合要求的练习题,自动算出答案,并将式子与答案以文档的形式呈现。并实现附加功能:支持有括号的运算、支持真分数的加减运算。

三、设计实现

    我共设计了5个类,如图:

                   

    Creat类:负责随机产生一条带括号的至少2种运算符四则运算的式子,且有3-5个运算符

    Calculator类:负责筛选运算过程中不产生负数和小数的式子,并计算答案

    MakeFile类:负责产生result.txt文件,并将学号和产生的练习题写入文件

    properFraction类:负责产生真分数式子并计算答案

    Main类:主类,负责接收命令行的参数并启动程序

   5个类的相互调用关系为: 

             

    比较重要的函数:

        Creat类:creatProblem():随机产生一条带括号的含3-5个运算符四则运算式子,

                                若式子不符合条件,会递归直到产生符合条件的式子。

                 index(int n):产生运算符下标数组,并保证至少有2个不同的运算符。

        Calculator类:

                 algorithm(String s):结合了调度场算法和逆波兰表达式的求值,计算出式子的答案。

                 calculate(int a,int b,String stmp):计算式子每一部分的运算,排除运算过程中出现小数和负数的式子。

        ProperFraction类:

                 createProblem():随机产生一条含3-5个运算符的真分数加减运算式子,并计算出结果。

                 greatFactor(int x,int y):求最大公因数,用于化简

    函数间的逻辑关系:creatProblem()调用index(int n)algorithm(String s)algorithm(String s)调用calculate(int a,int b,String stmp),还有生成文件相关的方法就不列举了。

四、算法详解

    本项目的关键在于Calculator类的计算,结合了调度场算法和逆波兰表达式(即后缀表达式)的求值,一步实现计算四则运算式子。

    关于调度场算法和逆波兰表达式求值,我花费了大量时间浏览博客理解它的实现过程,现总结如下:

    调度场算法的作用是将中缀表达式变为后缀表达式,它需要一个队列来装后缀表达式和一个栈来装符号。先从左到右遍历中缀表达式的每个符号和数字,若是数字就入队;若是符号,则判断其与栈顶符号的优先级,若该符号是右括号或其优先级低于或等于栈顶符号,则栈顶元素依次出栈并输出进入队列,并将当前符号进栈,一直到最终输出后缀表达式。

    逆波兰表达式求值步骤(只需要一个栈):

    1.先初始化一个空栈,开始遍历后缀表达式。

    2.如果字符是一个操作数,则令其入栈。

    3.如果字符是个运算符,则弹出栈里的两个操作数(一定会有两个数在栈里,因为是后缀表达式),进行运算,再把结果入栈。

    4.到后缀表达式末尾,从栈中弹出结果。

   

    理解了这两个算法后,就可将两个算法结合,只需两个栈就可一次性求出答案:

    1.初始化两个栈,分别是数字栈和符号栈。

    2.遍历中缀表达式,如果是数字,则入数字栈。

    3.如果是符号,则判断其与符号栈的栈顶符号的优先级。若当前符号是右括号或其优先级低于或等于栈顶符号,则栈顶元素依次出符号栈,并在数字栈弹出两个数进行相应运算,再使结果入数字栈,当前符号也入符号栈。

    4.当遇到等号,则将符号栈里的符号依次出栈,从数字栈弹出两个数进行相应运算,再把结果入数字栈,直到最后一个符号出栈。把数字栈的数字弹出,即为结果。

   

    至于符号的优先级,则使用Hashmap<String,int>建立多组键值对,使每个符号对应一个数值,数值越高说明优先级越高。

    优先级从小到大:)小于 + - 小于 ×÷ 小于 (

五、测试运行

    进入src文件夹,在命令行输入javac -encoding utf-8 Main.java 将类编译成class文件,再输入java Main 20 运行class文件,这里先做一个非法输入和越界测试,如输入java Main e或java Main 1200

  

    再正常输入如java Main 20,将会在根目录下(与src同级)产生result.txt文件:

   

    

          

    测试完成!

六、代码展示

    产生整数式子的方法:

public static String createProblem(){
        Random random = new Random();
        String[] operator = {"+","-","×","÷"};

        int operatorCount = 3+random.nextInt(3); //操作符的个数3-5
        int[] num = new int[operatorCount+1]; //操作数的个数比操作符多1
        int[] index = index(operatorCount); //操作符的下标
        String s = new String();

        for(int j=0;j<operatorCount+1;j++){
            num[j] = random.nextInt(101); //产生0-100范围的操作数,random.nextInt(n)的取值范围是[0,n)
        }


        int choose = random.nextInt(2); //选择式子括号形态

        switch (operatorCount){
            case 3:{
                if(choose==0){
                    s=num[0]+operator[index[0]]+"("+"("+num[1]+operator[index[1]]+num[2]+")"+operator[index[2]]+num[3]+")";//1+((2×3)-4)型
                }else s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×(3+4)型
                break;
            }

            case 4:{
                if(choose==0){
                    s="("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+num[4]+operator[index[3]]+"("+num[2]+operator[index[2]]+num[3]+")";//(1+2)×3÷(4-1)型
                }else s=num[4]+operator[index[3]]+"("+num[0]+operator[index[0]]+num[1]+")"+operator[index[1]]+"("+num[2]+operator[index[2]]+num[3]+")";//3×(1+2)+(4÷2)型
                break;
            }

            case 5:{
                if(choose==0){
                    s="("+num[0]+operator[index[0]]+num[1]+operator[index[4]]+num[5]+")"+operator[index[1]]+"("+num[4]+operator[index[3]]+num[2]+")"+operator[index[2]]+num[3];//(6+2×3)-(1+2)×3型
                }else s="("+num[0]+operator[index[0]]+"("+num[1]+operator[index[1]]+num[2]+operator[index[2]]+num[3]+")"+")"+operator[index[3]]+"("+num[4]+operator[index[4]]+num[5]+")";//(1+(2×3+4))-(6÷3)型
                break;
            }
        }


        s+="="; //给式子加上等号
        int answer = Calculator.calculate(s);

        if(answer>=0){ //判断式子是否符合要求,凡是返回负数的就是不合格的
            s+=answer;
        }else {
            return createProblem(); //递归,直到产生合格的式子
        }

        return s;

    }

 

    保证式子里至少有2个不同操作符的方法:

    private static int[] index(int n,int m){ //产生操作符的下标数组
        Random random = new Random();
        int similar=0;
        int[] a = new int[n];
        for(int j=0;j<n;j++){
            a[j] = random.nextInt(m);
        }
        for(int j=1;j<n;j++){
            if(a[0]==a[j]) similar++;
        }
        if(similar==n-1) return index(n); //保证一个式子里至少有2个不同的操作符,若所有操作符下标都一样,则重新产生操作符下标
        else {
            return a;
        }

    }

   

    产生真分数式子并计算的方法:

public String createProblem(){
        Random random = new Random();
        String[] operator = {"+","-"};
        int operatorCount = 3+random.nextInt(3); //操作符的个数3-5

        Create create = new Create();
        int[] index = create.index(operatorCount,2); //操作符的下标

        int sumx = 1+random.nextInt(10); //第一个数的分子1-10
        int sumy = 1+random.nextInt(10);//第一个数的分母1-10
        int greatFactor = greatFactor(sumx,sumy);
        sumx/=greatFactor; //化简
        sumy/=greatFactor;

        while (sumx>=sumy){
            sumx = 1+random.nextInt(10);
            sumy = 1+random.nextInt(10);
            greatFactor = greatFactor(sumx,sumy);
            sumx/=greatFactor;
            sumy/=greatFactor;
        }

        String s=sumx+"/"+sumy; //第一个数

        for(int i=0;i<operatorCount;i++){
            int numx = random.nextInt(25); //分子分母不宜过大
            int numy = 1+random.nextInt(25); //否则通分可能会产生很大的数导致溢出
            String currentOpreator = operator[index[i]];
            while (numx>=numy){ //当分子大于分母,即假分数,则重新生成
                numx = random.nextInt(25);
                numy = 1+random.nextInt(25);
                greatFactor = greatFactor(numx,numy);
                numx/=greatFactor;
                numy/=greatFactor;
            }
            if(currentOpreator.equals("+")){  //加法
                while(sumx*numy+sumy*numx>sumy*numy) //和为假分数
                {
                    numx=random.nextInt(25);
                    numy=1+random.nextInt(25);
                    greatFactor=greatFactor(numx,numy);
                    numx/=greatFactor;
                    numy/=greatFactor;
                }
                sumx=sumx*numy+sumy*numx;
                sumy=sumy*numy;
            }
            else {   //减法
                while(sumx*numy-sumy*numx<0) //差为负数
                {
                    numx=random.nextInt(25);
                    numy=1+random.nextInt(25);
                    greatFactor=greatFactor(numx,numy);
                    numx/=greatFactor;
                    numy/=greatFactor;
                }
                sumx=sumx*numy-sumy*numx;
                sumy=sumy*numy;
            }
            s+=currentOpreator+numx+"/"+numy;
        }

        greatFactor = greatFactor(sumx,sumy);
        sumx/=greatFactor; //最终结果化简
        sumy/=greatFactor;

        if(sumx==0) s+="="+sumx;
        else if(sumx==1&&sumy==1) s+="="+sumx;
        else s+="="+sumx+"/"+sumy;

        return s;

    }

    判断非法输入和越界输入的方法(主方法):

public static void main(String[] args) {
        int n = 0;
        try {
             n = Integer.parseInt(args[0]);
            if(n>1000||n<1){
                System.out.println("对不起,只允许输入1-1000的数字!");
                return; //结束运行
            }
        }catch (NumberFormatException e){ //输入非数字字符等
            System.out.println("对不起,只允许输入1-1000的数字!");
            return; //结束运行
        }

        MakeFile.creatFile(n);

    }

    其他代码请见coding.net,就不一一展示了。

六、PSP

SP2.1

任务内容

计划共完成需要的时间(h)

实际完成需要的时间(h)

Planning

计划

26

47

·        Estimate

·   估计这个任务需要多少时间,并规划大致工作步骤

26

47

Development

开发

20

40

·        Analysis

·         需求分析 (包括学习新技术)

3

5

·        Design Spec

·         生成设计文档

0

0

·        Design Review

·         设计复审 (和同事审核设计文档)

0

0

·        Coding Standard

·         代码规范 (为目前的开发制定合适的规范)

0

0

·        Design

·         具体设计

3

5

·        Coding

·         具体编码

10

15

·        Code Review

·         代码复审

2

5

·        Test

·         测试(自我测试,修改代码,提交修改)

2

10

Reporting

报告

6

7

·         Test Report

·         测试报告

5

6

·         Size Measurement

·         计算工作量

0.5

0.5

·         Postmortem & Process Improvement Plan

·         事后总结, 并提出过程改进计划

0.5

0.5

七、总结

    这次项目比我想象中的要难,原以为一两天就可以搞定,实际上花了整整4天时间在图书馆钻研。其实做这个项目我并没有完整地按照软件开发的步骤,也没有提前设计代码,想到哪就写到哪,导致敲代码的过程中遇到很多小问题,一遇到问题就得停下来去找相应的解决方法,经常是代码写了又删,删了又写,效率比较低,下次一定要事先设计,想好整个流程再写代码。此外,通过这次作业,我感受到了算法在项目中的重要性,算法是一个项目的灵魂。就如调度场算法和逆波兰表达式求值算法是这次作业的核心,我再也不敢说类似于不知算法有什么用之类的话了。

    原本我是没有实现分数加减的附加功能的,因为我潜意识里觉得它很难,所以压根没想过也不敢做这个附加功能。但是看了其他同学的博客后,发现很多同学都实现了,我分析他们的代码,突然觉得也不是很难了,几经思考后我最终也实现了分数的功能,开心不已。我想写博客的意义就在此吧,互相分享自己的学习成果,共同进步。很感谢那些愿意写博客分享技术的人,从他们的博客中真的可以学到很多东西!

    这4天,看了一篇又一篇博客,改了一段又一段代码,很疲惫却也很充实,让我感受到了全身心投入一件事情的快乐,专注与钻研,我喜欢这样的感觉。同时,也让我意识到自己的水平远比想象中的低,做一个四则运算就让我费了这么大的劲,说明我的水平真的还不够,我要好好努力。最后,还是忍不住分享独自完成一个小项目的喜悦,真的很开心,也给了我很大的鼓励!专注的感觉真好!

原文地址:https://www.cnblogs.com/hiwhz/p/8620687.html