文章目录
一、前缀、中缀、后缀表达式
1.1 前缀表达式
1.1.1 定义
前缀表达式又称为波兰式。前缀表达式的运算符位于操作数字之前。
例如:
(3+4)×5-6
对应的前缀表达式是:
- × + 3 4 5 6
1.1.2 计算机求值
前缀表达式(波兰式)的计算机求值步骤如下:
- 将波兰式从右至左扫描;
- 遇到数字时,将数字压入堆栈;
- 遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(用次顶元素对栈顶元素做运算),并将运算结果入栈;
- 重复上述过程直到表达式的最左端,最后栈中剩余的值就是表达式的结果。
1.2 中缀表达式
1.2.1 定义
中缀表达式就是常见的运算表达式本身。
例如:
(3+4)×5-6
1.2.2 计算机求值
中缀表达式的计算机求值步骤如下:
-
初始化两个栈,一个作为符号栈、一个作为数字栈;
-
通过一个索引
index
,来从左至右遍历中缀表达式; -
如果遍历到的是一个数字,就直接入数字栈;
-
如果遍历到的是一个符号:
-
如果当前符号栈为空,就直接入符号栈;
-
如果符号栈有操作符,就进行比较:
- 若当前的操作符优先级小于或等于栈顶的操作符,就从数字栈中
pop
出两个数,再从符号栈中pop
出一个符号进行运算。运算得到的结果push
入数字栈中,然后将当前的操作符入符号栈; - 若当前的操作符优先级大于栈顶的操作符,就直接入符号栈;
- 若当前的操作符优先级小于或等于栈顶的操作符,就从数字栈中
-
-
中缀表达式遍历完毕之后,就依次从数字栈和符号栈中
pop
出相应的数和符号,对他们进行运算; -
最后在数字栈中将只剩下一个数字,这个数字就是表达式的结果。
1.3 后缀表达式
1.3.1 定义
后缀表达式又称为逆波兰表达式。它与前缀表达式相似,只是运算符位于操作数字之后。
例如:
(3+4)×5-6
对应的后缀表达式是:
3 4 + 5 × 6 -
1.3.2 计算机求值
后缀表达式(逆波兰式)的计算机求值步骤如下:
-
从左至右扫描逆波兰式;
-
遇到数字时,将数字压入堆栈;
-
遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(用次顶元素对栈顶元素做运算),并将运算结果入栈;
-
重复上述过程直到逆波兰式的最右端,最后栈中剩余的值就是表达式的结果。
1.4 中缀表达式转换为后缀表达式
1.4.1 为什么要转换
从上面前缀、中缀、后缀表达式的计算机求值步骤中我们不难发现:中缀表达式的计算机求值过程最为繁琐,而前缀、后缀表达式的求值过程条理清晰、非常简单。
相对于后缀表达式的计算机求值过程,前缀表达式需要从右至左扫描波兰式,显然这并不符合我们平常书写代码的习惯(当然我们可以借助工具类如 StringBuilder
等将波兰式反转之后,便可以从左至右扫描,但是何必要多此一举呢?)。
我们不难得出结论:相较于前缀和中缀表达式,后缀表达式的计算机求值过程最为简单、明晰。
然而,我们人为书写的表达式都是中缀表达式,不可能直接就给你个后缀表达式让你来计算。所以我们需要先将中缀表达式转换为后缀表达式,再做计算。
1.4.2 转换步骤
中缀表达式转换为后缀表达式的具体步骤如下:
-
初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
-
从左至右扫描中缀表达式;
-
遇到操作数时, 将其压入 s2;
-
遇到运算符时, 比较该运算符与 s1 栈顶运算符的优先级:
- 如果 s1 为空,则直接将此运算符入栈;
- 如果 s1 不为空:
- 若栈顶运算符为左括号 “(”,则直接将此运算符入栈 s1;
- 若该运算符优先级比栈顶运算符的高, 也将运算符压入 s1;
- 若优先级低于栈顶运算符,则将 s1 栈顶的运算符弹出并压入到 s2 中, 再次转到步骤 4 (即本步骤重新开始)与 s1 中新的栈顶运算符相比较;
-
遇到括号时:
- 如果是左括号 “(” , 则直接压入 s1
- 如果是右括号 “)” , 则依次弹出 s1 栈顶的运算符, 并压入 s2, 直到遇到左括号 “(” 为止, 此时将这一对括号丢弃;
-
重复步骤 2 至 5, 直到表达式的最右边;
-
将 s1 中剩余的运算符依次弹出并压入 s2;
-
依次弹出 s2 中的元素并输出, 结果的逆序即为中缀表达式对应的后缀表达式 。
1.4.3 图解转换示例
求解 1+((2+3)x4)-5
对应的后缀表达式 ?
按照中缀表达式转后缀表达式的步骤,最终求得的后缀表达式为:1 2 3 + 4 × + 5 –
。
二、实现逆波兰计算器
2.1 需求描述
要求实现一个逆波兰计算器,该计算器可以将中缀表达式转换为后缀表达式进行计算。该计算器的具体需求可以描述如下:
- 可以计算含有多余空格的中缀表达式
- 支持小数运算
- 支持加、减、乘、除运算
例:
输入:1.1+ ((12 +3)*2.4) -5
输出:32.1
2.2 思路分析
要实现一个满足上述需求的逆波兰计算器,基本思路需要按照如下步骤:
- 首先将表达式中多余的空格去除;
- 将中缀表达式拆分为一个个操作数、运算符存放到一个
List
集合中,需要注意拆分过程中对多位数、小数的处理; - 将中缀表达式的
List
集合转换成后缀表达式的List
集合;(关于这个转换过程,参考上面 1.4.2 转换步骤) - 按照后缀表达式的计算机求值过程求得表达式的值。(求值过程,参考上面 1.3.2 计算机求值)
2.3 代码实现
具体的代码实现将按照上面的思路步骤来。
首先是将表达式中的空格去掉并将表达式中的元素拆分存放到 List
集合中:
/**
* @Description 将中缀表达式转换到 list 集合中存放
* 之所以这么做,主要是为了方便对中缀表达式中两位数以上的数字进行处理
*/
public static List<String> getList(String str){
ArrayList<String> list = new ArrayList<>();
StringBuilder numStr = new StringBuilder();
// 去掉表达式中多余的空格
str = str.replaceAll(" ", "");
// 遍历表达式字符
for (int i=0; i<str.length(); i++){
char c = str.charAt(i);
// 判断当前字符是数字还是符号
if (!(48 < c && c < 57) && !".".equals(c+"")){ // 如果是运算符
list.add(c + ""); // 把字符转为字符串后,添加到 list
}else{ // 如果是数字或者小数点
numStr.append(c);
// 需要判断当前字符的下个字符是否依然是数字或小数点
// 但是首先需要判断当前字符还有没有下一个字符,即是否是最后一个字符
if (!(i == str.length() - 1)){ // 如果不是最后一个字符
char c1 = str.charAt(i + 1);
if (!(c1 > 48 && c1 < 57) && !".".equals(c1+"")){ // 如果不是数字和小数点,就将拼接好的数字添加到 list
list.add(numStr.toString());
numStr = new StringBuilder();
}
}else{ // 如果是最后一个字符,就添加到 list
list.add(numStr.toString());
}
}
}
return list;
}
然后将中缀表达式元素的 List
集合转换到后缀表达式元素的 List
集合:
/**
* @Description 根据中缀表达式的 list 集合来获得后缀表达式的 list 集合
*/
public static List<String> getPostExpression(List<String> list){
Stack<String> s1 = new Stack<>(); // 运算符栈
// 因为 s2 中最终存放的元素的倒序才是后缀表达式,用 list 来代替 stack 则无需反转
ArrayList<String> s2 = new ArrayList<>(); // 集合代替临时存储栈
// 遍历中缀表达式的元素列表
for (String item : list){
if (item.matches("([1-9]\\d*\\.?\\d+)|(0\\.\\d*[1-9])|(\\d+)")){ // 该正则表达式匹配多位数(含小数)
s2.add(item);
}else if ("(".equals(item)){ // 如果是左括号,直接入栈
s1.push(item);
}else if (")".equals(item)){ // 如果是右括号
// s1 需要 pop 出栈顶的元素,然后 push 到 s2 中,直到遇见左括号抵消右括号
while (!"(".equals(s1.peek()) && s1.size() > 0){
s2.add(s1.pop());
}
s1.pop(); // pop 出左括号
}else{ // 如果是运算符
while(true){
// 如果 s1 为空或者栈顶为 "(",直接入栈
if (s1.isEmpty() || "(".equals(s1.peek())){
s1.push(item);
break;
}else{ // 如果不为空,且栈顶不为 "("
// 如果运算符优先级大于 s1 栈顶,直接入栈
if (getPriority(item) > getPriority(s1.peek())){
s1.push(item);
break;
}else{
// 如果运算符优先级小于 s1 栈顶,则栈顶元素出栈并入栈到 s2 中,然后继续循环判断
s2.add(s1.pop());
}
}
}
}
}
// 把 s1 的剩余运算符全部读出来
while (!(s1.isEmpty())){
s2.add(s1.pop());
}
return s2;
}
最后是对后缀表达式元素 List
集合的计算机求值:
/**
* @Description 根据后缀表达式元素的 List 集合计算结果
*/
public static Float calculate(List<String> list){
Stack<Float> numStack = new Stack<>();
for (String item : list){
// 该正则表达式匹配多位数(含小数)
if (item.matches("([1-9]\\d*\\.?\\d+)|(0\\.\\d*[1-9])|(\\d+)")){ // 如果是操作数,直接入栈
numStack.push(Float.valueOf(item));
}else{ // 如果是运算符,开始计算,计算后结果入栈
Float num_1 = numStack.pop();
Float num_2 = numStack.pop();
switch (item){
case "+":
numStack.push(num_2 + num_1);
break;
case "-":
numStack.push(num_2 - num_1);
break;
case "*":
numStack.push(num_2 * num_1);
break;
case "/":
numStack.push(num_2 / num_1);
break;
default:
throw new RuntimeException("运算符错误!");
}
}
}
return numStack.pop();
}
/**
* @Description 获取运算符优先级
*/
public static int getPriority(String operation){
switch (operation){
case "+":
case "-":
return 0;
case "*":
case "/":
return 1;
default:
return -1;
}
}
至此,逆波兰计算器的核心代码已经全部完成。
完整代码实现已经上传到 Gitee 仓库:点击此处跳转。