title: 前缀、中缀、后缀表达式及简易运算实现总结
date: 2023-06-30 10:25:50
tags:
- 表达式
categories: - 开发知识及其他
cover: https://cover.png
feature: false
1. 概念
1.1 什么是前缀、中缀、后缀表达式?
- 前缀表达式:又称波兰式(Polish Notation),操作符以前缀形式位于两个运算数前(如:3 + 2 的前缀表达形式就是 + 3 2)
- 中缀表达式:操作符以中缀形式位于运算数中间(如:3 + 2),是我们日常通用的算术和逻辑公式表示方法
- 后缀表达式:又称逆波兰式(Reverse Polish Notation - RPN),操作符以后缀形式位于两个运算数后(如:3 + 2 的后缀表达形式就是 3 2 +)
中缀表达式往往需要使用括号将操作符和对应的操作数括起来,用于指示运算的次序,如 5 * (2 + 1) 虽然 * 的优先级高于 + ,但括号的存在表示应优先执行括号内的 + 运算。适合于人类的思维结构和运算习惯,但并不适用于计算机
与中缀表达式不同,前缀和后缀表达式都不需要使用括号来标识操作符的优先级,适用于计算机。不过后缀表达式的计算按操作符从左到右出现的顺序依次执行(不考虑运算符之间的优先级),更加符合人类的阅读习惯,因此实际计算机程序中,基本都是用后缀表达式来存储公式的,前缀表达式效果次之。对于中缀表达式,我们则可以先将其转为后缀表达式,再进行求值
1.2 树结构对应
其实前缀表达式、中缀表达式、后缀表达式就是通过树来存储和计算表达式的三种不同方式,分别对应树的先序遍历、中序遍历、后序遍历。如下图,这是一颗二叉树
- 上面的树,先序遍历就是 *+a−bcd,即对应前缀表达式
- 中序遍历是 a+b−c∗d,但是这样的表示是有歧义的,这样表示 ab 是一颗子树,cd 是一颗子树,然后相减,所以中缀表达式必须借助括号,才能正确地表达出想要的结果。中缀表达式为:(a+(b−c))∗d,括号表示一个子树的整体
- 后序遍历是 abc−+d∗,即对应的后缀表达式
2. 表达式求值
2.1 通过树结构存储和求值表达式
实现思路比较简单,如果节点上存的是参数,那么该参数的值,就是该节点的值;如果节点上存的操作符,拿该节点左子树和右子树做对应运算,得到的结果作为该节点的值
代码略
2.2 前缀表达式解析和求值
∗ + a − b c d ∗+a−bcd ∗+a−bcd
观察前缀表达式的规律可以发现,每当连续出现两个数值时,前面必定会有一个操作符,这是先序遍历的特征决定的(根左右,根即为表达式),因此我们依次取三个元素出来,判断符合连续两个数值条件的进行运算,就可以得到一个操作符节点的数值,如此反复递归,最终就能求出表达式的值
代码略
2.3 后缀表达式解析和求值
a b c − + d ∗ abc−+d∗ abc−+d∗
和前缀表达式类似,其实也就是后序遍历的特征,即只要有运算符出现的地方,前面两个元素一定是操作数(左右根),然后同样取三个元素出来,判断符合条件的进行运算
详细代码见 3
2.4 中缀表达式转后缀表达式
( a + ( b − c ) ) ∗ d (a+(b−c))∗d (a+(b−c))∗d
中缀表达式直接求值比较麻烦,所以我们将其转换为后缀表达式,再求值就方便了。中缀表达式转后缀表达式的难点在于,要考虑括号和运算符优先级,步骤如下,这个转换算法不是凭空产生的,而是根据后缀表达式的特点反推出来的
- 创建两个栈,S1 用来存输出元素,S2 用来存运算符。由于表达式中的运算符是有优先级的,所以必须通过栈来暂存起来
- 从中缀表达式栈顶开始,向栈尾逐个读取元素
- 如果读到操作数,直接加到 S1 栈尾。因为后缀表达式操作数永远是在运算符前面的
- 如果读到左括号,则直接压入 S2 栈顶。因为左括号要等到右括号时才能处理
- 如果读到运算符,且 S2 栈为空或 S2 栈顶元素为左括号,则直接压入 S2 栈顶。因为这种情况不需要比较运算符优先级
- 如果读到运算符,且 S2 栈顶也为运算符,且当前运算符优先级大于栈顶元素,则将当前运算符压入 S2 栈顶。因为后面读取到的运算符可能比当前运算符优先级更高,因此暂时不能输出当前运算符
- 如果读到运算符,且 S2 栈顶也为运算符,且当前运算符优先级小于等于栈顶元素,则将 S2 栈顶运算符弹出,加到 S1 栈尾。因为优先级高的运算符要先参加运算。注意,这是一个递归过程,因为 S2 中可能已存在多个运算符,它们的优先级可能都大于等于当前运算符,当这些运算符都弹出时,再将当前运算符压入 S2 栈顶
- 如果读到右括号,则将 S2 内首个左括号以上的运算符,全部加到 S1 栈尾。因为括号的优先级是最高的,立刻进行运算
例:中缀表达式 2*(3+5)+7/1-4 转换为后缀表达式
可以先转换为树,然后后序遍历得到后缀表达式,再和通过上面步骤推算出来的结果进行验证,判断是否正确。转换需要强调的是,我们用括号表示优先计算
表达式 2*(3+5)+7/1-4 中我们约定 * 和 / 的优先级高于 + 和 -,因此 + 和 - 要优先计算时需要加上括号。但是本身对于 + 和 - 来说,* 和 / 优先级高也是一种优先计算,优先计算就需要加上括号,只是我们一开始约定了先算 * 和 /,同时也为了方便,因此省略了括号
包括同级的 * 和 / 或 + 和 -,我们约定了从左往右算,其实先算左边的,也是一种优先计算,我们给优先计算的都加上括号,那么原式应为:((2*(3+5))+(7/1)) -4
强调这一点主要为了转换成树的时候方便划分左右子树,括号为一个子树的整体,这样一来转换成树的结构就很清晰了,[左子树 运算符 右子树]
后序遍历为:235+*71/+4-,即后缀表达式
此时再通过上面的步骤得到后缀表达式
可以看到最终结果也是 235+*71/+4-
详细代码见 3
3. 简易运算实现
Calculator 类
public class Calculator {
private static final Map<String, Integer> OPERATORS = MapUtil.builder("+", 1).put("-", 1).put("*", 2).put("/", 2)
.put("%", 2).put("^", 3).put("(", 0).put(")", 0).build();
private Calculator() {
}
public static double calculate(String equation) {
if (!BaseUtil.isWholeSymbol(equation)) {
throw new IllegalArgumentException("请确认括号是否完整");
}
Deque<String> operand = new ArrayDeque<>();
Deque<String> operator = new ArrayDeque<>();
for (String str : toList(equation)) {
if (NumberUtils.isCreatable(str)) {
operand.push(str);
continue;
}
Integer opt = OPERATORS.get(str);
if (null == opt) {
throw new IllegalArgumentException("操作符不合法");
}
if (StrPool.LBRACKET.value().equals(str) || operator.isEmpty() || opt > OPERATORS.get(operator.peek())) {
operator.push(str);
} else if (StrPool.RBRACKET.value().equals(str)) {
// 判断是否是右括号, 存在右括号则运算符栈必有左括号, 即运算符栈不为空
while (!operator.isEmpty()) {
if (StrPool.LBRACKET.value().equals(operator.peek())) {
operator.pop();
break;
} else {
String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
operand.push(calculate);
}
}
} else if (opt <= OPERATORS.get(operator.peek())) {
while (!operator.isEmpty() && opt <= OPERATORS.get(operator.peek())) {
String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
operand.push(calculate);
}
operator.push(str);
}
}
while (!operator.isEmpty()) {
String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
operand.push(calculate);
}
return Double.parseDouble(operand.pop());
}
public static List<String> toList(String str) {
List<String> list = new ArrayList<>();
StringBuilder builder = new StringBuilder();
String replace = str.replaceAll("\\s*", "");
char[] chars = replace.toCharArray();
for (int i = 0; i < chars.length; i++) {
boolean isMinus = '-' == chars[i] && (i == 0 || '(' == chars[i - 1]);
if (isMinus) {
builder.append(chars[i]);
continue;
}
String val = String.valueOf(chars[i]);
if (null != OPERATORS.get(val)) {
if (StringUtil.INSTANCE.isNotBlank(builder)) {
list.add(builder.toString());
}
list.add(val);
builder = new StringBuilder();
} else {
builder.append(chars[i]);
}
}
if (StringUtil.INSTANCE.isNotBlank(builder)) {
list.add(builder.toString());
}
return list;
}
private static String calculate(String operator, String val2, String val1) {
double pre = Double.parseDouble(val1);
double suf = Double.parseDouble(val2);
switch (operator) {
case "+":
return pre + suf + "";
case "-":
return pre - suf + "";
case "*":
return pre * suf + "";
case "/":
return pre / suf + "";
case "%":
return pre % suf + "";
case "^":
return Math.pow(pre, suf) + "";
default:
return "0";
}
}
}
BaseUtil 类
public class BaseUtil {
private static final Map<Character, Character> R_SYMBOL = MapUtil.builder(')', '(').put(']', '[').put('}', '{').build();
private static final List<Character> L_SYMBOL = ListUtil.list('(', '[', '{');
private BaseUtil() {
}
public static boolean isWholeSymbol(String str) {
Deque<Character> symbol = new ArrayDeque<>();
for (char ch : str.toCharArray()) {
if (R_SYMBOL.containsKey(ch)) {
if (symbol.isEmpty() || !symbol.peek().equals(R_SYMBOL.get(ch))) {
return false;
}
symbol.pop();
} else if (L_SYMBOL.contains(ch)) {
symbol.push(ch);
}
}
return symbol.isEmpty();
}
}