实验
随着数据结构课程的结束,终于迎来了课程设计QAQ,这一阵子都忙着预习准备期末考试,用来完成课程设计的时间非常的有限,所以只能实现一些基本的要求。
一、实验内容及实验要求
【问题描述】
设计一个简单的算术表达式计算器。
【基本要求】
实现标准整数类型的四则运算表达式的求值(包含括号,可多层嵌入)
【测试数据】
(30+2*70)/3-12*3
5+(9*(62-37)+15)*6
要求自行设计非法表达式,进行程序测试,以保证程序的稳定运行。
【实现提示】
可以设计以下辅助函数
status isNumber(char ReadInChar); //视ReadInchar 是否是数字而返回 TRUE 或 FALSE 。
int TurnToInteger(char IntChar); // 将字符’0’.’9’ 转换为整数 9
二、问题分析
概述
- 本实验的编程语言采用Java,并用Swing实现了GUI,实现结果类似电脑上自带的计算器,只不过功能远不如其强大,另外该计算器支持小数的输入。
- 算术表达式的求解一般用双栈来实现。需要使用两个工作栈
OPTR——寄存运算符栈
OPRD——寄存操作数据或结果栈
来实现算数表达式的求解。 - 该问题可以先把输入的中缀表达式转换为后缀表达式进行求解,这样就不会出现括号匹配的异常,也可以直接通过中缀表达式进行求解,这里
由于不会转换后缀表达式,所以直接对中缀表达式进行求解。
算符优先法对中缀表达式求值
前面说到要用两个栈来实现求值,那么算法的具体过程是什么?下面进行简要分析:
- 表达式要求按照先乘除,后加减;从左往右依次计算,有括号的先算括号。所以,我们需要根据运算优先关系的规定来实现对表达式的编译。
上图是由操作符优先表,表中表现了下面三种关系:
θ 1 > θ 2 , θ 1 的 优 先 权 低 于 θ 2 θ 1 = θ 2 , θ 1 的 优 先 权 等 于 θ 2 θ 1 < θ 2 , θ 1 的 优 先 权 高 于 θ 2 \theta_1>\theta_2,\ \ \theta_1的优先权低于\theta_2\\ \theta_1=\theta_2,\ \ \theta_1的优先权等于\theta_2\\ \theta_1<\theta_2,\ \ \theta_1的优先权高于\theta_2\\ θ1>θ2, θ1的优先权低于θ2θ1=θ2, θ1的优先权等于θ2θ1<θ2, θ1的优先权高于θ2
在运算中,应注意下列关系:
当’(’ = ‘)’,即左右括号相遇,括号内运算完成。
当’#‘=’#‘,表示整个表达式求值完毕,因为表达式是以”#“结尾的。
而’)‘与’(‘,’#‘与’)‘,’(‘与’#'之间无优先关系。 - 算符优先算法思想的描述如下:
1.首先置操作数栈OPND为空,再置操作符(运算符)栈OPTR为空,然后将表达式起始符”#“作为运算符栈的栈底元素。
2.依次读入表达式中的每个字符,若是操作数,则进OPND栈;若是运算符,则和运算符栈OPTER的栈顶运算符比较优先级后作相应操作 ,直至整个表达式求值完毕。这里的相应操作为: - 如果栈顶运算符的优先级<读入的运算符即 θ 1 < θ 2 \theta_1<\theta_2 θ1<θ2,那么这个运算符就直接压栈。
- 如果栈顶运算符的优先级>读入的运算符即
θ
1
<
θ
2
\theta_1<\theta_2
θ1<θ2,那么从操作数栈OPND中弹出两个数,从操作符栈OPTR中弹出
θ
1
\theta_1
θ1对其进行运算,将结果放入OPND栈,并重新读入运算符
θ
1
\theta_1
θ1,与下一个栈顶元素进行优先级比较。
最后的结果就是OPND栈的栈顶元素。
对于异常输入的处理
对异常输入处理的最好方法就是拒绝异常的输入,这也是利用GUI处理的一大好处之一。对于用户的输入,本程序只能通过鼠标点击按钮实现,不能直接通过键盘的输入,这样就能避免很大一部分异常。
- 对于连续的运算符:比如++,*+,-/ 等直接将前一个运算符替换成后输入的即可
- 对于带有括号和小数点的异常,程序规定:
'('后不能输入运算符和小数点;
')'后不能输入'('和小数点;
小数点后不能输入运算符和括号
数字之后不能输入'(';
不能连续的输入小数点如:5..6或4.53.6等
如果用户尝试输入以上提到的情况,程序会不予理睬。 - 对于括号匹配的异常:之前提到过’)‘与’(‘,’#‘与’)‘,’(‘与’#'之间无优先关系,如果程序在执行时OPTR栈中出现了这种匹配情况,那么会抛出异常,通过文字直接提示用户。
- 对于数值越界的异常:我们都知道,在Java中int类型是有上下界的,如果数值过大或者过小,得到的答案会出现错误,这里程序采用位运算对输入和计算的数值进行判断,如果越界则会抛出异常提示用户。
三、逻辑设计
程序分为三个包分别为:service、utility、ui
- service包下提供了两个类:
Calculator类是核心的数据结构:双栈OPTR、OPND以及对其执行算符优先算法。
CalculateException类是自定义异常处理类,用来处理用户输入的各种异常。 - utility包中有一个工具类Utility,用于实现对数据的判断、比较以及运算操作。
- ui包是整个用户图形化界面的实现,它连接了service和utility,是整个程序的入口。
以上是类之间的调用关系
四、物理设计
本实验数据结构采用栈,使用算符优先算法对中缀表达式求解。
算符优先算法的流程图如下所示:
该算法及其数据结构在Calculator类中体现。
这里提供Calculator类的代码如下所示:
package com.service;
import java.util.Stack;
import static com.utility.Utility.*;
/*
Calculator类:双栈数据结构的提供以及算符优先算法的实现
*/
public class Calculator {
//输入得到的字符串
private String text;
//寄存运算符栈
private static final Stack<Character> OPTR = new Stack<>();
//寄存操作数栈
private static final Stack<Double> OPND = new Stack<>();
public Calculator() {
}
public Calculator(String text) {
this.text = text;
}
//利用栈实现算数表达式求值
public double evaluateExpression() throws CalculateException {
//先清空栈
OPTR.clear();
OPND.clear();
//先在OPTR栈中放入一个#
OPTR.push('#');
char operator; //从OPTR栈中弹出的运算符
double a, b; //从OPND栈中弹出的两个操作数
double round = 0; //记录整数部分的值
double decimal = 0; //记录小数部分的值
boolean beginR = false; //标记整数部分是否开始
int beginD = 0; //标记小数部分的位数
//遍历text中的所有字符
char[] ch = text.toCharArray();
for (int i = 0; i < ch.length; i++) {
//如果ch是数字或小数点.
if (!isOperator(ch[i])) {
//如果是整数部分
if (ch[i] != '.' && beginD == 0) {
beginR = true;
round = round * 10 + (ch[i] - '0');
} else if (beginD > 0) { //如果是小数部分
decimal = decimal * 10 + (ch[i] - '0');
beginD++;
} else { //如果ch是小数点,标记小数部分开始并统计位数
beginD++;
}
} else {
//如果运算符之前输入的是数字,那么将其入栈
if (beginR) {
//将decimal还原成小数
while (beginD > 1) {
decimal /= 10;
beginD--;
}
OPND.push(round + decimal);
//初始化
beginD = 0;
beginR = false;
round = 0;
decimal = 0;
}
switch (getPriority(OPTR.peek(), ch[i])) {
case '<':
//如果栈顶运算符的优先级小于ch,ch直接入栈
OPTR.push(ch[i]);
break;
case '>':
//如果栈顶运算符的优先级大于ch,就弹出一个运算符到OPND栈中取两个元素运算
operator = OPTR.pop();
a = OPND.pop();
b = OPND.pop();
//将弹出的两个操作数运算,并压入OPND栈中
OPND.push(operate(b, operator, a));
//这时需要将刚刚判断的ch再次和栈顶运算符的优先级进行判断
i--;
break;
case '=':
//如果栈顶运算符的优先级与ch相等,将栈顶元素弹出
OPTR.pop();
break;
case '?':
//如果栈顶运算符的优先级与ch无法比较,则抛出异常
throw new CalculateException("括号数量不匹配!请重新输入!");
}
}
}
return OPND.peek();
}
}
该类中对于像
- isOperator(char ch) //判断是否为运算符
- getPriority(char ch1, char ch2) //比较ch1和ch2的优先级
- operate(double d1, char operator, double d2) //根据operator运算符运算d1和d2
等方法都在工具类Utility中实现
以下是Utility工具类的代码:
package com.utility;
import com.service.CalculateException;
/*
工具类:实现一些必要的判断、比较以及运算操作
*/
public class Utility {
//判断ch是否为运算符
public static boolean isOperator(char ch) {
return ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '(' || ch == ')' || ch == '#';
}
//判断运算符的优先级
public static char getPriority(char ch1, char ch2) {
if ((ch1 == '(' && ch2 == ')') || (ch1 == '#' && ch2 == '#')) {
return '=';
} else if ((ch1 == ')' && ch2 == '(') || (ch1 == '(' && ch2 == '#') || (ch1 == '#' && ch2 == ')')) {
return '?';
} else if (ch1 == '(' || ch1 == '#' || ch2 == '(' || ((ch1 == '+' || ch1 == '-') && (ch2 == '*' || ch2 == '/'))) {
return '<';
} else return '>';
}
//对两数进行计算
public static double operate(double d1, char operator, double d2) throws CalculateException {
int x = (int) d1;
int y = (int) d2;
switch (operator) {
case '+':
if (addOverflow(x,y)){
throw new CalculateException("运算超出范围,请重新输入!");
}else return d1 + d2;
case '-':
if (subtractOverflow(x,y)){
throw new CalculateException("运算超出范围,请重新输入!");
}else return d1 - d2;
case '*':
if (multiplyOverflow(x,y)){
throw new CalculateException("运算超出范围,请重新输入!");
}else return d1 * d2;
case '/':
if (d2 == 0) {
throw new CalculateException("分母不能为0,请重新输入!");
} else return d1 / d2;
}
return 0;
}
//判断加法是否越界
public static boolean addOverflow(int x, int y) {
int r = x + y;
return ((x ^ r) & (y ^ r)) < 0;
}
//判读减法是否越界
public static boolean subtractOverflow(int x, int y) {
int r = x - y;
return ((x ^ y) & (x ^ r)) < 0;
}
//判断乘法是否越界
public static boolean multiplyOverflow(int x, int y) {
long r = (long) x * (long) y;
return (int) r != r;
}
}
对于Swing用户图形化界面的代码以及对异常输入的自动更改和处理这里就不再展示,现提供部分实验测试结果:
总结
对于本次实验其实还有许多可以完善的地方
- 算术表达式无法直接输入负数,对于负数的处理只能用(0-x)来替代-x,可以增加对于’-'符号的判别,在栈中进行特殊处理。
- Java等编程语言对于double类型数据的运算会有精度损失比如 :0.3 + 0.4=0.699999999999,所以本程序对于小数采取自动四舍五入保留三位,其实这里有更好的解决方法,比如用BigDecimal对小数进行处理,或者利用Swing组件中的复选框JCheckBox给用户提供选择小数位数的机会。
- 可以提供更多的运算符号,或者给计算器加上记忆功能。
终于弄完一个了还有两个实验555