因为之后的几天会有很多的事情要忙,所以就突击把这次的作业完成了。1.0版本还比较粗糙,之后如果有空的话会再做修改。
Coding.net原码仓库地址:https://git.coding.net/ShadowGhostH/homework.git
博客更新状态
3.20 16:30 更新
我也没想到这么快就更新了,感谢有小伙伴提出题目当中要求计算过程中不能出现负数。于是我就在计算时对‘-’操作符又做了一次特判,出现‘-‘时就计算之前表达式的值breNum,然后对减数在[1, breNum] 的范围内生成就可以了。同时需要注意,为了不使减数放大,我们要控制减号后的一个操作符不乘号,要不然会导致减数放大而出现负数
2.0 End 有空继续更新
3.25 18:00 更新
因为天梯赛和周训的原因,一直也没再有机会来做这个项目,所以附加内容没有完成。之前为了引导同学思路留下了输入引导和各种注释,在deadline之前把输入引导去掉换成了命令行读入,last更新。
End
4.16 17:21 更新
很开心ACM校赛拿了一等奖第一名,同时上次更新写的last更新好像也成了一个flag。拖到了deadline最后三小时才提交作业确实有些拖沓了,但是准备了这么久的校赛拿了不错的成绩还是特别开心。
本次更新将可生成的算式个数更新至1000(数组能开多大就能生成多少个,而且采用读取的sum值动态开数组,所以原则上不存在上限(上限和能开的数组范围有关)),并将result.txt文件的生成位置更改到与src文件夹同级目录下。
3.0 End 希望是最后一次更新
需求分析
首先明确项目需要,一个可以定做小学四则运算的程序。所以需要我们能够随机生成不同的四则运算,范围应在100以内,包含“+,-,*,÷”四种操作。要求:不存在小数,题目尽量不重复。
功能设计
基本功能应当包括,读入用户想要定制的算式数,以及对应的运算符数量,然后生成对应数目和难度的四则运算式子。并由程序自己计算,给出相应的答案。
扩展功能可以用:在算式中包含括号,计算存在真分数的式子。
设计实现
因为要随机生成运算数与运算符,所以我们可以用Math.random()生成随机数来定制我们的运算数,并可以事先将运算符“+,-,*,÷”存入数组内,然后通过Math.random()方法随机访问数组下标的方式,来随机确定运算符。
生成四则运算的式子后,要去由计算机计算这个表达式的值,因为计算机无法直观的判断表达式中各个运算的优先级关系,所以我们选择将中序表达式转化为后序表达式的方式来计算,又因为不需要输出后序表达式,所以我们可以在转化的过程中直接完成计算。
算法详解
1.定制运算数与运算符
/** 用于生成指定范围的随机数 **/ static private int makeRandom(int min, int max){ return (int)(Math.random()*(max - min) + min + 0.5); }
首先在Lib类中,我选择了这样一个方法,通过Math.random()方法,生成一个 [min, max] 范围中的数,在我们之后的运算过程中,会对不同范围的随机数有不同的要求,届时会多次调用这个方法。
2.生成随机随机的运算式
/** 一共生成sum个,ops个操作的算式 **/ static public void makeQuestions(String[] questionList, int sum, int ops){ /** 用于存放操作符 和 操作数 **/ char opInQuestion[] = new char[15]; int numInQuestion[] = new int[15]; /** for循环生成sum个question **/ for(int i=0; i<sum; i++){ numInQuestion[0] = makeRandom(1, 100); /** 每个问题预生成第一个操作数,然后for循环对应生成之后的ops个操作 和对应的操作数 **/ for(int j=0; j<ops; j++){ /** 乘除不连续出现避免出错 **/ if(j!=0 && getw(opInQuestion[j-1])==2) opInQuestion[j] = op[makeRandom(0, 1)]; else opInQuestion[j] = op[makeRandom(0, 3)]; numInQuestion[j+1] = makeRandom(1, 50); /** 特判除法构造整除 **/ if(opInQuestion[j] == '÷') { numInQuestion[j+1] = makeRandom(1, 10); numInQuestion[j] = numInQuestion[j+1]*makeRandom(2, 10); } /** 特判乘法控制范围 **/ else if(opInQuestion[j] == '*') { numInQuestion[j] = makeRandom(1, 20); numInQuestion[j+1] = makeRandom(1, 100/numInQuestion[j]); } } /** 将问题拼接为String **/ String question = "" + numInQuestion[0]; for(int j=0; j<ops; j++) question = question + opInQuestion[j] + numInQuestion[j+1]; //System.out.println(question); question = question + "=" + calQuestion(question); System.out.println(question); questionList[i] = question; } }
makeQuestions() 方法中共有三个参数,questionList[] 用于储存生成的运算式,sum为定制运算式的个数,ops为定制运算式中操作符的个数。
在实现的过程中,opInQuestion[] 数组用于顺序储存运算式中出现的操作符,numInQuestion[] 用于顺序储存运算式中出现的操作数。因为每个式子中至少有一个数字,所以我们可以预生成numInQuestion[0], 然后循环生成其余的操作数和操作符。
根据题目要求不能出现小数,所以我们可以在操作符出现除法操作时,特判除号前后的两个数字,并构造他们让他们成为倍数关系。即,先随机生成除数,然后随机生成倍数,通过除数和倍数来构造被除数。
最后将所有的操作数和操作符拼接成一个字符串存入questionList[] 中,问题生成完成。
以上是我最开始的想法,后来在调试过程中我发现自己的思路是存在漏洞的:因为每次会定制除号前后的两个数,所以当出现连续除法操作时,第一个除数会被覆盖从而第一个除法运算将不满足整除。而如果我不定制被除数的话,又不能保证被除数是不是素数(除1这个操作实在是太蠢了),所以我偷懒选择了不让除法连续出现。而同时考虑到连续乘法可能会让数据过大不适合小学生计算,所以有对乘法进行了特判来调整最后结果可能的大小不至于太大。
3.计算表达式的值
/** 将question转化为后缀表达式并求值 **/ static private int calQuestion(String question){ char[] ch = new char[50]; char[] oc = new char[15]; int[] num = new int[15]; int temp = 0, pn = -1, pc = -1; //temp用于存放中间数值,pn用于在num数组中模拟栈顶, pc用于在ch数组中模拟栈顶 ch = question.toCharArray(); for(int i=0; i<ch.length; i++){ if(Character.isDigit(ch[i])) { temp = temp*10 + ch[i]-'0'; if(i == ch.length-1) num[++pn] = temp; //最后一个数入栈 } else { num[++pn] = temp; //temp入栈 temp = 0; while(pc!=-1 && getw(oc[pc]) >= getw(ch[i]) ) { int num1 = num[pn--]; int num2 = num[pn--]; char ch1 = oc[pc--]; num[++pn] = calc(num2, num1, ch1); } //if(pc == -1) oc[++pc] = ch[i]; oc[++pc] = ch[i]; } }
中序表达式转后序表达式算是比较基础的栈的应用,考虑到项目并不需要输出后序表达式,所以在转后序表达式的过程中直接完成计算操作。又因为JAVA不像C++有stl容器(也可能是因为我菜所以不知道), 所以选择了用数组模拟栈的方法。
顺序读入字符串中的每个字符,如果是数字就计算其代表的值,如果是操作符就将计算的数字入栈(数字栈 num[]), 然后如果字符栈(oc[] )为空,就将操作符入栈。如果不为空,则将字符栈中优先级大于等于当前操作符的字符弹出并计算。当扫完整个数组后,将最后一个操作数入栈,并顺序计算字符栈中的每个操作符,最后数字栈顶的元素即为运算表达式的结果。
其中调用了两个方法,分别完成一步运作操作和判断运算符优先级,代码如下。
/** 完成一步运算 **/ static private int getw(char c){ if(c=='*' || c=='÷') return 2; else if(c=='+' || c=='-') return 1; else return -1; } /** 判断运算优先级 **/ static private int calc(int a, int b, char c) { //System.out.println("" + a +c + b); if(c == '+') return a + b; else if(c == '-') return a - b; else if(c == '*') return a * b; else if(c == '÷') return a / b; else return -1; }
4.输出运算及结果到文件中
/** 将question中的sum个问题,输出到path文件中 **/ static public void filePrint(String[] questionList, int sum, String path) throws IOException{ FileOutputStream fs = new FileOutputStream(new File(path)); PrintStream p = new PrintStream(fs); p.println("2016012096"); for(int i=0; i<sum; i++) p.println(questionList[i]); p.close(); }
用到了文件输出流,并且优先输出我自己的学号
5.主函数的写法及相关方法的调用
public class Main { public static void main(String args[]) { Scanner cin = new Scanner(System.in); String[] questionList = new String[105]; String path = "result.txt"; int sum, ops; System.out.println("您好,欢迎使用四则运算定制系统!"); System.out.println("请输入您想定制的四则运算式子个数(0~100):"); sum = cin.nextInt(); System.out.println("请输入您想定制的每个算式的操作符个数(0~10):"); ops = cin.nextInt(); Lib.makeQuestions(questionList, sum, ops); try { Lib.filePrint(questionList, sum, path); } catch(IOException ioe) { ioe.printStackTrace(); } cin.close(); } }
完成了相关功能的实现,只需要在main函数中进行调用,做出输入引导,并且catch一下异常就好。
测试运行
在项目的开发过程中采用了输出中间值以及自定义testPrint()方法的方法进行了调试,并发现了一两个代码中的BUG,并在项目完成后针对边界问题做了测试。
5个算式,每个算式3个操作符。
0个算式0个操作符,只输出我自己的学号
100个10操作符的有点多截不完整,不过是成功了的。
总结
在这次项目中,通过事先对项目的分析设计,省去了不少时间,在制作的过程中不至于手忙脚乱。因为不想放deadline上仓促完成,所以提前赶出来了,但是由于时间紧张所以功能方面还是有些粗糙,之后可以再做优化。
同时在这次项目中暴露出来了对JAVA应用的不熟练,这个项目我用C++先码出来一个大致然后边百度边转化成JAVA然后再去debug的。之后可以对JAVA再进行复习和探究。
“模块化”规则确实让我在这次的作业中收益良多,将功能分块实现,不仅可以让代码逻辑更加清晰,在实现过程中按部就班地梳理实现,还可以在调试阶段针对出现的问题进行合理有效地排查。提高了效率。
展示PSP
非常抱歉我在完成的时候没有注意到还需要展示自己的PSP,所以也就没有一个记录。很遗憾不能分享给大家,但是我也不想去杜撰一个时间表出来来对大家造成误解,真的十分抱歉以及遗憾。
不过根据我的亲身感受,我在整个项目开发过程中,花在计划的部分比想象中稍多,一部分原因是用琐碎时间在思考,一部分原因是我个人一开始对用JAVA实现毫无头绪所以动手慢了一些。而在开发方面我的用时比想象中要少,原因应该是之前合理的计划和模块化的实现让整个代码的逻辑更加清晰,也确确实实提高了效率,这个方法真的很好,不是在写代码的同时构思着之后每一步要怎么做,而是清楚地知道现在正在写的内容会在之后产生什么样的作用和效果,于是就在第一次写的时候就为之后做好了铺垫,模块间更好的交互以及不用往返做重用功,似的开发的时间比想象中少了很多。而在测试的时间上确实如课上所讲,我花在测试上的时间比较少,可能是不知道具体应该怎么做测试的原因吧,之后会通过学习了解更多。
1.0 End 有改进再更新。