前言
数学计算的加减乘除人脑算很简单,但是放到计算中却需要进行一些转换,在正式写Java计算数学表达式前,我们需要先来介绍两个概念:前缀表达式、中缀表达式和后缀表达式;(这里的某缀指的是符号的位置)
中缀表达式:我们所接收的教育中所学的就为中缀表达式,形如(ab/c-d+e),也即表达式中的运算符号在数字的中间且有运算符的优先级。
后缀表达式:后缀表达式即运算符号位于操作数的后面,形如(abc/d-e+),需要从左往右计算,可以不用考虑优先级,是一种线性的计算,也是计算机易于计算的一种方式;
前缀表达式:前缀表达式可由后缀表达式可知,运算符号位于操作数之前,形如(±/*abcde),也是一种计算机易于计算的一种方式。
介绍完上面的数学表达式的三种方式以后,就解决了我们用Java计算数学表达式的第一步了,将中缀表达式(人类易于理解)转换为后缀表达式(计算机易于计算);
中缀表达式转后缀表达式
将中缀表达式转换为后缀表达式可以借助堆栈实现,具体转换方式;
(1)从左到右进行遍历
(2)如果是运算数,则直接输出
(3)如果是左括号,则直接压入堆栈(括号是最高优先级)入栈后变成优先级最低的,确保其它运算符号能够正常入栈。
如果遇到的是右括号,则说明这个括号中的式子已经结束了,弹出并输出栈顶元素直到遇到左括号为止,遇到左括号弹出但不输出;
如果遇到运算符(加、减、乘、除运算符),将该运算符与栈顶运算符进行比较,
如果优先级高于栈顶运算符则压入堆栈;如果优先级低于栈顶运算符则将栈顶运算符弹出并输出,并比较新的栈顶运算符直到优先级大于栈顶元素或者栈空的时候在将该运算符入栈。
如果处理完毕后,栈不为空,则依次弹出并输出。
代码如下:
public static String infixToSuffix(String expression) {
// 创建操作符堆栈
Stack<Character> stack = new Stack<>();
// 要输出的后缀表达式字符串
StringBuild suffix = new StringBuild();
for (int i = 0; i < expression.length(); i++) {
// 中间变量
char temp;
// 获取该中缀表达式的每一个字符进行判断
char ch = expression.charAt(i);
switch (ch) {
// 过滤空格
case ' ':
break;
// 遇到左括号直接入栈
case '(':
stack.push('(');
break;
// 遇到+、- 运算符,将栈中的所有运算符全部弹出,遇到堆栈中的左括号为止
case '+':
case '-':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '(') {
stack.push('(');
break;
}
suffix.append(temp);
}
// 没有进入循环说明当前为第一次进入或者其他前面运算都有括号等情况导致栈已经为空,此时需要将符号进栈
stack.push(ch);
break;
// 遇到*、/ 运算符,则将堆栈中的所有运算符全部弹出,遇到加号、减号以及左括号为止
case '*':
case '/':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '+' || temp == '-' || temp == '(') {
stack.push(temp);
break;
}
suffix.append(temp);
}
// 没有进入循环说明当前为第一次进入或者其他前面运算都有括号等情况导致栈已经为空,此时需要将符号进栈
stack.push(ch);
break;
// 遇到右括号,则弹出所有运算符直到遇到左括号为止,并抛弃左括号。
case ')':
while (!stack.isEmpty()) {
temp = stack.pop();
if (temp == '(') {
// 这里左括号不重新入栈了
break;
}
suffix.append(temp);
}
break;
// 默认情况下,将作为操作数直接输出到队列中
default:
suffix.append(ch);
break;
}
}
// 如果堆栈不为空,则把剩余的运算符一次弹出,输出到队列中
while (!stack.isEmpty()) {
suffix.append(stack.pop());
}
// 返回后缀表达式
return suffix.toString();
}
我们做一下测试,将中缀表达式a+b*c+(d-e)/f-g 转换为后缀表达式(这里先用字母代替数字),main方式如下:
public static void main(String[] args) {
System.out.println(Calculation.infixToSuffix("a+b*c+(d-e)/f-g"));
}
转换后的后缀表达式为:abc*+de-f/+g-
打印每次for循环堆栈的大小以及输出能够更好的如和将中缀表达式转换为后缀表达式
com.zlc.otherproject.calculationutil.Calculation
第1次
堆栈中的数据大小0
输出为a
第2次
堆栈中的数据大小1
输出为a
第3次
堆栈中的数据大小1
输出为ab
第4次
堆栈中的数据大小2
输出为ab
第5次
堆栈中的数据大小2
输出为abc
第6次
堆栈中的数据大小1
输出为abc*+
第7次
堆栈中的数据大小2
输出为abc*+
第8次
堆栈中的数据大小2
输出为abc*+d
第9次
堆栈中的数据大小3
输出为abc*+d
第10次
堆栈中的数据大小3
输出为abc*+de
第11次
堆栈中的数据大小1
输出为abc*+de-
第12次
堆栈中的数据大小2
输出为abc*+de-
第13次
堆栈中的数据大小2
输出为abc*+de-f
第14次
堆栈中的数据大小1
输出为abc*+de-f/+
第15次
堆栈中的数据大小1
输出为abc*+de-f/+g
中缀表达式转换后缀表达式为:abc*+de-f/+g-
Process finished with exit code 0
计算后缀表达式
计算后缀表达式还可以借助堆栈来实现,从左到右依次读取,如果为数字则直接入栈,遇到运算符则弹出栈顶元素与弹出栈顶元素后的新栈顶元素进行运算;需要注意的地方就是除法被除数不能为0这种了。(数值计算使用了BigDecimal)
计算后缀表达式代码如下:
/**
* 定义公式计算符号
*/
public static final String ADD = "+";
public static final String SUBTRACTION = "-";
public static final String MULTIPLICATION = "*";
public static final String DIVISION = "/";
public static BigDecimal suffixToArithmetic(String express) throws ArithmeticException {
// 将后缀表达式分割成字符串数组,此处直接使用空白也可以对字符串进行分割!!
// 定义一个操作符集合,用于判断是否为操作符(后缀表达式中已经没有了左右括号)
List<String> operatorList = Arrays.asList("+","-","*","/");
String[] values = express.split("");
Stack<BigDecimal> stack = new Stack<>();
String value;
for (String s : values) {
// 这里最好是进行判断彻底消除空格,在该数组的第一位为一个隐形的空格,这里一定要注意在使用exp.split("")剔除空白""
value = s;
// 由于使用的是""截取导致在数组第一位上面的值为空白
if (StringUtils.isBlank(value)) {
continue;
}
// 如果遇到了数字,放入操作数栈等待计算
if(!operatorList.contains(value)){
stack.push(new BigDecimal((value)));
} else {
// 如果是运算符,则弹出栈顶的两个数进行计算
// x为第一次弹出的栈顶元素,这里面的顺序不能错
BigDecimal x = stack.pop();
BigDecimal y = stack.pop();
// 将运算结果重新压栈
stack.push(calculate(x, y, value));
}
}
// 弹出栈顶元素就是最终结果
return stack.pop();
}
/**
* 这里需要注意x为第一次弹出栈顶的数,所以下面的加减乘除使用y操作x
* @param x 操作数
* @param y 操作数
* @param operator 操作符
* @return 计算结果
*/
private static BigDecimal calculate(BigDecimal x, BigDecimal y, String operator) {
BigDecimal calculateResult;
switch (operator.trim()){
case Calculation.ADD:
calculateResult = y.add(x);
break;
case Calculation.SUBTRACTION:
calculateResult = y.subtract(x);
break;
case Calculation.MULTIPLICATION:
calculateResult = y.multiply(x);
break;
case Calculation.DIVISION:
if (x.intValue() == 0){
throw new ArithmeticException("被除数为0,无法计算!");
}else {
// 结果保留4位小数
calculateResult = y.divide(x,4,RoundingMode.HALF_UP);
}
break;
default:
throw new ArithmeticException("无法运算的运算符!");
}
return calculateResult;
}
我们做一下测试,计算中缀表达式1+2*3+(4-2)/2-4,计算结果为4,保丢4位小数位4.0000.
public static void main(String[] args) {
String infixExpression = "1+2*3+(4-2)/2-4";
String postfixExpression = Calculation.infixToSuffix(infixExpression);
System.out.println("中缀表达式:" + infixExpression +"转换为后缀表达式后为:" + postfixExpression);
System.out.println("计算结果为:" + Calculation.suffixToArithmetic(postfixExpression));
}
计算结果如下:
中缀表达式:1+2*3+(4-2)/2-4转换为后缀表达式后为:123*+42-2/+4-
计算结果为:4.0000
Process finished with exit code 0
计算形式公式
上面的例子我们计算的公式只能是公式中的数字已经确定好的,这样还不够灵活,下面使用26个小写的英文字母代替数字,使用字母作为形式计算数字,为相应的字母赋值,计算相应的数学公式计算结果,对上面的代码进行一些改造。
package com.zlc.javaproject.calculationutils;
import org.apache.commons.lang.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.regex.Pattern;
import static java.util.regex.Pattern.compile;
/**
* @author Jacob
* @version 1.0
* @date 2020/9/14 20:43
*/
public class Calculation {
/**
* 定义公式计算符号
*/
private static final String ADD = "+";
private static final String SUBTRACTION = "-";
private static final String MULTIPLICATION = "*";
private static final String DIVISION = "/";
/**
* @param expression 计算公式
* @param param 参数
* @return 根据计算公式返回结果
*/
public static BigDecimal getCalculateResult(String expression, Map<String,String> param) throws ArithmeticException{
return stringToArithmetic(expression, param);
}
/**
* @param string 中缀表达式
* @param map 公式形参实参map
* @return 公式计算结果
* @throws ArithmeticException 计算异常
*/
private static BigDecimal stringToArithmetic(String string, Map<String, String> map) throws ArithmeticException{
return suffixToArithmetic(infixToSuffix(string), map);
}
/**
* @param expression 中缀表达式
* @return 后缀表达式
*/
public static String infixToSuffix(String expression) {
// 创建操作符堆栈
Stack<Character> stack = new Stack<>();
StringBuilder suffix = new StringBuilder();
for (int i = 0; i < expression.length(); i++) {
// 中间变量
char temp;
// 获取该中缀表达式的每一个字符进行判断
char ch = expression.charAt(i);
switch (ch) {
// 过滤空格
case ' ':
break;
// 遇到左括号直接入栈
case '(':
stack.push('(');
break;
// 遇到+、- 运算符,将栈中的所有运算符全部弹出,遇到堆栈中的左括号为止
case '+':
case '-':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '(') {
stack.push('(');
break;
}
suffix.append(temp);
}
// 没有进入循环说明当前为第一次进入或者其他前面运算都有括号等情况导致栈已经为空,此时需要将符号进栈
stack.push(ch);
break;
// 遇到*、/ 运算符,则将堆栈中的所有运算符全部弹出,遇到加号、减号以及左括号为止
case '*':
case '/':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '+' || temp == '-' || temp == '(') {
stack.push(temp);
break;
}
suffix.append(temp);
}
// 没有进入循环说明当前为第一次进入或者其他前面运算都有括号等情况导致栈已经为空,此时需要将符号进栈
stack.push(ch);
break;
// 遇到右括号,则弹出所有运算符直到遇到左括号为止,并抛弃左括号。
case ')':
while (!stack.isEmpty()) {
temp = stack.pop();
if (temp == '(') {
// 这里左括号不重新入栈了
break;
}
suffix.append(temp);
}
break;
// 默认情况下,将作为操作数直接输出到队列中
default:
suffix.append(ch);
break;
}
}
// 如果堆栈不为空,则把剩余的运算符一次弹出,输出到队列中
while (!stack.isEmpty()) {
suffix.append(stack.pop());
}
// 返回后缀表达式
return suffix.toString();
}
/**
* @param express 需要计算的后缀表达式
* @return 计算结果
* @throws ArithmeticException 计算异常
*/
public static BigDecimal suffixToArithmetic(String express, Map<String, String> map) throws ArithmeticException {
// 使用正则表达式匹配数字
Pattern pattern = compile("[a-z]");
// 将后缀表达式分割成字符串数组,此处直接使用空白也可以对字符串进行分割!!
// 定义一个操作符集合,用于判断是否为操作符(后缀表达式中已经没有了左右括号)
List<String> operatorList = Arrays.asList("+","-","*","/");
String[] values = express.split("");
Stack<BigDecimal> stack = new Stack<>();
String value;
for (String s : values) {
// 这里最好是进行判断彻底消除空格,在该数组的第一位为一个隐形的空格,这里一定要注意在使用exp.split("")剔除空白""
value = s;
// 由于使用的是""截取导致在数组第一位上面的值为空白
if (StringUtils.isBlank(value)) {
continue;
}
// 如果遇到了数字,放入操作数栈等待计算
if(!operatorList.contains(value)){
stack.push(new BigDecimal(map.get(value)));
} else {
// 如果是运算符,则弹出栈顶的两个数进行计算
// x为第一次弹出的栈顶元素,这里面的顺序不能错
BigDecimal x = stack.pop();
BigDecimal y = stack.pop();
// 将运算结果重新压栈
stack.push(calculate(x, y, value));
}
}
// 弹出栈顶元素就是最终结果
return stack.pop();
}
/**
* 这里需要注意x为第一次弹出栈顶的数,所以下面的加减乘除使用y操作x
* @param x 操作数
* @param y 操作数
* @param operator 操作符
* @return 计算结果
*/
private static BigDecimal calculate(BigDecimal x, BigDecimal y, String operator) {
BigDecimal calculateResult;
switch (operator.trim()){
case Calculation.ADD:
calculateResult = y.add(x);
break;
case Calculation.SUBTRACTION:
calculateResult = y.subtract(x);
break;
case Calculation.MULTIPLICATION:
calculateResult = y.multiply(x);
break;
case Calculation.DIVISION:
if (x.intValue() == 0){
throw new ArithmeticException("被除数为0,无法计算!");
}else {
// 结果保留4位小数
calculateResult = y.divide(x,4, RoundingMode.HALF_UP);
}
break;
default:
throw new ArithmeticException("无法运算的运算符!");
}
return calculateResult;
}
public static void main(String[] args) {
// 中缀表达式 1+2*3+(4-2)/2-4
String infixExpression = "a+b*c+(d-b)/b-d";
Map<String, String> params = new HashMap<>(16);
params.put("a","1");
params.put("b", "2");
params.put("c","3");
params.put("d","4");
String postfixExpression = Calculation.infixToSuffix(infixExpression);
System.out.println("中缀表达式:" + infixExpression +"转换为后缀表达式后为:" + postfixExpression);
System.out.println("计算结果为:" + Calculation.getCalculateResult(infixExpression,params));
}
}
运行结果如下:
中缀表达式:a+b*c+(d-b)/b-d转换为后缀表达式后为:abc*+db-b/+d-
计算结果为:4.0000
经过上面的代码改造就可以使计算更加的灵活方便。