一.需求分析与功能设计
二.设计实现
三.算法详解及代码展示
四.测试运行
五.遇到问题及解决
六.不足与改进
七.项目中遇到的Java小知识点
八.PSP
九.总结
十.参考资料
文本编辑器:notepad++
编译器:jdk1.8.0
项目地址:https://git.dev.tencent.com/hey_wuqw/fourArithmetic1.git
一.需求分析与功能设计
使用JAVA编程语言,完成一个3到5个运算符的四则运算练习的软件。
基本功能:
1.输入一个参数n,随机产生n道加减乘除运算(符号为+-*÷)
2.每个参加运算的数字在0~100间
3.每个运算的运算符个数为3~5个
4.每个运算至少包含2种运算符
5.运算过程中不得出现负数与非整数
6.将我的学号与生成的n道练习题及其对应答案输出到文件“result.txt”,文件目录与程序目录一致
附加功能:
1.支持有括号的运算式,包括出题与求解正确答案。注意,算式中存在的括号数必须大于2对,且不得超过运算符的个数。
2.扩展程序功能支持真分数的出题与运算(只需要涵盖加减法即可),例如:1/6 + 1/8 + 2/3= 23/24。注意在实现本功能时,需支持运算时分数的自动化简,比如 1/2+1/6=2/3,而非4/6,且计算过程中与结果都须为真分数。
二.设计实现
Main类:
main():接收命令行参数n,调用创建含四则运算练习题的文件的函数;
Lib类:
createRe(int n):创建能输出练习题的文件;
createExp(int m):生成有m个运算符的简单四则运算;
bracket(int m):生成有m个运算符的带括号的四则运算;
fraction(int m):生成有m个运算符的真分数运算;
toInfixExpression(String s):将字符串转换为中序表达式;
toReversePolishNotation(List infix):将中缀表达式转换为逆波兰(后缀)表达式;
calc(List rpn):通过逆波兰表达式计算结果;
toOp(int numofOp):将运算符的替代数字转换为运算符(0--”+”,1--”-”,2--”*”,3--”÷”);
Priority(String operation):比较运算符的优先级。
三.算法详解及代码展示
核心:
在生成简单四则运算和带括号的四则运算中我都采用了将中缀表达式转换为后缀(逆波兰)表达式,再通过后缀表达式计算运算结果的方法
1.将中缀表达式转换为后缀表达式:
初始化两个栈s1,s2,分别存放运算符和操作数;从左往右遍历中缀表达式,如果遇到运算符,则放入s1中,如果遇到数字,则放入s2中。运算符存入s1中的规则:
(1)栈为空时或栈顶元素为“(”时,直接入栈
(2)待压栈元素为普通运算符,则比较优先级:
①待压栈元素优先级大于s1栈顶元素优先级,待压栈元素入栈;
②否则,s1栈顶元素出栈并压入s2,后待压栈元素再压入s1;
(3)待压栈元素为“)”,依次弹出s1栈顶元素,并压入s2,直至遇到“(”, 再将这一对括号丢弃;
(4)无待压栈元素时,将s1中所有元素依次弹出并压入s2。
栈s2的逆序(由栈底到栈顶)即为后缀表达式。
public static List<String> toReversePolishNotation(List<String> s) {
//List<String> ls = Arrays.asList(s.split(""));
Stack<String> s1 = new Stack<String>();//初始化一个栈s1用于存放运算符
Stack<String> s2 = new Stack<String>();//初始化一个栈s2用于存放 ??操作数??中间结果
//List<String> rpn = new ArrayList<String>;//
for (String str:s) {
if (str.matches("\\d+")) {
s2.push(str);
} else if (str.equals("(")) {
s1.push(str);
} else if (str.equals(")")) {
while (!s1.peek().equals("(")) {
s2.push(s1.pop());
}
s1.pop();
} else {
while (s1.size()!=0 && priority(s1.peek()) >= priority(str)) {//当s1不为空,且栈顶优先级不低于运算符优先级,则将s1栈顶运算符弹出并压入s2
s2.push(s1.pop());
}
s1.push(str);//否则,直接入栈
}
}
while (s1.size()!=0) {//将s1中的剩余运算符弹出压入s2直至s1空了为止
s2.push(s1.pop());
}
return s2;
}
2.通过后缀表达式计算四则运算的结果:
初始化一个栈s,扫描上述生成的后缀表达式,遇到数字则压入栈s,遇到运算符则弹出栈顶的两个数字,并用该运算符进行计算,计算后的结果再压入栈s中,循环此操作得到最终结果。
Tips:在该方法中实现运算过程中不得出现负数与非整数的要求,还需注意除数不能为0
public static int calc(List<String> rpn) {
Stack<String> s = new Stack<String>();
for (String str:rpn) {
if (str.matches("\\d+")) {
s.push(str);
} else {
int result = 0;
int a = Integer.parseInt(s.pop());
int b = Integer.parseInt(s.pop());
switch (str) {
case "+": {
result = a + b;
break;
}
case "-": {
if (b - a < 0)
return -1;
else
result = b - a; //产生负数就不合格
break;
}
case "*": {
result = a * b;
break;
}
case "÷": {
if(a==0)//除数不为0
return -1;
else if(b%a!=0) //产生小数就不合格
return -1;
else
result = b / a;
break;
}
}
s.push("" + result); //""+result???
}
}
return Integer.parseInt(s.pop());
}
其他:
(一)生成简单四则运算的方法:(此方法借鉴了陈芳学姐的实现方法,但该方法存在了一些局限性,改善方法及原博地址都会在后面的不足与改进和参考资料处给出)
public static String createExp(int m) {
int[] op = new int[m];//存放有m个运算符的数组
int[] num = new int[m+1];//存放操作数的数组,操作数数目比运算符多1,为m+1
int[] exp = new int[m+m+1];//存放四则运算的数组
String str = "";
int calcResult = -2;
do{
for (int i = 0; i < m+1; i++) {
num[i] = (int)(Math.random()*101);
}
for (int i = 0; i < m;i++) {
op[i] = (int)(Math.random()*4);
}
//保证至少2种运算符
if (m == 3) {
while (op[0] == op[1] && op[1] == op[2]) {
op[2] = (int)(Math.random() * 4);
}
}
if (m == 4) {
while (op[0] == op[1] && op[1] == op[2] && op[2] == op[3]) {
op[3] = (int)(Math.random() * 4);
}
}
if (m == 5) {
while (op[0] == op[1] && op[1] == op[2] && op[2] == op[3] && op[3] == op[4]) {
op[4] = (int)(Math.random() * 4);
}
}
//将运算符数组和操作符数组交错排列生成运算数组
exp[0] = num[0];
exp[1] = op[0];
int i;
for (i = 2; i < m+m+1; i++) {
if (i%2 == 0) {
exp[i] = num[i/2];
}
if (i%2 != 0) {
exp[i] = op[i/2];
}
}
//把生成的数组转为字符串且把数字转换为运算符
str = String.valueOf(exp[0]) + toOp(exp[1]);
for (int j = 2; j < exp.length; j++) {
if (j%2 == 0) {
str += String.valueOf(exp[j]);
}
if (j%2 != 0) {
str += toOp(exp[j]);
}
}
calcResult = calc(toReversePolishNotation(toInfixExpression(str)));
str = str + "=" + String.valueOf(calcResult);
}while(calcResult < 0);
return str;
}
(二)生成带括号的四则运算的方法:(该方法的实现的思想借鉴了庄莉学姐按概率生成括号的方法:以一定概率生成左括号,当左括号数大于右括号数时,再以一定概率生成右括号,然后补全右括号)
public static String bracket(int m) {
int calcResult = -1;
String s = "";
while (calcResult == -1) {
int brack_left = 0; // 记录未匹配的左括号个数
int brack = 0; // 括号个数
int mark = 0;//标记式子最开头是否有左括号
int op;
int[] opa = new int[m];
String s2 = "";
int[] num = new int[m+1];
for (int j = 0; j < m+1; j++) {
num[j] = (int) (Math.random() * 101); // 数字
}
int i;
for (i = 0; i < (m - 1); i++) { // 循环生成算式
if (((brack * 2) <= m) && (((int) (Math.random() * 2)) == 0)) { // 以一定概率生成左括号,概率为1/2
s2 += "(";
mark = 1;
brack++;
brack_left++;
s2 += num[i];
op = (int) (Math.random() * 4);
opa[i] = op;
s2 += toOp(op);
i++;
}
s2 += num[i];
int tmp = brack_left;
for (int j = 0; j < tmp; j++) { // 若当前有未匹配的左括号,则对每一个未匹配的左括号,都有一定概率生成相应右括号。
if ((int) (Math.random() * 5) > 1) { // 生成右括号概率为0.6
brack_left--;
s2 += ")";
}
}
op = (int) (Math.random() * 4); // 生成运算符
opa[i] = op;
if (i == m) {
s2 += toOp(makeOp(m,opa));
} else {
s2 += toOp(op);
}
}
while (i != m) { // 判断是否为最后一个数
s2 += num[i];
if(mark == 1){//当式子最前面有一个左括号时,为了避免(只能降低概率)出现式子最前和最后有一对括号的无效括号
while ((brack_left) != 0) { // 补全右括号
s2 += ")";
brack_left--;
}
}
op = (int) (Math.random() * 4);
opa[i] = op;
if (i == m) {
s2 += toOp(makeOp(m,opa));
} else {
s2 += toOp(op);
}
i++;
}
s2 += num[m];
while ((brack_left) != 0) { // 补全右括号
s2 += ")";
brack_left--;
}
calcResult = calc(toReversePolishNotation(toInfixExpression(s2)));
s2 = s2 + "=" + String.valueOf(calcResult);
s = s2;
}
return s;
}
(三)生成真分数运算的方法:(该功能还未按要求完全实现,目前只能保证参与运算的数字都为真分数和运算时的自动化简,尚不能保证预算过程和结果都是真分数)
public static String fraction(int m) {
String s = "";
int cf = 0;
int numerator = 0;
int denominator = 0;
int numerator1 = 0;
int denominator1 = 0;
int a = -1;
int b = 0;
String re = "";
int[] op = new int[m];
while (numerator >= denominator || denominator == 0){//分母不为0且为真分数
numerator = (int) (Math.random()*101);
denominator = (int) (Math.random()*101);
}
s = numerator + "/" + denominator;
for (int i = 0; i < m; i++) {
op[i] = (int) (Math.random()*2);
if (op[i] == 0) {
while (numerator1 >= denominator1 || denominator1 == 0) {//分母不为0且为真分数
while (a >= b) {//防止运算过程或结果产生非真分数
numerator1 = (int) (Math.random()*101);
denominator1 = (int) (Math.random()*101);
a = numerator * denominator1 + numerator1 * denominator;
b = denominator * denominator1;
}
}
s = s + toOp(op[i]) + numerator1 + "/" + denominator1;
//System.out.println("denominator的值是:"+denominator);
//System.out.println("denominator1的值是:"+denominator1);
//System.out.println("b的值是:"+cf);
cf = getCf(a,b);
//System.out.println("cf的值是:"+cf);
numerator = a / cf;
denominator = b / cf;
//re = re + a + "/" +b;
}
else {
while (numerator1 >= denominator1 || denominator1 == 0) {//分母不为0且为真分数
while (a >= b || a < 0) {//防止运算过程或结果产生非真分数和负数
numerator1 = (int) (Math.random()*101);
denominator1 = (int) (Math.random()*101);
a = numerator * denominator1 - numerator1 * denominator;
b = denominator * denominator1;
}
}
s = s + toOp(op[i]) + numerator1 + "/" + denominator1;
cf = getCf(a,b);
numerator = a / cf;
denominator = b / cf;
//re = re + a + "/" +b;
}
}
s = s + "=" + numerator + "/" + denominator;
return s;
}
Tips:真分数的计算过程中的化简分子分母调用的求公因数的方法是来自于童宇欣学姐的博客(递归实现)
(四)将数字转运算符和比较运算符的优先级:
public static String toOp(int numofOp) {
String s = "";
switch (numofOp) {
case 0:
s = "+";
break;
case 1:
s = "-";
break;
case 2:
s = "*";
break;
case 3:
s = "÷";
break;
}
return s;
}
public static int priority(String operation){
int re;
switch (operation){
case "+":
re=1;
break;
case "-":
re=1;
break;
case "*":
re=2;
break;
case "÷":
re=2;
break;
default:
// System.out.println("不存在该运算符");
re=0;
}
return re;
}
四.测试运行
五.遇到问题及解决
1.逆波兰计算结果时明明当弹出的两数相减小于0时就返回函数并重新生成式子,怎么还出现负数了呢?——弹出的两数a和b计算顺序颠倒了,应先弹出的数(a)作为减数,后弹出的数(b)作为减数(即b-a)。除法也同理。
2.bracket(int m)方法中把要返回的变量s2 定义在while循环里出现如下结果:
——把变量s2定义在while循环外,如此一来,每次循环变成在上次返回参数s2的基础上进行操作,出现如下结果(命令行停住了):
——解决:定义一个变量s在while循环外,每次循环要结束时,把循环内的变量s2赋值给s,于是乎每次循环都重新初始化s2,最终返回参数为s。
3.刚开始生成带括号的运算中有一个局限:式子的最开头出现括号的几率为0。
——解决:调换代码顺序,先以一定概率生成括号,再生成数。
——又出现问题:存在了无效括号(式子最前后最后有一对括号)。
——解决:初始化一个变量(int mark = 0)用来标记是否式子最前头有左括号, 若有则mark = 1;然后在判断是否是最后一个数时加入。
if(mark == 1){//当式子最前面有一个左括号时,为了避免(只能降低概率)出现式子最前和最后有一对括号的无效括号
while ((brack_left) != 0) { // 补全右括号
s2 += ")";
brack_left--;
}
}
——但这个措施只能降低生成无效括号的概率,并不能完全解决这个问题。
4.生成真分数运算时把循环顺序颠倒造成无限循环
错误代码如下:
while (a > b) {//防止运算过程或结果产生非真分数
while (numerator1 >= denominator1 || denominator1 == 0) {//分母不为0且为真分数
numerator1 = (int) (Math.random()*101);
denominator1 = (int) (Math.random()*101);
a = numerator * denominator1 + numerator1 * denominator;
b = denominator * denominator1;
}
}
这样第二重循环永远无法进行从而导致了外层循环无限,正确顺序应为:
while (numerator1 >= denominator1 || denominator1 == 0) {//分母不为0且为真分数
while (a > b) {//防止运算过程或结果产生非真分数
numerator1 = (int) (Math.random()*101);
denominator1 = (int) (Math.random()*101);
a = numerator * denominator1 + numerator1 * denominator;
b = denominator * denominator1;
}
}
六.不足与改进
1.在生成简单四则运算方法中保证一个式子中至少2种运算符时,用的方法有局限性,当参与运算的数很多时则该方法不合理。
——可采用变量标记(int same = 0)的方法,即对运算符数组进行遍历,若op[0] == op[j],则same++,然后当same的值与运算符的数目减一后相等时,则重新生成运算符。故而在生成带括号的运算方法中调用了下面的方法:
//保证至少2种运算符
public static int makeOp(int m,int[] opa) {
int same = 0;
for (int j = 1; j < m; j++) {
if (opa[0] == opa[j]) {
same++;
}
}
if (same == m-1) {
return makeOp(m,opa);
} else {
return opa[m-1];
}
}
七.项目中遇到的java小知识点
1.int转换为String:
第①种方法:String str = “” + i;
第②种方法:String.valueOf(i);
第③种方法:Integer.toString(i);
2.接收命令行参数(参数为整数): Integer.parseInt(args[0]);
3.复习了将字符串转为List集合的方法:
public static List<String> toInfixExpression(String s) {
List<String> ls = new ArrayList<String>();//存储中序表达式
int i = 0;
String str = "";
for (i = 0; i < s.length(); i++) {
if(Character.isDigit(s.charAt(i))){//判断指定字符是否是一个数字
str = str + s.charAt(i);
}else{
if(str != ""){//当字符不是数字且字符串str不为空时就向集合列表加入str
ls.add(str);
}
ls.add(s.charAt(i) + "");
str = "";
}
}
if(str != ""){
ls.add(str);
}
return ls;
}
八.PSP
PSP2.1 | 任务内容 | 计划共完成需要的时间(min) | 实际完成需要的时间(min) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
· Estimate | · 估计这个任务需要多少时间,并规划大致工作步骤 | 20 | 20 |
Development | 开发 | 750 | 2490 |
· Analysis | · 需求分析 (包括学习新技术) | 100 | 120 |
· Design Spec | · 生成设计文档 | 20 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 0 | 0 |
· Design | · 具体设计 | 60 | 90 |
· Coding | · 具体编码 | 120 | 120 |
· Code Review | · 代码复审 | 30 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 420 | 2100 |
Reporting | 报告 | 70 | 85 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 45 | 60 |
九.总结
我开始准备写这个项目是在某晚的选修课上,课上到一半拿起了小本本开始写写画画设计功能模块(我没有按照规范的软件工程步骤来)。一开始的思路是想分几个类,不同的类实现不同的不同类型的功能,后来没有把包建好,类之间的功能调用出了点问题,于是乎我把所有的功能放到同一个类中,一个功能一个方法,通过静态方法之间的调用来完成。正式开始是在周六早上,一头栽进图书馆开始做项目。本以为我把功能设计得差不多了,编代码应该挺快的,奈何我的代码实践经验不足,动手能力不好,跟挤牙膏似的编代码。
我先把每个模块(方法)定义好,再去补充每个方法具体实现方法。有的方法实现起来没什么头绪,去阅读了学长学姐的博客,吸取了上届学长学姐的项目经验,再结合自己的思路与理解去做或优化。比如计算整数四则运算结果用的逆波兰表达式就是在看了学姐的博客后认识到可以用这种方法去解,然后就去看相关的博客进行算法学习。
我觉得分享博客不论是对自己还是他人都是很有意义的事,在写博客的过程中我能重新把项目思路理一遍,从项目中学习到的知识再巩固一遍,还能清晰的看到项目存在的问题和自己存在的问题,同时还能训练一下文字能力,感觉好处贼多,虽然花费的时间挺多的,可能是因为第一次做不熟练,但想想有这么多的益处也挺值。
十.参考资料
算术表达式的前缀,中缀,后缀:
https://www.cnblogs.com/james111/p/7027820.html
https://www.cnblogs.com/hellosce/p/4917699.html
https://www.cnblogs.com/chensongxian/p/7059802.html
学长学姐的博客:
https://www.cnblogs.com/maomao-miao/p/8641912.html (按概率生成括号)
https://www.cnblogs.com/hiwhz/p/8620687.html (保证式子里至少两个不同的操作符)
https://www.cnblogs.com/zmbeijixing/p/8612795.html (求两数最大公因数)