本项目报告结构一览:
一.前言
二.需求分析
三.功能设计
四.设计实现
五.算法详解(代码展示部分包含在其中)
六.测试运行
七.不足与改进
八.项目总结
九.PSP
十.后记
代码仓库地址:https://git.dev.tencent.com/czx826187190/CZX_caculate.git
一、前言
大家好,很感谢你能在百忙之中点开这篇博客。我在本博客中分享了逆波兰表达式的学习,构建有括号、分数的运算式,实现四则运算等功能。如果你对此有兴趣,我们就一起继续向下看吧!
二、需求分析
1、实验目的与要求
(1)掌握软件项目个人开发流程。
(2)掌握Coding.net上发布软件项目的操作方法。
2、实验内容和步骤
任务:
使用JAVA编程语言,独立完成一个3到5个运算符的四则运算练习的软件。
软件基本功能要求如下:
· 程序可接收一个输入参数n,然后随机产生n道加减乘除(分别使用符号+-*÷来表示)练习题,每个数字在 0 和 100 之间,运算符在3个到5个之间。
· 每个练习题至少要包含2种运算符。同时,由于小学生没有分数与负数的概念,你所出的练习题在运算过程中不得出现负数与非整数,比如不能出 3÷5+2=2.6,2-5+10=7等算式。
· 练习题生成好后,将你的学号与生成的n道练习题及其对应的正确答案输出到文件“result.txt”中,不要输出额外信息,文件目录与程序目录一致。
· 当程序接收的参数为4时,以下为一个输出文件示例。
********
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,且计算过程中与结果都须为真分数。
三、功能设计
四、设计实现
1.整数四则运算:生成一个运算符的运算式子,再连接成多个运算符的式子,然后通过符号分解出数与操作符,构建逆波兰表达式,最后计算得答案。
2.分数加减运算:通过生成两个随机数,一个做分子,一个做分母(保证分子小于分母才生成)。然后计算,分母相乘,分子加减,得到结果,求最大公约数进行化简。
五、算法详解
String[] op = { "+", "-", "*", "÷" };// 加减乘除的操作集
String[] op1 = { "+", "*" };// 连接两个简单等式时的操作符
int flag = 0;// 标记简单等式产生减号或加号的情况,也就是括号产生的情况
int ifsame=0;//判断是不是符号都相同
int answer=0;//判断结果是否为负数
1.生成3-5个运算符:可以通过生成一个运算符的基本式子,通过连接,构造多个运算符的式子
int a = (int) (Math.random() * 100);// 产生100以内的随机数
int b = (int) (Math.random() * 100);
int c = (int) (Math.random() * 4);// 产生整数0到3
if (c == 1) {// 如果是“-”,保证a比b大,避免出现负数
if (a < b) {
int temp = a;
a = b;
b = temp;
}
}
if (c == 3) {// 如果是除法,保证能整除
b = (int) (Math.random() * 20) + 1;
a = (int) (Math.random() * 6) * b;
}
String m = a + op[c] + b;
if (op[c].equals("-") || op[c].equals("+")) {// 减号时使flag等于1
flag = 1;
}
return m;
2.生成括号:可以根据优先级来区分是否加括号
String question = "";
for (int i = 0; i < p; i++) {
if (i < p - 1) {
String con = op1[(int) (Math.random() * 2)];
if (con.equals("*")) {
String com = MakeQuestion1();
if (flag == 1) {//如果前面是加减后面是乘除的话,加括号
question += "(" + com + ")" + con;
} else {
question += com + con;//生成一个最简运算式子,和符号
}
} else {
question += MakeQuestion1() + con;
}
} else {
question += MakeQuestion1();//最后一次只需生成一个最简运算式子,不需要再生成符号
}
flag = 0;//每次生成完把falg清0
}
return question;
3.逆波兰表达式计算答案:通过栈,将运算式转换为后缀表达式,并计算出答案
实现后缀表达式的一篇博客:https://www.cnblogs.com/luoxn28/archive/2017/02/12/6391050.html
后缀表达式的实现:是数字进入数字栈,操作符进入操作栈中,但是出现优先级不同时需要注意,入栈操作优先级更高的需要将操作栈中优先级低的取出放入数字栈中;如果出现括号,右括号与左括号之间的需要进入数字栈中,括号抵消掉。然后将操作栈全部取出并按顺序放入数字栈中,取出后逆序排列就是后缀表达式(可以通过数组存储,再倒序输出)。最后的计算就按照后缀表达式计算,遇见符号就计算取出的两个数,直至算完所有数。
for (int j = -1; j < len - 1; j++) {//把题目字符串进行拆分,拆分出数字和运算符,分别进行存储
if (question.charAt(j + 1) == '+' || question.charAt(j + 1) == '-' || question.charAt(j + 1) == '*'
|| question.charAt(j + 1) == '÷' || question.charAt(j + 1) == '(' || question.charAt(j + 1) == ')'
|| j == len - 2) {
if (j == -1) {//如果第一个就是运算符,即左括号,存储在操作符栈中
operate.push(question.charAt(0));
} else if (j == len - 2) {//如果到字符串的最后了,直接存储到数字栈中
if (question.charAt(len - 1) == ')') {
number.push(question.substring(k, len - 1));
number.push(String.valueOf(operate.pop()));
if (!operate.empty()) {
operate.pop();
}
} else {
number.push(question.substring(k));
}
break;
} else {
if (k <= j) {
number.push(question.substring(k, j + 1));//是数字的话存储到数字这个栈中
}
if (operate.empty() || question.charAt(j + 1) == '(') {//操作符栈为空或者接下来的符号是左括号的话都直接存储到操作符栈中
operate.push(question.charAt(j + 1));
} else if ((operate.peek() == '+' || operate.peek() == '-')
&& (question.charAt(j + 1) == '*' || question.charAt(j + 1) == '÷')) {
operate.push(question.charAt(j + 1));//如果将要放入栈中的运算符优先级比栈顶元素高,直接入栈
} else if (operate.peek() == '(') {//栈顶是左括号的话,下一个操作符也直接入栈
operate.push(question.charAt(j + 1));
} else if (question.charAt(j + 1) == ')') {//下一个操作符是右括号的话,弹出操作符栈顶元素并压入数字栈中
number.push(String.valueOf(operate.pop()));
if (!operate.empty()) {
operate.pop();
}
} else {//操作符是同等优先级的时候,把栈顶元素弹出压入数字栈中,并把下一个操作符压入操作符栈中
if(operate.peek()==question.charAt(j + 1)){
same++;
}
number.push(String.valueOf(operate.pop()));
operate.push(question.charAt(j + 1));
}
}
k = j + 2;
}
}
if(same==p+2){//判断题目的符号是否都相同
ifsame=1;
}
while (!operate.empty()) {//最后把操作符栈中剩余的元素都压入数字栈中
number.push(String.valueOf(operate.pop()));
}
String[] result = new String[20];
int k1 = 0;
while (!number.empty()) {//把数字栈中的元素也就是形成的后缀表达式存储在数组中
result[k1] = number.pop();
k1++;
}
for (k1 = k1 - 1; k1 >= 0; k1--) {//逆序遍历数组,运算得到的后缀表达式
if (!result[k1].equals("+") && !result[k1].equals("-") && !result[k1].equals("*")
&& !result[k1].equals("÷")) {//是数字的话,先压入栈中
number.push(result[k1]);
} else {
int a1 = 0;
int b1 = 0;
if (!number.empty()) {//弹出两个数进行相应运算
b1 = Integer.parseInt(number.pop());
}
if (!number.empty()) {
a1 = Integer.parseInt(number.pop());
}
if (result[k1].equals("+")) {//如果是加号的话,弹出两个数相加
int c1 = a1 + b1;
number.push(String.valueOf(c1));
} else if (result[k1].equals("-")) {
int c1 = a1 - b1;
number.push(String.valueOf(c1));
} else if (result[k1].equals("*")) {
int c1 = a1 * b1;
number.push(String.valueOf(c1));
} else {
int c1 = a1 / b1;
number.push(String.valueOf(c1));
}
}
}
if(Integer.parseInt(number.peek())<0){
answer=1;
}
if(ifsame==1||answer==1){//如果全部符号都相等或结果小于0话,不输出题目
;
}else{
System.out.println(question + " = " + number.pop());//最后输出问题和答案
}
4.构建真分数
String[] op2 = { "+", "-" };// 存储连接分数的操作符的数组
int denominator = 1;
int numerator = 1;
int denominator1 = (int) (Math.random() * 19) + 1;// 生成分母
int numerator1 = (int) (Math.random() * 20);// 生成分子
if (numerator1 != 0) {
if (numerator1 > denominator1) {// 如果分子大于分母,也就是不是真分数时,交换分子分母,使其变成真分数
int temp = numerator1;
numerator1 = denominator1;
denominator1 = temp;
}
if (numerator1 == denominator1) {// 如果分子刚好等于分母,重新生成分子
numerator1 = (int) (Math.random() * 20);
}
int gcd1 = gcd(numerator1, denominator1);// 求分子分母最大公因数,保证分数形式最简
denominator1 = denominator1 / gcd1;// 化简
numerator1 = numerator1 / gcd1;// 化简
}
String question1 = numerator1 + "/" + denominator1;// 存储题目
5.分数计算,化简得答案
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 = gcd(numerator, denominator);
denominator = denominator / gcd;
numerator = numerator / gcd;
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 = gcd(numerator, denominator);
denominator = denominator / gcd;
numerator = numerator / gcd;
question1 += op2[symbol] + numerator2 + "/" + denominator2;
denominator1 = denominator;// 储存通分结果
numerator1 = numerator;
}
}
六、测试运行
1.进入需要编译的Java文件所在的目录;
2.javac 文件名.java (编译出class文件)
3.java 文件名 (运行出结果result.txt)
七、不足与改进
1.实现4个运算符的表达式
在连接时进行一次判断,如果是3或5个运算符就直接连接两个运算式子,如果是四个运算符就连接式子和一个随机数
String question = "";
for (int i = 0; i < p; i++) {
if (i < p - 1) {
String con = op1[(int) (Math.random() * 2)];
if (con.equals("*")) {
String que = MakeQuestion1();
if (flag == 1) {
question += "(" + que + ")" + con;
} else {
question += que + con;
}
} else {
question += MakeQuestion1() + con;
}
} else {
if(p == 3){
int judge = (int) Math.random() * 2;
if(judge == 1)
question += MakeQuestion1();
else{
question += (int) Math.random() * 100;
}
}
else{
question += MakeQuestion1();
}
}
flag = 0;
}
return question;
八、项目总结
本次项目,在前期阅读他人博客时,发现每个人都有不同思路,也有不同错误。我们在借鉴别人的学习经验时,也一定要有自己独特的见解。比如,在实现逆波兰表达式的时候,每个人的代码长度都不一样,解释也不同。这个时候我们就需要自己思考哪一种才是既容易理解,又代码少的最优解。再比如,在实现构建3-5个运算符表达式的时候,仔细阅读分析她人博客后,发现只能生成3或5个运算符的式子,出现没有4个运算符的bug。等等,还有许多问题,我只能说,网上的博客不都是完美无缺的,都来自大家的分享,我们在借鉴时都要有自己的独立思考,才能完美解决自己的问题。最后一句,找到一篇靠谱的博客,能让你解决问题事半功倍,希望我的这篇博客就很靠谱O(∩_∩)O哈哈~
九、PSP
PSP | 任务内容 | 计划时间(h) | 完成时间(h) |
Planning | 计划 | 15 | 22 |
Estimate | 估计完成时间 | 15 | 22 |
Development | 开发 | 13.5 | 20 |
Analysis | 需求分析 | 2 | 3.5 |
Design Spec | 生成文档 | 1.5 | 2 |
Design Review | 设计复审 | 1 | 1 |
Coding Standard | 代码规范 | 0.5 | 1 |
Design | 具体设计 | 2 | 3 |
Coding | 代码 | 5 | 8 |
Code Review | 代码复审 | 1 | 0.5 |
Test | 测试 | 0.5 | 1 |
Reporting | 报告 | 1.5 | 2 |
Test Report | 测试报告 | 0.5 | 1 |
Postmortem & Process Improvement Plan | 总结改进 | 1 | 1 |
十、后记
最后,再次感谢你耐心读完本篇博客。希望对你有所帮助,欢迎评论,一起交流学习~