👏 Hi! 我是 Yumuing,一个技术的敲钟人
👨💻 每天分享技术文章,永远做技术的朝拜者
📚 欢迎关注我的博客:Yumuing's blog
平时我们进行数学计算使用的常见书写方式就是中缀表达式,即每一个运算符号都位于计算数的中间,如下:
$$ (1+2)\div3 $$
而这对于计算机进行求取结果来说,并不是一个最优的方案,毕竟每次读取到一个运算符,都得判断一次优先级,并且需要确定两个计算数的目标位置。在四则运算的基础之上,还在添加一个括号的使用,这将更为复杂,中缀表达式将更加对计算机不友好。那什么样的表达式结构会是计算机较为优化的呢?
答案就是逆波兰表达式,也叫做后缀表达式,它最大的好处就是无需再考虑运算符的优先级,也无需考虑计算数目标位置。当然,完成逆波兰表达式的转换,仍需要对运算发优先级进行判断,但给出一个逆波兰表达式,就可以直接读取计算,无需考虑优先级。
逆波兰表达式特征:
- 不存在括号运算符来约束,也无需考虑运算符的优先级
- 运算符的两个计算数位于运算符前两位
- 严格遵循“从左到右”运算,越后的运算符越晚计算
$1+2\div3$ 的逆波兰表达式如下:
$$ 1\quad2\quad3\quad\div\quad- $$
而实现一个带有括号的使用的中缀表达式转换为逆波兰表达式的思路,既可以以二叉树的后序遍历来实现,也可以使用栈的方式的实现,这里将采用栈的方式实现。对于括号的处理,我们可以从它的性质下手,括号的出现一定是成对存在的,有左括号,必定有右括号。只要在读取的过程中,遇到左括号就入栈,无论多少重左括号,直至遇到右括号,出栈直至左括号出栈,期间进行数字运算。
算法思路
实现一个中缀表达式转换成后缀表达式,再依据后缀表达式计算出最后的运算结果的基本思路如下:
将中缀表达式转换为后缀表达式
定义两个栈:符号栈 s1、中间结果栈 s2
注:s2 采用 add() 方法入栈,保证顺序输出为所需结果,且其无需弹栈,采用 List 作为栈的存储结构,s1 都可以。
定义一个中缀表达式字符串 expression2
遍历中缀表达式,将中缀表达式转成对应的list
一一遍历中缀表达式,并判断字符类型
数字:
入栈中间结果栈 s2
左括号:
入栈符号栈 s1
右括号:
依次弹出 s1 栈顶元素,并压入 s2,直至遇到左括号为止,将左括号弹出 s1 栈,消除括号
加减乘除运算符:
获取当前遍历字符优先级,当小于或等于栈顶优先级,或为左括号时,s1 栈顶运算符弹出并压入 s2 中,最后将当前字符压入 s1 栈中
将中间结果栈顺序输出为最终结果
如下即为使用栈结构实现逆波兰表达式的基本思路图示
![逆波兰表达式](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3cc8f56282a84e7c901e781e735e5e91~tplv-k3u1fbpfcp-zoom-1.image)
读取并计算后缀表达式结果
- 定义结果栈
- 对逆波兰表达式一一遍历,判断字符
- 若为数字,压入栈中
- 若为运算符,依次弹出栈中两个元素,进行运算,将结果入栈
- 遍历结束,将结果出栈,即为最终答案
代码如下
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;
public class RPN {
public static void main(String[] args) {
// 完成一个中缀表达式转换成后缀表达式
// 说明
// 1. 1+((2+3)*4)-5 => 1 2 3 + 4 * + 5 -
// 2.因为直接对str进行操作,不方便,因此先将“1+((2+3)*4)-5” =>转成中缀表达式
// 即“1+((2+3)*4)-5” => ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]
// 3. 将得到的中缀表达式对应的list转成后缀表达式对应的list
// 即 ArrayList[1,+,(,(,2,+,3,),*,4,),-,5] => ArrayList[1, 2 ,3, +, 4, *, +, 5, -]
Scanner in = new Scanner(System.in);
String expression2 = in.next();
List<String> infixExpressionList = toInfixExpressionList(expression2);
System.out.println("中缀表达式对应List" + infixExpressionList);
List<String> parseSuffixExpressionList = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的list" + parseSuffixExpressionList);
System.out.println(expression2 + "结果是" + calculate(parseSuffixExpressionList));
}
// 方法: 将得到的中缀表达式对应的list 转换成 后缀表达式对应的list
public static List<String> parseSuffixExpressionList(List<String> ls) {
// 定义两个栈
Stack<String> s1 = new Stack<String>(); // 符号栈
List<String> s2 = new ArrayList<String>(); // 存储中间结果的栈,因为该栈一直没有弹出过,所以使用List存储
// 遍历ls
for (String item : ls) {
//如果是一个数就入栈,入栈s2,\\d+ 匹配数字且至少出现一次
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
// 如果是左括号就入栈,入栈s1
s1.push(item);
} else if (item.equals(")")) {
// 如果是右括号,则依次弹出S1栈顶的运算符,并压入S2,直到遇到左括号为止
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop(); // 将 ( 弹出s1栈,消除括号,小括号
} else {
// 当 item 的优先级,小于或者等于栈顶运算符的优先级的时候,就应该将s1栈顶的运算夫弹出并压入s2中
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
// 还需要将item压入栈
s1.push(item);
}
}
// 将s1中剩余的元素压入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2; // 因为是存放到List,因此按顺组输出就是对应的逆波兰表达式的List
}
// 方法:将中缀表达式转成对应的list
public static List<String> toInfixExpressionList(String s) {
// 定义一个List,存放中缀表达式 对应的内容
List<String> ls = new ArrayList<>();
int i = 0; // 这个相当于一个指针,用于遍历中缀表达式字符串s
String str = ""; // 用于对多位数的拼接工作
char c; // 每遍历到一个字符就放到c
do {
// 如果c事一个非数字,我们就需要加入到ls中去
if ((c = s.charAt(i)) < '0' || (c = s.charAt(i)) > '9') {
ls.add("" + c);
i++; // 需要后移
} else {
// 如果是数字,需要考虑多位数的问题
str = ""; // 先将 str 置成空串
while (i < s.length() && ((c = s.charAt(i)) >= '0' && (c = s.charAt(i)) <= '9')) {
str += c; // 拼接
i++;
}
ls.add(str);
}
}
while (i < s.length());
return ls;
}
// 将一个逆波兰表达式,依次将数据和运算符 放入ArrayList
public static List<String> getListString(String expression) {
// 将 expression 分割
String[] split = expression.split(" ");
List<String> list = new ArrayList<String>();
for (String ele : split) {
list.add(ele);
}
return list;
}
// 完成对逆波兰表达式的运算
/*
1. 从左至右扫描,将 3 和 4 压入堆栈
2. 遇到 + 运算符,因此弹出 4 和 3 (4为栈顶元素,3 为次顶元素),计算出 3 + 4 的值,得 7, 再将 7 入栈
3. 将 5 入栈
4. 接下来是 X 运算符,因此弹出 5 和 7,计算出 7 X 5 = 35, 将 35 入栈
5. 将 6 入栈
6. 最后是 - 运算符,计算出 35 - 6(减法运算或者除法运算的时候,后缀表达式是 次顶元素 减去或除以 堆顶元素) 的值,即 29 ,由此得出最终结果
*/
public static int calculate(List<String> ls) {
// 创建一个栈,只需要一个栈即可
Stack<String> stack = new Stack<>();
// 遍历 ls
for (String item : ls) {
// 这里使用一个正则表达式取出数
if (item.matches("\\d+")) {
// 匹配的是多位数
stack.push(item);
} else {
// pop 出两个数并运算,在入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("符号有问题");
}
// 把res入栈,入栈的时候要将res转换成字符,因为我们的栈是字符串类型的
stack.push(res + "");
}
}
// 最后留在stack的数据是运算结果
return Integer.parseInt(stack.pop());
}
}
// 编写一个类Operation 可以返回一个运算符 对应的优先级
class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 3;
// 写一个方法,返回对应的
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
}
return result;
}
}