之前在字节面试的时候,面试官给我出了一道算法题:给定一个用字符串表示的算式,算式中只有+
, -
, *
, /
,请计算出最终结果。
比如 2+3*4-2/2
。
很明显,如果没有 *
和 /
,那我们直接一个循环处理即可,但是有了 *
和 /
,算式就有了优先级,我们需要先计算优先级高的,在计算优先级低的。
面试官提醒我可以使用栈来处理,我才恍然大悟,然后迅速用代码实现。
面试结束后,我就去细了解了一下这类问题的做法,这种字符串公式的表达叫做:中缀表达式
,与之类似的还有前缀表达式
和后缀表达式
,前缀表达式和后缀表达式又被叫做波兰表达式
和逆波兰表达式
。
中缀表达式
其实很多读者在数据结构这一门课上,就已经学过中缀表达式了,但是对于我这个非科班的来说,第一次接触还是非常新鲜的,对于学过的朋友权当复习了。
使用栈这种数据结构,可以很好的处理仅有加减乘除四种运算符(不含括号)的公式。
栈是一种先入后出(FILO)的数据结构,可以将数据入栈和出栈,入栈数据会在栈顶,出栈是将栈顶的数据取出。
对 2+3*4-2/2
表达式来说,我们将数字压入栈,如果遇到 *
和 /
这两个优先级高的操作,我们将栈中的数据出栈,和操作符后面的数据进行 *
和 /
操作之后,将结果入栈,遇到 +
则不做操作,遇到 -
则对后一个数据进行取负,将数据压入栈即可。
就对 2+3*4-2/2
举例子:
|
|
13
即是最终值。
因为 golang
中没有现成的栈 api
,我们需要用队列来手动实现一个。
实现代码如下:
|
|
栈的基本功能已经实现,有:
push
方法,在栈顶压入数据pop
方法,从栈顶删除一个数据,并返回该数据top
方法,去栈顶的数据返回,不删除栈顶数据isEmpty
方法,判断栈是否为空
下面我们来实现计算中缀表达式的代码:
|
|
逆波兰表达式(后缀表达式)
对中缀表达式来说,如果不涉及 ()
这种计算,那么用上面的代码即可处理,但是一旦涉及了 ()
,计算就会非常复杂。
波兰的一位逻辑学家卢卡西维兹在 1929
年提出了一种计算表达式的表达方法:
将操作符放到操作数之前(中缀表达式是将操作符放到操作数中间)
。
由于操作符在操作数之前,于是这种表达式被叫做前缀表达式,因为发明者是波兰人,这种表达式又被叫做波兰表达式。
很明显,逆波兰表达式就是操作符在操作数之后的。
举个例子:
- 中缀表达式为:
2+3*2-2
- 波兰表达式为:
[-, +, 2, *, 3, 2, 2]
- 逆波兰表达式为:
[2, 3, 2, *, +, 2, -]
我们来看看如何计算逆波兰表达式。
根据逆波兰表达式的特点,操作符是表示前两个数的操作的,比如 [3, 2, *]
,代表的就是 3 * 2
。
那么我们在对表达式轮询的时候,如果是操作数,就压入栈中,如果是操作符,那么就把栈顶的两个数据取出,进行操作之后,压入栈中。
轮询完之后,栈顶的数据就是我们需要的值。
代码实现:
|
|
中缀表达式转逆波兰表达式
我们通过各种表达式的表达中可以发现:
中缀表达式为:2+3*2-2
逆波兰表达式为:[2, 3, 2, *, +, 2, -]
操作数的顺序是没有变化的,都是 2
、3
、2
、2
只有操作符的顺序发生了改变,那么我们在中缀表达式转逆波兰表达式中,维护一个操作符的栈,一个操作数的栈,然后将操作符插入操作数的栈中即可。
以下是具体的操作步骤:
- 遇到操作数,将操作数压入操作数栈中
- 遇到操作符,在操作符栈中找平级或更高级的操作符,依次将这些操作符压入操作数栈,将本身轮询到的操作符压入栈中
- 遇到界限符
()
,如果是(
直接压入操作符栈中,遇到)
,就在操作符中往前查找,直到找到(
,将中间遇到的所有操作符都加到操作数栈中,然后将(
删除
中缀表达式轮询完毕,将操作符栈里面的所有数据都加到操作数中。 操作数栈数据即为逆波兰表达式 我们来模拟一次:
|
|
[2, 3, +, 2, 4, 2, *, +, -, 2, 2, /, +]
即为逆波兰表达式。
里面涉及了一个操作符优先级的比较,我们先写一个方法比较:
|
|
该方法名是 lowerOP
,返回 true
代表 op1
比 op2
低级,false
代表 op1
比 op2
高级。
op1
代表操作符栈中的数据,op2
代表循环的中缀表达式中的操作符。
如果 op1
是 +
或者 -
,那么就比 op2
的 *
或 /
要低级。
如果 op1
是 (
, 直接返回 true
,因为 (
是界限符,不参与比较,循环到这里就要停止。
其他情况,都是 op1
比 op2
优先级高。
我们还需要注意一点,上面写的 stack api
,只能接受 int
类型数据,波兰表达式里面是 string
类型数据,需要对 stack
进行微小改造(由读者自行完成)。
所以,根据以上逻辑,我们可以写出中缀表达式转逆波兰表达式的代码:
|
|
我们可以看到代码:
|
|
这是为了处理有可能多位数字,比如 12
、13
等。
如果循环到当前,发现是数字,就另起循环,对下一个数进行探测,如果是数字,就 10*num + int(s[j]-'0’)
如果不是数据,就终止循环,然后将外层循环的指针 i
,指向 j
,把数字压入操作数栈中。
核心逻辑代码:
|
|
case (
和 case )
都比较简单。
只有 default
里面涉及到了 op1
和 op
的比较。
op1
是操作符栈中取出的数据,op
是循环到当前 s
中的操作符。
如果 op1
比 op
优先级高或平级,那么加入操作数栈中。
我们试着输入:
- 输入:
2+3-(2+4*2)+2/2
- 输出:
[2 3 + 2 4 2 * + - 2 2 / +]
输出结果和我们之前模拟结果:[2, 3, +, 2, 4, 2, *, +, -, 2, 2, /, +]
是一致的。
结尾
无论是中缀表达式还是逆波兰表达式,其实都是栈的应用,无非就是中缀表达式转逆波兰表达式的时候,需要注意的点有点多,需要操作的逻辑比较负责,其实核心还是不停使用栈的 pop
和 push
方法,利用栈的后进先出,来实现我们的需要。