1、什么是前缀、中缀、后缀表达式
首先说明一下中缀表达式,接着再来介绍前缀表达式,后缀表达式。其实前缀表达式、后缀表达式的出现主要是为了解决计算机运算问题,这和计算机线性读取内容的特性有关。
1.1、中缀表达式
事实上,中缀表达式就是我们熟悉的数学表达式。 例如下面的表达式:
这种表达式对于我们人类理解和计算没有任何问题,人类分析表达式是从全体出发,非线性读取表达式并且分析运算顺序的,但是计算机可不像我们人类这么聪明。
计算机是按照从前到后、从左到右的线性读取方式来读取操作数和操作符,等到读取足够的信息来执行一个运算时,找到两个操作数和一个操作符进行运算,有时候如果后面是更高级别的操作符或者括号时,就必须推迟运算,必须要解析到后面级别高的运算,然后回头来执行前面的运算。我们发现这个过程是极其繁琐的,而计算机是一个机器,只认识高低电平,想要完成一个简单表达式的计算,我们可能要设计出很复杂的逻辑电路来控制计算过程,那更不用说很复杂的算术表达式,所以这样来解析算术表达式是不合理的,那么我们应该采取什么办法呢?因此,我们引出了前缀表达式和后缀表达式,这两种表达式便于计算机的运算。
1.2、前缀表达式
前缀表达式,指的是不包含括号,运算符放在两个运算对象的前面,严格从右向左进行(不再考虑运算符的优先规则),所有的计算按运算符出现的顺序。 前缀表达式也被称为波兰表达式。例如下面的表达式:
1.3、后缀表达式
后缀表达式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则)。 后缀表达式也被称为逆波兰表达式。例如下面的表达式:
1.4、前缀、后缀表达式对比分析
相同点:
- 都能被计算机理解
- 都没有括号
不同点:
- 前缀表达式的运算符在运算对象之前,后缀表达式的运算符在运算对象之后。
2、转换方法
2.1、中缀表达式转前缀表达式
中缀表达式转后缀表达式有3种转换方式,分别是快速方式、辅助栈方式、二叉树方式。
2.1.1、快速方式
快速方式只是我们人类快速将中缀表达式转成后缀表达式的方式(这种方式不能在计算机中实现,一般用于线下做题时使用)。步骤如下:\
- 按照从左到右、先加减后乘除、先括号里的后括号外的运算顺序,将表达式全部都添加上括号(原本有括号的就不用再加了)。
- 从里到外将所有运算符都拿到左括号的左边。
- 去掉所有的括号。
示例如下: 假定有中缀表达式1+((2+3)*4)–5,请将它转化为前缀表达式。\
a.先按照运算顺序添加括号
((1+((2+3)*4))-5)
b.从里到外将运算符移到括号右边
-(+(1*(+(23)4))5)
c.去掉所有括号
-+1*+2345
2.1.2、辅助栈方式
辅助栈方式可以在计算机中实现,将运算符保存在辅助栈中,将转换后的前缀表达式保存在字符串中。具体步骤如下:\
- 初始化一个大小合适的运算符栈s1。
- 从右到左扫描中缀表达式。
- 遇到运算对象时,将运算对象追加到保存到后缀表达式的字符串中。
- 遇到运算符时,进行如下判断:
如果运算符为括号,进行如下判断:\
- 运算符为右括号),则直接将运算符入栈。\
- 运算符为左括号(,则依次从s1栈中弹出运算符并追加到字符串中,直到遇到右括号)此时将这一对括号丢弃。\
如果运算符不是括号:\
- 栈顶元素为右括号)或者s1栈为空,直接将刚弹出的右括号)和运算符依次入s1栈。
- 否则,比较运算符与s1栈顶元素的优先级大小。
如果运算符的优先级大于等于(注意这里是大于等于)栈顶元素时,直接将运算符入s1栈。
如果运算符的优先级小于栈顶元素时,则将刚弹出的元素追加到字符串,并继续弹出s1栈中的元素并执行步骤4。
- 重复步骤2-4,直到表达式的最左边。
- 将s1栈中的元素全部弹出并追加到字符串中,并将字符串反转。
2.1.3、二叉树方式
首先,要将中缀表达式转换成表达式树。如果小伙伴不会请参考中缀表达式转换成表达式树 表达式树如下:\
前序遍历表达式树即可得到前缀表达式。
2.2、中缀表达式转后缀表达式
中缀表达式转后缀表达式也有3种转换方式,分别是快速方式、辅助栈方式、二叉树方式。
2.2.1、快速方式
步骤如下:\
- 按照从左到右、先加减后乘除、先括号里的后括号外的运算顺序,将表达式全部都添加上括号(原本有括号的就不用再加了)。
- 从里到外将所有运算符都拿到右括号的右边。
- 去掉所有的括号。
示例如下: 假定有中缀表达式1+((2+3)*4)–5,请将它转化为后缀表达式。\
a.先按照运算顺序添加括号
((1+((2+3)*4))-5)
b.从里到外将运算符移到括号右边
((1((23)+4)*)+5)-
c.去掉所有括号
123+4*+5–
2.2.2、辅助栈方式
将运算符保存在辅助栈中,将转换后的后缀表达式保存在字符串中。具体步骤如下:\
- 初始化一个大小合适的运算符栈s1。
- 从左到右扫描中缀表达式。
- 遇到运算对象时,将运算对象追加到保存到后缀表达式的字符串中。
- 遇到运算符时,进行如下判断:
如果运算符为括号,进行如下判断:\
- 运算符为左括号(,则直接将运算符入栈。\
- 运算符为右括号),则依次从s1栈中弹出运算符并追加到字符串中,直到遇到左括号(此时将这一对括号丢弃。\
如果运算符不是括号:\
- 栈顶元素为左括号(或者s1栈为空,直接将刚弹出的左括号(和运算符依次入s1栈。
- 否则,比较运算符与s1栈顶元素的优先级大小。
如果运算符的优先级大于(注意这里是大于)栈顶元素时,直接将运算符入s1栈。
如果运算符的优先级小于或等于栈顶元素时,则将刚弹出的元素追加到字符串,并继续弹出s1栈中的元素并执行步骤4。
- 重复步骤2-4,直到表达式的最右边。
- 将s1栈中的元素全部弹出并追加到字符串中。
2.2.3、二叉树方式
请查看上图中的表达式树,后序遍历表达式树即可得到后缀表达式。
3、求值
下面介绍如何根据前缀表达式或者后缀表达式来求这个表达式的最终结果,也就是模拟了计算机中表达式的运算过程。其实根据前缀表达式或者后缀表达式求值的方法基本类似,我就写成一个小节。
3.1、根据前缀表达式或后缀表达式来求值
首先要注意一点,为了区别表达式中的一位数和多位数,我在这里统一规定,无论是前缀表达式,还是后缀表达式,运算对象和运算符,运算对象和运算对象、运算符和运算符之间使用空格隔开。
3.1.1、算法思路
借助一个辅助栈,如果是前缀表达式求值则从右向左(后缀表达式从左向右)扫描表达式。
遇到运算对象直接入栈。
遇到运算符op则从辅助栈中弹出两个运算对象n1,n2,进行运算,前缀表达式是n1 op n2(后缀表达式是n2 op n1),并将运算结果入辅助栈。
当表达式扫描结束则弹出栈顶运算对象,即为结果。
3.1.2、实现代码
/**
* 根据前缀(后缀)表达式(注意表达式内容之间使用空格隔开)来计算结果
*
* @param expression
* @return
*/
public static int calExpression(String expression) {
String[] elements = new StringBuffer(expression).reverse().toString().split(" "); //前缀是从右向左扫描;后缀是从左向右扫描。
StringStack helperStack = new StringStack(elements.length); //辅助栈
int n1 = 0, n2 = 0;
for (int i = 0; i < elements.length; i++) {
if (isOperator(elements[i])) {
n1 = Integer.valueOf(helperStack.pop());
n2 = Integer.valueOf(helperStack.pop());
helperStack.push(calculate(n1, n2, elements[i]) + ""); //前缀是n1 op n2;后缀是n2 op n1
} else {
helperStack.push(elements[i]);
}
}
return Integer.valueOf(helperStack.pop());
}
/**
* 计算n1 op n2
*
* @param n1
* @param n2
* @param operator
* @return 计算后的结果
*/
public static int calculate(int n1, int n2, String operator) {
int result = 0;
switch (operator) {
case "+":
result = n1 + n2;
break;
case "-":
result = n1 - n2;
break; //注意:次顶元素 op 栈顶元素
case "*":
result = n1 * n2;
break;
case "/":
result = n1 / n2;
break;
case "%":
result = n1 % n2;
break;
}
return result;
}