前言
近期,由于业务需要,需要对字符串公式进行解析计算,包括加减乘除,幂,取最大最小值运算,同时还包括圆括号的优先级问题。
首先想到的解决思路是,使用栈,将运算符优先级进行排序,计算值按优先级依次入栈,低的在栈底,高的在栈顶。高优先级符号计算完毕后,得到的中间值再次入栈,用于下一个运算符号的计算。符号的优先级计算问题,转为入栈出栈操作。
为实现上述操作,需要将计算优先级混乱的字符串公式,消除括号,按优先级进行排序,后缀式完美贴合了上述操作。
后缀表达式又称逆波兰表达式,明显的特点是:逆波兰表达式中没有括号,计算时将操作符之前的第一个数作为右操作数,第二个数作为左操作数,进行计算,得到的值继续放入逆波兰表达式中。
加(+)、减(-)、乘(*)、除(/)、幂(^),属于二元运算,后缀式可满足,遇到操作符只需取栈顶两个元素进行运算即可。
而最值运算(MAX、MIN),属于多元运算,且要取最值得参数个数不确定,遇到最值运算符时,无法确定从栈顶取多少个元素。为解决上述问题,可以在最值运算符的参数后,再将一个记录参数元素个数的数值入栈。这样遇到最值运算符后,栈弹出的第一个元素标识后续接下来的n个元素都是要取最值的参数元素。
整体解决思路如上,接下来上代码,详情见注释。
接收字符串默认语法正确。关于校验的相关方法,笔者也写过一个拙码,后续分享。
不足可改进之处欢迎指出。
计算类
核心方法:
- calculate:计算方法,返回计算结果
- convertToPostfix:转换方法,将字串表达式转换为后缀式,并存入后缀式栈中
注:为区分最值运算符MAX、MIN的括号和优先级圆括号,字符串表达式中MAX、MIN的括号改为方括号 MAX[1,2,3 ],MIN[1,2,3]。
/**
* 表达式字符串计算工具类
*
* @since 2020/11/17 15:59
* @author MYeshion
*/
public class Calculator {
/** 后缀式栈*/
private Stack<String> postfixStack = new Stack<>();
/** 运算符栈*/
private Stack<String> operatorStack = new Stack<>();
/** 最大值最小值参数个数栈*/
private Stack<Integer> paramCountStack = new Stack<>();
/** 最大最小值前缀*/
private static final String MAX_MIN_PREFIX = "M";
/** 最低级运算符*/
private static final String LOWEST_OPERATOR = ",";
/** 最值多元运算符*/
private static final List<String> MULTIMODAL_OPERATOR = Arrays.asList("MIN", "MAX");
/** 取最大值*/
private static final String MAX = "MAX";
/** 取最小值*/
private static final String MIN = "MIN";
/** 右方括号*/
private static final String RIGHT_SQUARE_BRACKET = "]";
/** 右括号*/
private static final String RIGHT_BRACKET = ")";
/** 左括号*/
private static final String LEFT_BRACKET = "(";
/**
* 运算符优先级,越大优先级越高
*/
private static final Map<String, Integer> OPERATOR_PRECEDENCE = new HashMap<>();
static {
// 逗号默认最低优先级
OPERATOR_PRECEDENCE.put(",", -1);
OPERATOR_PRECEDENCE.put("(", 0);
OPERATOR_PRECEDENCE.put("[", 0);
OPERATOR_PRECEDENCE.put("MAX", 1);
OPERATOR_PRECEDENCE.put("MIN", 1);
OPERATOR_PRECEDENCE.put("+", 2);
OPERATOR_PRECEDENCE.put("-", 2);
OPERATOR_PRECEDENCE.put("*", 3);
OPERATOR_PRECEDENCE.put("/", 3);
OPERATOR_PRECEDENCE.put("^", 4);
OPERATOR_PRECEDENCE.put(")", 5);
OPERATOR_PRECEDENCE.put("]", 5);
}
/**
* 二元运算方法
*/
private static final Map<String, BiFunction<String, String, String>> BINARY_CALCULATES = new HashMap<>();
static {
BINARY_CALCULATES.put("+", ArithmeticHelper::add);
BINARY_CALCULATES.put("-", ArithmeticHelper::subtract);
BINARY_CALCULATES.put("*", ArithmeticHelper::multiply);
BINARY_CALCULATES.put("/", ArithmeticHelper::divide);
BINARY_CALCULATES.put("^", ArithmeticHelper::square);
}
/**
* 判断是否为运算符号,其中"M"标识MAX或者MIN
*
* @param op 判断字符
* @return boolean
* @since 2020/11/17 11:12
* @author MYeshion
*/
private boolean isOperator(String op) {
return (OPERATOR_PRECEDENCE.containsKey(op) || MAX_MIN_PREFIX.equals(op));
}
/**
* 将整数栈顶元素值+1
*
* @param stack
* @return void
* @since 2020/11/17 14:18
* @author MYeshion
*/
private void increaseStackTop(Stack<Integer> stack) {
if (!stack.isEmpty()) {
Integer temp = stack.pop();
temp += 1;
stack.push(temp);
}
}
/**
* 比较栈顶操作符和当前操作符的优先级,如果大于返回true
*
* @param cur 当前操作符
* @param peek 栈顶操作符
* @return boolean
* @since 2020/11/17 14:47
* @author MYeshion
*/
private boolean operatorCompare(String cur, String peek) {
return OPERATOR_PRECEDENCE.get(peek) >= OPERATOR_PRECEDENCE.get(cur);
}
/**
* 将表达式转换为后缀式,并存入后缀式栈中
*
* @param expression 运算表达式字符串
* @return void
* @since 2020/11/17 15:42
* @author MYeshion
*/
private void convertToPostfix(String expression) {
// 栈底放入最低优先级运算符号
operatorStack.push(",");
// 当前字符的位置
int currentIndex = 0;
// 上次运算符到本次运算符之间的长度(用于获取数值)
int count = 0;
// 当前操作字符和栈顶字符
String currentOp, peekOp;
for (; currentIndex < expression.length(); currentIndex++) {
currentOp = expression.substring(currentIndex, currentIndex + 1);
// 如果当前字符是运算符
if (isOperator(currentOp)) {
//取两个运算符之间的数值
if (count > 0) {
postfixStack.push(expression.substring(currentIndex - count, currentIndex));
}
peekOp = operatorStack.peek();
// MAX、MIN运算符特殊处理:
if (StringUtil.equals(currentOp, MAX_MIN_PREFIX)) {
operatorStack.push(expression.substring(currentIndex, currentIndex + 3));
paramCountStack.push(0);
currentIndex += 3;
// 遇到“,”运算符
} else if (StringUtil.equals(currentOp, LOWEST_OPERATOR)) {
// 参数栈顶参数个数+1
increaseStackTop(paramCountStack);
// 将运算符栈中的元素移到后缀式栈中直到遇到最值符号
while (!MULTIMODAL_OPERATOR.contains(operatorStack.peek())) {
postfixStack.push(operatorStack.pop());
}
// 遇到取最值运算结束符
} else if (StringUtil.equals(RIGHT_SQUARE_BRACKET, currentOp)) {
// 将运算符栈中的元素移到后缀式栈中直到遇到最值符号
while (!MULTIMODAL_OPERATOR.contains(operatorStack.peek())) {
postfixStack.push(operatorStack.pop());
}
// 将最值运算参数个数和最值运算符依次存入后缀式栈
postfixStack.push(String.valueOf(paramCountStack.pop() + 1));
postfixStack.push(operatorStack.pop());
// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
} else if (StringUtil.equals(RIGHT_BRACKET, currentOp)) {
while (!StringUtil.equals(LEFT_BRACKET, operatorStack.peek())) {
postfixStack.push(operatorStack.pop());
}
operatorStack.pop();
} else {
// 将运算符栈中,比当前运算符优先级大的符号都转移到后缀式栈中
while (!StringUtil.equals(LEFT_BRACKET, currentOp) && !StringUtil.equals(LOWEST_OPERATOR, peekOp)
&& operatorCompare(currentOp, peekOp)) {
postfixStack.push(operatorStack.pop());
peekOp = operatorStack.peek();
}
operatorStack.push(currentOp);
}
count = 0;
} else {
count++;
}
}
// 最后一个字符不是运算符,加入后缀式栈中
if (count > 0) {
postfixStack.push(expression.substring(currentIndex - count, currentIndex));
}
// 将运算符栈中的剩余元素加入到后缀式栈中
while (!StringUtil.equals(LOWEST_OPERATOR, operatorStack.peek())) {
postfixStack.push(operatorStack.pop());
}
}
/**
* 计算字符串表达式
*
* @param expression 要计算的字符串
* @return java.lang.String
* @since 2020/11/18 11:25
* @author MYeshion
*/
public String calculate(String expression) {
Stack<String> resultStack = new Stack<>();
convertToPostfix(expression);
// 将后缀式栈反转
Collections.reverse(postfixStack);
// 参与计算的第一个参数,第二个参数,当前值
String firstVar, secondVar, currentValue;
// 多元取最值运算的参数个数
int paramCount;
while (!postfixStack.isEmpty()) {
currentValue = postfixStack.pop();
// 非操作符,直接存入计算结果集栈中
if (!isOperator(currentValue)) {
resultStack.push(currentValue);
// 多元运算计算:操作符在栈顶,下一个值为参数个数,之后的连续参数个数个值为要取最值的范围集
} else if (MULTIMODAL_OPERATOR.contains(currentValue)) {
paramCount = Integer.parseInt(resultStack.pop());
List<String> paramList = new ArrayList<>();
while (paramCount-- > 0) {
paramList.add(resultStack.pop());
}
resultStack.push(StringUtil.equals(MAX, currentValue) ? ArithmeticHelper.max(paramList) : ArithmeticHelper.min(paramList));
// 二元运算,从结果栈中依次取两个元素进行计算
} else {
BiFunction<String, String, String> calculate = BINARY_CALCULATES.get(currentValue);
if (Objects.nonNull(calculate)) {
secondVar = resultStack.pop();
firstVar = resultStack.pop();
resultStack.push(calculate.apply(firstVar, secondVar));
}
}
}
return resultStack.pop();
}
}
计算方法类
存放加、减、乘、除、幂、最大值、最小值的运算方法,返回结果都为String类型。
/**
* 计算方法类
*
* @since 2020/11/17 18:48
* @author Yeshion
*/
public class ArithmeticHelper {
private ArithmeticHelper() {
}
public static String add(String var1, String var2) {
return BigDecimalUtil.add(var1, var2).toPlainString();
}
public static String divide(String var1, String var2) {
return BigDecimalUtil.divide(var1, var2).toPlainString();
}
public static String multiply(String var1, String var2) {
return BigDecimalUtil.multiply(var1, var2).toPlainString();
}
public static String subtract(String var1, String var2) {
return BigDecimalUtil.subtract(var1, var2).toPlainString();
}
public static String square(String base, String index) {
BigDecimal baseValue = BigDecimal.ZERO;
if (StringUtil.isNotBlank(base)) {
baseValue = new BigDecimal(base);
}
int indexValue = 0;
if (StringUtil.isNotBlank(index)) {
indexValue = Integer.parseInt(index);
}
if (indexValue == 0) {
return "1";
} else {
while (indexValue-- > 0) {
baseValue = baseValue.multiply(baseValue);
}
return baseValue.toPlainString();
}
}
public static String max(List<String> varList) {
BigDecimal currValue;
BigDecimal maxValue = new BigDecimal(varList.get(0));
for (String var : varList) {
currValue = new BigDecimal(var);
if (currValue.compareTo(maxValue) > 0) {
maxValue = currValue;
}
}
return maxValue.toPlainString();
}
public static String min(List<String> varList) {
BigDecimal currValue;
BigDecimal minValue = new BigDecimal(varList.get(0));
for (String var : varList) {
currValue = new BigDecimal(var);
if (currValue.compareTo(minValue) < 0) {
minValue = currValue;
}
}
return minValue.toPlainString();
}
}