一文拿下Leetcode中的3个计算器——步骤2 infix -> RPN

249 阅读3分钟

一文拿下Leetcode中的3个计算器——步骤2 infix -> RPN

实现计算器的第二步是通过调度场算法(Shunting Yard Algorithm)将惯用的表达式转换为后缀表达式。该算法由迪杰斯特拉引入,因其操作类似于火车调度场而得名。

迪杰斯特拉是欧洲编程语言三巨头之一。荷兰人,发明了“最短路径算法”,“信号量”,提出了“Goto是有害的”,1972年因对结构化编程的贡献获得图灵奖。

img

在常见的算式中,操作符处于操作数中间(如3 + 4),这样的表示法叫做中缀表示法(或中缀记法,infix notation)。要想改变默认的运算优先级(先乘除后加减),中缀表达式中的括号是不能省略的,虽然我们很习惯这种写法,但它却不便于程序处理。

后缀表示法,也称为逆波兰记法(RPN,reverse polish notation),是由波兰数学家扬·卢卡西维茨于1920年引入的表达式形式。在逆波兰记法中,操作符置于(与该操作符相关的)操作数的后面。在1960年代早期,迪杰斯特拉和弗里德里希·鲍尔提议将逆波兰记法用于表达式求值。

逆波兰记法的一个好处是不需要括号来表示操作符的优先级。例如,在中缀记法中,对于3 − 4 × 5,要想先计算3 − 4,就必须写作(3 − 4) × 5。而在后缀记法中,前者写做3 4 5 × −,后者写做3 4 − 5 ×,计算顺序不同,写法也不同,无须通过括号标记。

在Linux中,bc命令用于对惯用的表达式求值,而dc命令可以对逆波兰记法的算式(后缀表达式)求值,例如

$ bc -e "3 - 4 * 5"   
-17
$ bc -e "(3 - 4) * 5"
-5

# dc命令结尾处的 p 表示打印结果
$ dc -e '3 4 5 * - p'
-17
$ dc -e '3 4 - 5 * p'
-5

逆波兰记法的缺点也很明显,这种表示法不符合人们的读写习惯,需要经过大量训练才能习惯。但好在借助调度场算法就能将常见的算式机械地转换成逆波兰记法的算式。

下面我们就来结合下图看看如何将中缀表达式A + B × C − D 转换成后缀表达式。

img

我们可以把图中的线条想象成是下图中的铁轨,把操作数ABCD和操作符+×——可统称为token——都想象成是火车的车厢。

img

调度场算法(Shunting Yard Algorithm)的规则是:

  • 如图a)、c)、e)、h)所示,如果当前的符号(token,火车车厢)是操作数,则直接将其输出(通过上方的水平轨道驶入左侧Output那一端)
  • 如图b)所示,如果当前的符号是操作符且操作符栈(Operator stack,两段弧形铁轨的交汇处)为空,则将该操作符(此时是+)压入操作符栈
  • 如图d)所示,如果当前符号是一个操作符,并且优先级高于操作符栈顶部的操作符,则将其压入操作符栈(此时是×的优先级高于+的优先级)
  • 如图f)、g)所示,只要当前符号是一个操作符,并且其优先级低于或等于操作符栈顶部的操作符(此时是的优先级低于×+),就持续弹出操作符栈顶部的操作符,直到操作符栈为空。然后,将当前的操作符()推入操作符栈
  • 如图i)所示,最后弹出操作符栈上的所有操作符

另外,当中缀表达式中包含括号时,有如下规则:

  • 如果传入的符号是左括号((),则将其压入堆栈

  • 如果传入的符号是右括号()),则丢弃右括号,并持续弹出操作符栈中的符号,直到看到左括号,最后弹出左括号并将其丢弃

参考