1 字符串算术表达式分类
字符串算术表达式分为前缀表达式、中缀表达式和后缀表达式。其中前缀表达式又称波兰表达式,后缀表达式基于前缀表达式,又称逆波兰表达式。下面给出百度百科关于几种表达式的定义:
前缀表达式是一种没有括号的算术表达式,与中缀表达式不同的是,其将运算符写在前面,操作数写在后面。为纪念其发明者波兰数学家Jan Lukasiewicz,前缀表达式也称为“波兰式”。例如,- 1 + 2 3,它等价于1-(2+3)。前缀表达式就是前序表达式,是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式方式。
中缀表达式(或中缀记法)是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间(例:3 + 4),中缀表达式是人们常用的算术表示方法。
逆波兰式(Reverse Polish Notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)。如:我们平时写a+b,这是中缀表达式,写成后缀表达式就是:ab+。
- 前缀或者后缀表达式的作用:实现逆波兰式算法,难度并不大,但为什么要将看似简单的中缀表达式转换为复杂的逆波兰式?原因就在于这个简单是相对人类的思维结构来说的,对计算机而言中序表达式是非常复杂的结构。相对的,逆波兰式在计算机看来却是比较简单易懂的结构。因为计算机普遍采用的内存结构是栈式结构,它执行先进后出的顺序。
2 前缀表达式
2.1 运算优势
前缀表达式是一种十分有用的表达式,将中缀表达式转换为前缀表达式后,就可以只依靠出栈、入栈两种简单操作完全解决中缀表达式的全部运算。
2.2 求值算法
对前缀表达式求值,要从右至左扫描表达式,首先从右边第一个字符开始判断,若当前字符是数字则一直到数字串的末尾再记录下来,若为运算符,则将右边离得最近的两个“数字串”作相应运算,然后以此作为一个新的“数字串”并记录下来;扫描到表达式最左端时扫描结束,最后运算的值即为表达式的值。
例如:对前缀表达式“- 1 + 2 3”求值,扫描到3时,记录下这个数字串,扫描到2时,记录下这个数字串,当扫描到+时,将+右移做相邻两数字串的运算符,记为2+3,结果为5,记录下5这个新数字串,然后继续向左扫描,扫描到1时,记录下这个数字串,扫描到-时,将-右移做相邻两数字串的运算符,记为1-5,结果为-4,此时关于这个表达式的全部运算已完成,故表达式的值为-4。
2.3 中缀转前缀算法
(1) 首先构造一个运算符栈(也可放置括号),运算符(以括号为分界点)在栈内遵循越往栈顶优先级不降低的原则进行排列。
(2)从右至左扫描中缀表达式,从右边第一个字符开始判断:
如果当前字符是数字,则分析到数字串的结尾并将数字串直接输出。
如果是运算符,则比较优先级。如果当前运算符的优先级大于等于栈顶运算符的优先级(当栈顶是括号时,直接入栈),则将运算符直接入栈;否则将栈顶运算符出栈并输出,直到当前运算符的优先级大于等于栈顶运算符的优先级(当栈顶是括号时,直接入栈),再将当前运算符入栈。
如果是括号,则根据括号的方向进行处理。如果是右的括号,则直接入栈;否则,遇左括号前将所有的运算符全部出栈并输出,遇右括号后将左、向右的两括号一起出栈(并不输出)。
(3) 重复上述操作(2)直至扫描结束,将栈内剩余运算符全部出栈并输出,再逆缀输出字符串。中缀表达式也就转换为前缀表达式了。
2.3 中缀转前缀算法代码实现
/**
* 中缀表达式转前缀表达式
* @param expression 中缀表达式
* @return 前缀表达式
*/
private String toPrefix(String expression) {
final Stack<Character> opStack = new Stack<>();
opStack.push(',');// 运算符放入栈底元素逗号,此符号优先级最低
char[] arr = expression.toCharArray();
int len = arr.length;
int currentIndex = len - 1 ;// 当前字符的位置
int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值
char currentOp, peekOp;// 当前操作符和栈顶操作符
// 自由向左扫描表达式
for (int i = len - 1; i >= 0; i--,currentIndex--) {
currentOp = arr[i];
if (isOperator(currentOp)) {// 如果当前字符是运算符
if (count > 0) {
stack.push(new String(arr, currentIndex + 1, count));// 取两个运算符之间的数字
}
peekOp = opStack.peek();
if (currentOp == '(') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
while (opStack.peek() != ')') {
stack.push(String.valueOf(opStack.pop()));
}
opStack.pop();
} else {
while (currentOp != ')' && peekOp != ',' && peekOp !=')' &&comparePrefix(currentOp, peekOp)) {
stack.push(String.valueOf(opStack.pop()));
peekOp = opStack.peek();
}
opStack.push(currentOp);
}
count = 0;
} else {
count++;
}
}
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex + 1]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
stack.push(new String(arr, currentIndex + 1, count));
}
while (opStack.peek() != ',') {
// 将操作符栈中的剩余的元素添加到后缀式栈中
stack.push(String.valueOf(opStack.pop()));
}
StringBuilder sb = new StringBuilder();
stack.forEach(sb::append);
return sb.toString();
}
2.4 前缀表达式求值代码实现
/**
* 计算前缀表达式的值
*
* @param expression 前缀表达式
* @return 计算结果
*/
private double calculatePrefix(String expression) {
// 中缀表达式转前缀表达式
convert(transform(expression), ExpressionType.PREFIX);
// 反转栈
Collections.reverse(stack);
Stack<String> resultStack = new Stack<>();
String firstValue = null, secondValue = null, currentOp;// 参与计算的第一个值,第二个值和算术运算符
while (false == stack.isEmpty()) {
currentOp = stack.pop();
if (false == isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中
currentOp = currentOp.replace("~", "-");
resultStack.push(currentOp);
} else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
firstValue = resultStack.pop();
secondValue = resultStack.pop();
// 将负数标记符改为负号
firstValue = firstValue.replace("~", "-");
secondValue = secondValue.replace("~", "-");
BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0));
resultStack.push(tempResult.toString());
}
}
return Double.parseDouble(resultStack.pop());
}
3 中缀表达式
就是我们平常书写的表达式形式,不在赘述。
4 后缀表达式
逆波兰式(Reverse Polish Notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)。
4.1 中缀转后缀算法
将一个普通的中缀表达式转换为逆波兰表达式的一般算法是:
首先需要分配2个栈,一个作为临时存储运算符的栈S1(含一个结束符号),一个作为存放结果(逆波兰式)的栈S2(空栈),S1栈可先放入优先级最低的运算符#,注意,中缀式应以此最低优先级的运算符结束。可指定其他字符,不一定非#不可。从中缀式的左端开始取字符,逐序进行如下步骤:
(1)若取出的字符是操作数,则分析出完整的运算数,该操作数直接送入S2栈。
(2)若取出的字符是运算符,则将该运算符与S1栈栈顶元素比较,如果该运算符(不包括括号运算符)优先级高于S1栈栈顶运算符(包括左括号)优先级,则将该运算符进S1栈,否则,将S1栈的栈顶运算符弹出,送入S2栈中,直至S1栈栈顶运算符(包括左括号)低于(不包括等于)该运算符优先级时停止弹出运算符,最后将该运算符送入S1栈。
(3)若取出的字符是“(”,则直接送入S1栈顶。
(4)若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
(5)重复上面的1~4步,直至处理完所有的输入字符。
(6)若取出的字符是“#”,则将S1栈内所有运算符(不包括“#”),逐个出栈,依次送入S2栈。
完成以上步骤,S2栈便为逆波兰式输出结果。不过S2应做一下逆序处理。便可以按照逆波兰式的计算方法计算了!
4.2 中缀转后缀代码实现
/**
* 中缀表达式转后缀表达式
* @param expression 中缀表达式
* @return 后缀表达式
*/
private String toPostfix(String expression) {
final Stack<Character> opStack = new Stack<>();
opStack.push(',');// 运算符放入栈底元素逗号,此符号优先级最低
char[] arr = expression.toCharArray();
int currentIndex = 0;// 当前字符的位置
int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值
char currentOp, peekOp;// 当前操作符和栈顶操作符
for (int i = 0; i < arr.length; i++) {
currentOp = arr[i];
if (isOperator(currentOp)) {// 如果当前字符是运算符
if (count > 0) {
stack.push(new String(arr, currentIndex, count));// 取两个运算符之间的数字
}
peekOp = opStack.peek();
if (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
while (opStack.peek() != '(') {
stack.push(String.valueOf(opStack.pop()));
}
opStack.pop();
} else {
while (currentOp != '(' && peekOp != ',' && peekOp != '(' &&comparePostfix(currentOp, peekOp)) {
stack.push(String.valueOf(opStack.pop()));
peekOp = opStack.peek();
}
opStack.push(currentOp);
}
count = 0;
currentIndex = i + 1;
} else {
count++;
}
}
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
stack.push(new String(arr, currentIndex, count));
}
while (opStack.peek() != ',') {
// 将操作符栈中的剩余的元素添加到后缀式栈中
stack.push(String.valueOf(opStack.pop()));
}
StringBuilder sb = new StringBuilder();
stack.forEach(sb::append);
return sb.toString();
}
4.3 后缀求值算法
新建一个表达式,如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。
4.4 后缀求值实现
/**
* 计算后缀表达式的值
*
* @param expression 后缀表达式
* @return 计算结果
*/
private double calculatePostfix(String expression) {
// 中缀转后缀
convert(transform(expression), ExpressionType.POSTFIX);
// 反正后缀栈
Collections.reverse(stack);
Stack<String> resultStack = new Stack<>();
String firstValue, secondValue , currentOp;// 参与计算的第一个值,第二个值和算术运算符
while (false == stack.isEmpty()) {
currentOp = stack.pop();
if (false == isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中
currentOp = currentOp.replace("~", "-");
resultStack.push(currentOp);
} else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
secondValue = resultStack.pop();
firstValue = resultStack.pop();
// 将负数标记符改为负号
firstValue = firstValue.replace("~", "-");
secondValue = secondValue.replace("~", "-");
BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0));
resultStack.push(tempResult.toString());
}
}
return Double.parseDouble(resultStack.pop());
}
5 基于栈的简单计算器实现
此实现的参考cn.hutool.core.math.Calculator,其中hutool使用的是后缀表达式的方式;我们的实现也给出了前缀表达式的计算方式,默认使用后缀表达式形式。
完整如下5-1代码所示:也可以去后面仓库代码中查看完整代码及测试代码
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Stack;
/**
* @author Administrator
* @version 1.0
* @description 简单计算器
* @date 2022-10-31 20:26
*/
public class Calculator {
/**
* 表达式栈
*/
private final Stack<String> stack = new Stack<>();//
/**
* 运用运算符ASCII码-40做索引的运算符优先级,%单独判断优先级和*,/同级
*/
private final int[] OperatorPriorities = new int[]{4, 4, 2, 1, -1, 1, 3, 2};
/**
* 计算表达式的值
*
* @param expression 表达式
* @return 计算结果
*/
public double execute(String expression, ExpressionType type) {
if (type == null || ExpressionType.POSTFIX.equals(type)) {
return calculatePostfix(expression);
} else if (type.equals(ExpressionType.PREFIX)) {
return calculatePrefix(expression);
} else {
throw new IllegalStateException("Unexpected type: " + type);
}
}
/**
* 计算表达式的值
*
* @param expression 表达式
* @return 计算结果
*/
public double execute(String expression) {
return calculatePostfix(expression);
}
/**
* 计算后缀表达式的值
*
* @param expression 后缀表达式
* @return 计算结果
*/
private double calculatePostfix(String expression) {
// 中缀转后缀
convert(transform(expression), ExpressionType.POSTFIX);
// 反正后缀栈
Collections.reverse(stack);
Stack<String> resultStack = new Stack<>();
String firstValue, secondValue , currentOp;// 参与计算的第一个值,第二个值和算术运算符
while (false == stack.isEmpty()) {
currentOp = stack.pop();
if (false == isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中
currentOp = currentOp.replace("~", "-");
resultStack.push(currentOp);
} else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
secondValue = resultStack.pop();
firstValue = resultStack.pop();
// 将负数标记符改为负号
firstValue = firstValue.replace("~", "-");
secondValue = secondValue.replace("~", "-");
BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0));
resultStack.push(tempResult.toString());
}
}
return Double.parseDouble(resultStack.pop());
}
/**
* 计算前缀表达式的值
*
* @param expression 前缀表达式
* @return 计算结果
*/
private double calculatePrefix(String expression) {
// 中缀表达式转前缀表达式
convert(transform(expression), ExpressionType.PREFIX);
// 反转栈
Collections.reverse(stack);
Stack<String> resultStack = new Stack<>();
String firstValue = null, secondValue = null, currentOp;// 参与计算的第一个值,第二个值和算术运算符
while (false == stack.isEmpty()) {
currentOp = stack.pop();
if (false == isOperator(currentOp.charAt(0))) {// 如果不是运算符则存入操作数栈中
currentOp = currentOp.replace("~", "-");
resultStack.push(currentOp);
} else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
firstValue = resultStack.pop();
secondValue = resultStack.pop();
// 将负数标记符改为负号
firstValue = firstValue.replace("~", "-");
secondValue = secondValue.replace("~", "-");
BigDecimal tempResult = calculate(firstValue, secondValue, currentOp.charAt(0));
resultStack.push(tempResult.toString());
}
}
return Double.parseDouble(resultStack.pop());
}
/**
* 数据准备阶段将表达式转换为指定类型的表达式
*
* @param expression 表达式
* @param type 表达式类型
*/
public String convert(String expression, ExpressionType type) {
if (type == null || ExpressionType.POSTFIX.equals(type)) {
return toPostfix(expression);
} else if (type.equals(ExpressionType.PREFIX)) {
return toPrefix(expression);
} else {
throw new IllegalStateException("Unexpected type: " + type);
}
}
/**
* 数据准备阶段将表达式转换成为后缀式栈
*
* @param expression 表达式
*/
public String convert(String expression) {
return toPostfix(expression);
}
/**
* 中缀表达式转前缀表达式
* @param expression 中缀表达式
* @return 前缀表达式
*/
private String toPrefix(String expression) {
final Stack<Character> opStack = new Stack<>();
opStack.push(',');// 运算符放入栈底元素逗号,此符号优先级最低
char[] arr = expression.toCharArray();
int len = arr.length;
int currentIndex = len - 1 ;// 当前字符的位置
int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值
char currentOp, peekOp;// 当前操作符和栈顶操作符
// 自由向左扫描表达式
for (int i = len - 1; i >= 0; i--,currentIndex--) {
currentOp = arr[i];
if (isOperator(currentOp)) {// 如果当前字符是运算符
if (count > 0) {
stack.push(new String(arr, currentIndex + 1, count));// 取两个运算符之间的数字
}
peekOp = opStack.peek();
if (currentOp == '(') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
while (opStack.peek() != ')') {
stack.push(String.valueOf(opStack.pop()));
}
opStack.pop();
} else {
while (currentOp != ')' && peekOp != ',' && peekOp !=')' &&comparePrefix(currentOp, peekOp)) {
stack.push(String.valueOf(opStack.pop()));
peekOp = opStack.peek();
}
opStack.push(currentOp);
}
count = 0;
} else {
count++;
}
}
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex + 1]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
stack.push(new String(arr, currentIndex + 1, count));
}
while (opStack.peek() != ',') {
// 将操作符栈中的剩余的元素添加到后缀式栈中
stack.push(String.valueOf(opStack.pop()));
}
StringBuilder sb = new StringBuilder();
stack.forEach(sb::append);
return sb.toString();
}
/**
* 中缀表达式转后缀表达式
* @param expression 中缀表达式
* @return 后缀表达式
*/
private String toPostfix(String expression) {
final Stack<Character> opStack = new Stack<>();
opStack.push(',');// 运算符放入栈底元素逗号,此符号优先级最低
char[] arr = expression.toCharArray();
int currentIndex = 0;// 当前字符的位置
int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值
char currentOp, peekOp;// 当前操作符和栈顶操作符
for (int i = 0; i < arr.length; i++) {
currentOp = arr[i];
if (isOperator(currentOp)) {// 如果当前字符是运算符
if (count > 0) {
stack.push(new String(arr, currentIndex, count));// 取两个运算符之间的数字
}
peekOp = opStack.peek();
if (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
while (opStack.peek() != '(') {
stack.push(String.valueOf(opStack.pop()));
}
opStack.pop();
} else {
while (currentOp != '(' && peekOp != ',' && peekOp != '(' &&comparePostfix(currentOp, peekOp)) {
stack.push(String.valueOf(opStack.pop()));
peekOp = opStack.peek();
}
opStack.push(currentOp);
}
count = 0;
currentIndex = i + 1;
} else {
count++;
}
}
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
stack.push(new String(arr, currentIndex, count));
}
while (opStack.peek() != ',') {
// 将操作符栈中的剩余的元素添加到后缀式栈中
stack.push(String.valueOf(opStack.pop()));
}
StringBuilder sb = new StringBuilder();
stack.forEach(sb::append);
return sb.toString();
}
/**
* 判断是否为算术符号
*
* @param c 字符
* @return 是否为算术符号
*/
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')' || c == '%';
}
/**
* 利用ASCII码-40做下标去算术符号优先级
*
* @param cur 下标
* @param peek peek
* @return 优先级,如果cur高或相等,返回true,否则false
*/
private boolean comparePostfix(char cur, char peek) {// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低
final int offset = 40;
if(cur == '%'){
// %优先级最高
cur = 47;
}
if(peek == '%'){
// %优先级最高
peek = 47;
}
return OperatorPriorities[peek - offset] >= OperatorPriorities[cur - offset];
}
private boolean comparePrefix(char cur, char peek) {// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低
final int offset = 40;
if(cur == '%'){
// %优先级最高
cur = 47;
}
if(peek == '%'){
// %优先级最高
peek = 47;
}
return OperatorPriorities[peek - offset] > OperatorPriorities[cur - offset];
}
/**
* 按照给定的算术运算符做计算
*
* @param firstValue 第一个值
* @param secondValue 第二个值
* @param currentOp 算数符,只支持'+'、'-'、'*'、'/'、'%'
* @return 结果
*/
private BigDecimal calculate(String firstValue, String secondValue, char currentOp) {
BigDecimal result;
switch (currentOp) {
case '+':
result = NumberUtil.add(firstValue, secondValue);
break;
case '-':
result = NumberUtil.sub(firstValue, secondValue);
break;
case '*':
result = NumberUtil.mul(firstValue, secondValue);
break;
case '/':
result = NumberUtil.div(firstValue, secondValue);
break;
case '%':
result = NumberUtil.toBigDecimal(firstValue).remainder(NumberUtil.toBigDecimal(secondValue));
break;
default:
throw new IllegalStateException("Unexpected value: " + currentOp);
}
return result;
}
/**
* 将表达式中负数的符号更改
*
* @param expression 例如-2+-1*(-3E-2)-(-1) 被转为 ~2+~1*(~3E~2)-(~1)
* @return 转换后的字符串
*/
private static String transform(String expression) {
expression = StrUtil.cleanBlank(expression);
expression = StrUtil.removeSuffix(expression, "=");
final char[] arr = expression.toCharArray();
for (int i = 0; i < arr.length; i++) {
if (arr[i] == '-') {
if (i == 0) {
arr[i] = '~';
} else {
char c = arr[i - 1];
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == 'E' || c == 'e') {
arr[i] = '~';
}
}
}
}
if (arr[0] == '~' && (arr.length > 1 && arr[1] == '(')) {
arr[0] = '-';
return "0" + new String(arr);
} else {
return new String(arr);
}
}
}
7 思考
下面留下一些问题小伙伴来回答下:
- 我们实现的中缀转后缀与hutool中实现有什么不同?
- 提示:一个判断条件
- 我们实现的前缀计算和后缀计算有什么不同之处?
- 提示:与中缀转相应表达式时读取原中缀表达式顺序有关
- 如何用我们自己实现的栈结构替换JDK中的Stack呢?
欢迎小伙伴下方留言讨论。
6 后记
如果小伙伴什么问题或者指教,欢迎交流。
⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm
参考:
[1]百度百科.<a https://baike.baidu.com/item/%E5%89%8D%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F">前缀表达式[EB/OL].2022-09-19/2022-11-02.
[2]百度百科.逆波兰式[EB/OL].2022-09-19/2022-11-02.
[3]gitee.cn.hutool.core.math.Calculator[EB/OL].2022-10-02/2022-11-02.