项目地址:https://git.dev.tencent.com/sunjj942/sunjj942Homework1.git
1、实验目的与要求
(1)掌握软件项目个人开发流程。
(2)掌握Coding.net上发布软件项目的操作方法。
2、实验内容和步骤
任务1:
使用JAVA编程语言,独立完成一个3到5个运算符的四则运算练习的软件。
软件基本功能要求如下:
- 程序可接收一个输入参数n,然后随机产生n道加减乘除(分别使用符号+-*÷来表示)练习题,每个数字在 0 和 100 之间,运算符在3个到5个之间。
- 每个练习题至少要包含2种运算符。同时,由于小学生没有分数与负数的概念,你所出的练习题在运算过程中不得出现负数与非整数,比如不能出 3÷5+2=2.6,2-5+10=7等算式。
- 练习题生成好后,将你的学号与生成的n道练习题及其对应的正确答案输出到文件“result.txt”中,不要输出额外信息,文件目录与程序目录一致。
- 当程序接收的参数为4时,以下为一个输出文件示例。
2018010203
13+17-1=29
11*15-5=160
3+10+4-16=1
15÷5+3-2=4
软件附加功能要求如下:(请有余力的同学完成)
- 支持有括号的运算式,包括出题与求解正确答案。注意,算式中存在的括号数必须大于2对,且不得超过运算符的个数。
- 扩展程序功能支持真分数的出题与运算(只需要涵盖加减法即可),例如:1/6 + 1/8 + 2/3= 23/24。注意在实现本功能时,需支持运算时分数的自动化简,比如 1/2+1/6=2/3,而非4/6,且计算过程中与结果都须为真分数。
-
在看到这个项目作业的时候,一开始觉得很简单,估计就是限定一些条件来进行判断,但是当实际编写代码的时候,却发现有非常非常多的问题,其次,由于对java知识的生疏,很多知识都是现看的,还有就是一些基本的算法,也需要自己在学习一下,就像中缀表达式变后缀表达式,根据后缀表达式计算结果。
首先是我项目中类和类中的方法:
public class Process{
public void Arithmetic(int num) //执行运算,这个方法是Main方法直接调用的函数
}
public class Fraction {
public void Fraction() //这个方法是执行分数运算的一个方法,分数运算跟整数运算没有一起处理
}
public class ProduceExpression {
public LinkedList<String> expression()//这个方法是产生运算式的方法,同时产生的是中缀表达式形式
}
public class ChangeToPost {
public int changeToPostfixExpression(LinkedList<String> statements, int n, String[] ss)
//这个方法是将中缀表达式转换成前缀表达式,并在此方法中调用postCalculate()完成计算
public int postCalculate(int n, String[] ss)//这个方法是对后缀表达式进行计算
}
public class Util {
public static int gcd(int a, int b) //求最大公因数,此处是非递归实现,相当于是辗转相除
public int exactDivision(int x, int y) //递归的方式,解决整除的问题
public int priority(String symbol) //乘法除法跟加减法和括号的优先级
}
public class ArithmeticNeed {
public static int[] produceOperation() //产生运算符,并限制种类大于2种
public int caculate(int num1, int num2, String operator)//传进两个数,进行加减乘除的运算
public boolean isOperator(String operator)//判断是否是运算符
}
下面是类和类之间的调用关系结构图:
下面是编译器下面的项目的结构图:
其中result.txt生成在项目的根目录下面,还有result.txt中生成的式子:
同时经过了编译,这个项目可以在命令行下面运行。截图如下:
由于在src下面运行命令行程序的,因此在src下面也生成了一个result.txt文件,通过命令行运行的都会生成在src下面,但是通过IDEA编译器生成的result.txt会输入到项目的根目录下面。
在编写项目的过程中,遇到了很多的问题,
1.在随机生成运算符的时候,我本来是想要直接生成加减乘除,但是尝试了很多次,利用了数组,但是跟数字放在一起,两个数组就不好控制运算符的位置,于是我最终就采用了先是随机生成一个数字,这个数字对应着运算符数组的下角标,然后这个问题就一步步解决了。
2.在计算分数的方法中,我对分数的约分本来是,对每一个分数都进行处理的,这样就要处理至少三个数,于是我就在util中定义的一个gcd方法,计算出公因子,就减少了代码量。
3.处理括号问题其实整的我挺崩溃的,这个差不多花了一天的时间(哈哈,当然是有水分的啦),我先是尝试用数组解决(貌似我对数组情有独钟啊),我先是以对的形式进行往数组里面放,然后尝试定义swich,左右括号依次插入数组,试了这些方法反正都不行,最后采用额外开了两个数组(一个左括号数组,一个右括号数组),来记录左右括号的位置。。虽然括号的事情解决了,但是新的问题又来了,有些括号括住的是非常没有必要的,比如只括住单独的一个数字,这也是我在运行多次以后发现的问题,,之后又对括号的位置加以限制,让如果括号生成的位置,如果左括号在一个数字前,而这个有括号在这个数字后,就重新循环一次(i--);但是事情没有这么简单~又出了这样类似的问题,思考了一下,问题出在两对括号的位置可能会有重叠,从而又出现了括住单独一个数字的情况。又对此进行限制,才使得问题得以解决。
4.这是一个小的教训:我之前所有的代码都是定义在一个类里面的,再代码写的差不多的时候,我就进行将这个类里面的方法放到不同的类里面,由于没有实现进行代码备份,自己将这些方法分开的时候,出了大问题,就是整式的结果一直出不来。
5.运行结束以后,出现的结果会存在错误的情况甚至会在编译器的控制台报错的情况,对比了运算式很久,发现是整除有问题,又一步步在考虑哪里整除没有考虑到,,最后意识到是括号的问题,因为括号的存在,可能会使括号内的结果与外面进行整除的时候出现运算上的问题。
修改后的代码如下:
expression.add(String.valueOf(operator1[operatorNum[i]]));
if (operatorNum[i] == 3) {//3代表着除号
number[i + 1] = util.exactDivision(number[i], number[i + 1]);
//重新判断是因为在加上括号以后,整体的运算可能会存在无法整除的情况。因此需要二次进行判断
}
}
6.我本来是将中缀变后缀,根据后缀进行运算这两个方法放在不同的类里面,但是会出不来结果的情况,,在修改的时候,我进行在一些循环和方法里面加入system.out.println("=="),来判断有没有运行。解决办法,后缀表达式没有传入,,放在一个类下面,在定义一个StringBuilder 存放式子,解决了问题。
7.......................(好多好多问题)
PSP2.1 | 任务内容 | 计划共完成需要的时间(h) | 实际完成需要的时间(h) |
Planning | 计划 |
| less than0.5 |
Estimate | 估计这个任务需要多少时间,并规划大致工作步骤 |
| less than0.5 |
Development | 开发 | 24 | 43 |
Analysis | 需求分析 (包括学习新技术) | 6 | 8 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) |
| (编写的时候就注意了) |
Design | 具体设计 |
| 3 |
Coding | 具体编码 |
| 13 |
Code Review | 代码复审 |
| 4 |
Test | 测试(自我测试,修改代码,提交修改) |
| 15 |
Reporting | 报告 |
|
|
Size Measurement | 计算工作量 |
|
|
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 |
| 2 |
| 写博客 |
| 5 |
部分代码展示:
import java.util.LinkedList;
import java.util.Random;
public class ProduceExpression {
Util util = new Util();
public LinkedList<String> expression() {
char[] operator1 = new char[]{'+', '-', '*', '÷'};
Random random = new Random();
LinkedList<String> expression = new LinkedList<String>();
int[] operatorNum = ArithmeticNeed.produceOperation(); //产生的运算符的个数
int[] number = new int[operatorNum.length + 1]; //运算数的个数,该数组存储运算数
for (int i = 0; i <= operatorNum.length; i++) {
number[i] = random.nextInt(100) + 1; //4-5个数字,产生数字
}
int bracketNum = random.nextInt(operatorNum.length - 1);//随机产生括号的个数,题目限定括号的个数小于运算符的个数
if (bracketNum > 0) {//当随机产生的括号数大于零时,接下来就是产生括号了
int[] leftBracketLocation = new int[bracketNum];
int[] rightBracketLocation = new int[bracketNum];
int[] leftNum = new int[operatorNum.length + 1];//记录每个数前左括号的数目
int[] rightNum = new int[operatorNum.length + 1];//记录每个数前右括号的数目
for (int i = 0; i < bracketNum; i++) {
leftBracketLocation[i] = random.nextInt(operatorNum.length);//随机产生左括号的位置,此括号在第几个运算数的前面,运算数不包括最后一个数
rightBracketLocation[i] = random.nextInt(operatorNum.length) + 1;//随机产生右括号的位置,此右括号在第几个运算数后面,运算数不包括第一个数
if (rightBracketLocation[i] <= leftBracketLocation[i]) {
i--;//若右括号的位置在左括号前面或者左右括号只括住一个数字,则去掉本次产生的括号
}
}
for (int i = 0; i < bracketNum; i++) {//记录每个运算数对应的括号的数目
leftNum[leftBracketLocation[i]]++;
rightNum[rightBracketLocation[i]]++;
}
for (int i = 0; i < operatorNum.length + 1; i++) {//再次进行限制
if (!(leftNum[i] == 0 || rightNum[i] == 0)) {
while (!(leftNum[i] == 0 || rightNum[i] == 0)) {
leftNum[i]--;
rightNum[i]--;
}
}
}
int right = 0;//记录加进式子的右括号数目
int left = 0;//记录加进式子的左括号数目
for (int i = 0; i < operatorNum.length; i++) {
for (int j = 0; j < leftNum[i]; j++) {
expression.add("(");
left++;
}
expression.add(String.valueOf(number[i]));
for (int k = 0; k < rightNum[i]; k++) {
expression.add(")");
right++;
}
expression.add(String.valueOf(operator1[operatorNum[i]]));
if (operatorNum[i] == 3) {
number[i + 1] = util.exactDivision(number[i], number[i + 1]);//重新判断是因为在加上括号以后,整体的运算就变了。因此需要二次进行判断
}
}
expression.add(String.valueOf(number[operatorNum.length]));
for (int i = right; i < left; i++) {
expression.add(")");
}
} else {//此时没有括号
for (int i = 0; i < operatorNum.length; i++) {
expression.add(String.valueOf(number[i]));
expression.add(String.valueOf(operator1[operatorNum[i]]));
if (operatorNum[i] == 3) {
number[i + 1] = util.exactDivision(number[i], number[i + 1]);
}
}
expression.add(String.valueOf(number[operatorNum.length]));
}
return expression;
}
}
import java.util.Iterator;
import java.util.LinkedList;
public class ChangeToPost {
ArithmeticNeed arithmeticNeed = new ArithmeticNeed();
Util util = new Util();
// PostCaculate postCaculate = new PostCaculate();
//用于记录操作符的栈
public LinkedList<String> operators = new LinkedList<String>();
//用于记录输出的栈
private LinkedList<String> output = new LinkedList<String>();
//用于展示后缀表达式
private StringBuilder stringBuilder = new StringBuilder();
//中缀表达式转化为后缀表达式,并且调用后缀表达式计算出整式式子的结果
public int changeToPostfixExpression(LinkedList<String> statements, int n, String[] ss) {
Iterator<String> iterator = statements.iterator();
while (iterator.hasNext()) {
/*System.out.println("------------------------------------st1");*/
String string1 = iterator.next();
if (arithmeticNeed.isOperator(string1)) {//当迭代器的第一个是运算符时
if (operators.isEmpty()) {//当储存运算符的栈为空时
operators.push(string1);
} else {
if ((!string1.equals(")")) && util.priority(operators.peek()) < util.priority(string1)) {//如果读入的操作符不是“)”而且栈顶元素小于要判断的运算符的优先级
operators.push(string1);//将这个运算符入栈
} else if ((!string1.equals(")")) && util.priority(operators.peek()) >= util.priority(string1)) {//如果读入的操作符不是“)”,而且栈顶元素大于等于要判断的运算符的优先级
while ((!operators.isEmpty()) &&util.priority(operators.peek()) >= util.priority(string1) && (!operators.peek().equals("("))) {
if (!operators.peek().equals("(")) {
String operator = operators.pop();
stringBuilder.append(operator).append(" ");
output.push(operator);
}
}
operators.push(string1);
}
//如果读入的操作符是“)”,则弹出从栈顶开始第一个“(”及其之前的所有操作符
else if (string1.equals(")")) {
while (!operators.peek().equals("(")) {
String operator = operators.pop();
stringBuilder.append(operator).append(" ");
output.push(operator);
}
operators.pop();//弹出“(”
}
}
} else {
//读入的为非操作符
stringBuilder.append(string1).append(" ");
output.push(string1);
}
/*System.out.println("-----------------------------end");*/
}
if (!operators.isEmpty()) {
Iterator<String> iterator1 = operators.iterator();
while (iterator1.hasNext()) {
String oprator = iterator1.next();
stringBuilder.append(oprator).append(" ");
output.push(oprator);
iterator1.remove();
}
}
// for (int i =0;i<ss.length;i++){
// System.out.println(ss[i]);
// }
int m =postCalculate(n, ss);
// System.out.println(m);
stringBuilder.delete(0, stringBuilder.length());//进行清空一下
return m;
}
//根据后缀表达式计算表达式
public int postCalculate(int n, String[] ss) {
LinkedList<String> list = new LinkedList<>();
String[] middle = stringBuilder.toString().split(" ");
for (String s : middle) {
if (arithmeticNeed.isOperator(s)) {
if (!list.isEmpty()) {
int num1 = Integer.valueOf(list.pop());
int num2 = Integer.valueOf(list.pop());
if (s.equals("÷") && (num1 == 0 || num2 % num1 != 0)) {//除数不能为零或者不能整除的时候
n--;
return n;
}
if (s.equals("-") && num2 < num1) {//存在负数产生的情况
n--;
return n;
}
int newNum = arithmeticNeed.caculate(num2, num1, s);
// System.out.println(newNum);
// System.out.println("============");
list.push(String.valueOf(newNum));//转换成字符串以后添加进list中
}
} else {//如果不是运算符的时候
// System.out.println(s);
list.push(s);//将数字压入栈中
}
// int result = Integer.valueOf(list.pop());
// System.out.println(result);
// if (result>100){
// n--;
// }
}
String content = "";
if (!list.isEmpty()) {
for (int i = 0; i < ss.length; i++) {
content += ss[i];
}
int result = Integer.valueOf(list.pop());
// System.out.println(result);
if (result>100){
n--;
// System.out.println(n);
// System.out.println("junjie加油");
}else {
String jieguo = String.valueOf(result);
content += "=" + jieguo;
System.out.println(content);
}
}
// System.out.println(content);
return n;
}
}
public class Fraction {
Util util = new Util();
//接下来实现分数的出题以及运算,分子:numerator (二)分母:denominator
public void Fraction() {
String[] op2 = {"+", "-"};// 存储连接分数的操作符的数组
int denominator = 1;//分子
int numerator = 1;//分母
int denominator1 = (int) (Math.random() * 9) + 1;// 生成分母
int numerator1 = (int) (Math.random() * 10);// 生成分子
if (numerator1 != 0) {
if (numerator1 > denominator1) {// 如果分子大于分母,也就是不是真分数时,交换分子分母,使其变成真分数
int temp = numerator1;//如果分子大于分母,也就是不是真分数是,交换分子分母,使其变成真分数
numerator1 = denominator1;
denominator1 = temp;
}
if (numerator1 == denominator1) {// 如果分子刚好等于分母,重新生成分子,如果分子正好等于分母,则重新生成分子
numerator1 = (int) (Math.random() * 10);
}
int gcd1 = util.gcd(numerator1, denominator1);// 求分子分母最大公因数,保证分数形式最简
denominator1 = denominator1 / gcd1;// 化简
numerator1 = numerator1 / gcd1;// 化简
}
String question1 = numerator1 + "/" + denominator1;// 存储题目
int count = (int) (Math.random() * 2) + 3;// 随机产生运算符的数目
for (int u = 0; u < count; u++) {// 小于运算符数量时不断产生分数,不断计算
int denominator2 = (int) (Math.random() * 9) + 1;// 生成分母
int numerator2 = (int) (Math.random() * 10);// 生成分子
if (numerator2 != 0) {
if (numerator2 > denominator2) {// 避免不是真分数
int temp = numerator2;
numerator2 = denominator2;
denominator2 = temp;
}
if (numerator2 == denominator2) {// 如果分子等于分母,重新生成分子
numerator2 = (int) (Math.random() * 10);
}
int gcd2 = util.gcd(numerator2, denominator2);// 化简分式,使其最简
denominator2 = denominator2 / gcd2;
numerator2 = numerator2 / gcd2;
}
int symbol = (int) (Math.random() * 2);// 随机产生运算符
if (op2[symbol].equals("+")) {// 如果是加号,实现分数加法
if (denominator1 == denominator2) {// 如果两个分母相同,直接将分子相加
numerator = numerator1 + numerator2;
} else {// 通分,相加
denominator = denominator1 * denominator2;
numerator = numerator1 * denominator2 + numerator2 * denominator1;
}
if (denominator < numerator) {// 如果运算结果不是真分数
u--;// 计数的u减一,也就是重新生成重新计算
} else {// 在给定范围内的话,通分运算结果
int gcd = util.gcd(numerator, denominator);
denominator = denominator / gcd;
numerator = numerator / gcd;
if (denominator>100){
u--;
}else {
question1 += op2[symbol] + numerator2 + "/" + denominator2;// 把题目进行完善
denominator1 = denominator;// 储存运算结果到denominator1和numerator1
numerator1 = numerator;
}
}
} else {// 如果是减号,实现减法操作
if (denominator1 == denominator2) {// 分母相同直接分子相减
numerator = numerator1 - numerator2;
} else {// 其他情况,先通分再相减
denominator = denominator1 * denominator2;
numerator = numerator1 * denominator2 - numerator2 * denominator1;
}
if (numerator < 0) {// 如果导致结果小于0了,就重新生成
u--;
} else {// 通分结果化简
int gcd = util.gcd(numerator, denominator);
denominator = denominator / gcd;
numerator = numerator / gcd;
if (denominator>100){
u--;
}else {
question1 += op2[symbol] + numerator2 + "/" + denominator2;
denominator1 = denominator;// 储存通分结果
numerator1 = numerator;
}
}
}
}
System.out.println(question1 + " = " + numerator + "/" + denominator);// 输出题目和答案
}
}
总结反思:通过这次作业,让我充分认识到了自己基础知识(java语言)和基本算法的薄弱和欠缺,但是经过一周多的查漏补缺,这个四则运算的项目已经按照作业要求完成,也增添了两个附加功能,自己的附加功能就自己测试而言还是有一处小问题的,就是括号括住的可能是乘除法,也就是像(2*3)+3-8=1 这样的式子,但是实际上这样的括号是没有必要假的,我本来想要尝试使用运算符的优先级来判断的,但是由于我括号插入的方式是根据运算数字进行确定的,所以自己想要改没有改成功,最后也就放弃了这类式子的判断,有一点点小小的遗憾,希望自己以后可以实现这类式子的判断。加油加油!