波兰式、逆波兰式是《数据结构》课程中讲解关于栈的时候提到的,栈是很简单的一种数据结构。但是这些理论的提出却是计算机早期发展领域的重大突破,值得仔细回味。

1. 中缀表达式

我们在数学中学到的表达式被称为中缀表达式,操作符号在操作数中间,比如 ​​2 + 3 * (5 - 1)​​。对人类而言,这种表达方式显而易见,求值也很直接,先算乘除再算加减,先算括号内再算括号外。

然而,这个表达式对于计算机而言却很费解。你可能会有疑问:这有什么难理解的嘛,在JavaScript、Python或者Ruby,甚至是​​Java​​里面都可以通过​​eval("2 + 3 * (5 - 1)")​​来计算这个表达式。当然,这里的计算机并不是指现而今强大的计算机和高级编程语言,而是指上个世纪中页还处于发展初期的计算机。

2. 前缀表达式

早在1920年,波兰科学家​​扬·武卡谢维奇​​就发明了一种不需要括号的表示法,可以用来表示一个计算表达式。即将操作符号写在操作数之前,也就是前缀表达式,即波兰式(Polish Notation, PN)。这种表达式直到1960年计算机出现后才发挥出其威力。

比如​​2 + 3 * (5 - 1)​​这个表达式的前缀表达式为​​+ 2 * 3 - 5 1​​来表示。

阅读这个表达式需要从左至右读入表达式,如果一个操作符后面跟着两个操作数时,则计算,然后将结果作为操作数替换这个操作符和两个操作数,重复此步骤,直至所有操作符处理完毕。从左往右依次读取,直到遇到​​- 5 1​​,做计算后,将表达式替换为​​+ 2 * 3 4​​,然后从左往右再次读取,直到遇到​​* 3 4​​,做计算后将表达式替换为​​+ 2 12​​,然后从左往右依次读取,读到​​+ 2 12​​,计算得到14,到此结束。

可以看到,这种计算过程也相当复杂,需要多次遍历表达式,而且需要识别一个操作符后面跟着两个操作数这种模式,相比而言,下文中的逆波兰式要更为直接和简单。

如果你熟悉各种编程语言的话,这很像Lisp语言中的表达式(​​如下代码​​)。需要注意的是,Lisp语言中的括号并不是数学意义上的的括号,Lisp中的函数是可以携带多个参数的,比如​​(+ 1 2 3)​​,因此需要使用括号来标明函数参数。

Clojure1.5.1
user=>(+2(*3(-51)))14


3. 后缀表达式

后缀表达式也称为逆波兰式(Reverse Polish Notation, RPN),更加广为人知一些,和前缀表达式刚好相反,是将操作符号放置于操作数之后,比如​​2 + 3 * (5 - 1)​​用逆波兰式来表示则是:​​2 3 5 1 - * +​​。

逆波兰式的计算也是从左往右依次读取,当读到操作符时,将之前的两个操作数做计算,然后替换这两个操作数和操作符,接着读取,重复此步骤。对于这个表达式,读到​​5 1 -​​,得到​​4​​,然后读取乘号,取出前面的​​3​​和上一步的计算结果​​4​​,并计算,到​​12​​,接着读取加号​​+​​,计算​​2 12 +​​得到​​14​​,计算结束。

上面这个步骤可以很容易的用栈来实现:

从左往右依次读取表达式,如果是数字则将该数字压栈,如果是符号,则将之前的两个数字出栈,做计算后,将计算结果压栈,直到表达式读取结束。栈中剩下的一个数就是计算结果。

逆波兰式看起来像波兰式反过来,比如​​5 + 1​​的波兰式是​​+ 5 1​​,逆波兰式为​​5 1 +​​或者​​1 5 +​​。也很明显,逆波兰式并不是简单的将波兰式反过来,因为,减法和除法中减数和被减数、除数与被除数是不能交换的,即​​- 10 5​​和​​- 5 10​​就完全不一样。

4. 中缀表达式到后缀表达式的转换

因为通过后缀表达式来进行计算只需要一个栈即可,从硬件和软件上实现都是极为便利的,因此逆波兰式在计算机领域的应用更加广泛,因此将中缀表达式转换为逆波兰式非常重要。

依然仅仅使用栈就可以将中缀表达式转换成逆波兰式,转换过程如下:

从左往右遍历中缀表达式中的每个数字和符号,弱是数字就输出,成为逆波兰式的一部分; 如果是右括号,或者是其他符号并且比当前栈顶符号的优先级低,则栈顶元素依次出栈并输出; 然后将当前符号进栈,重复以上操作直到结束。

还是以​​2 + 3 * (5 - 1)​​为例:

  1. 首先读入数字​​2​​,直接将其输出,输出为​​2​​,栈为空
  2. 接着读入加号​​+​​,由于栈为空,因此将其进栈,输出为​​2​​,栈为​​+​
  3. 接着读入数字​​3​​,直接将其输出,输出为​​2 3​​,栈为​​+​
  4. 接着读入乘号​​*​​,比栈顶元素优先级高,进栈,输出为​​2 3​​,栈为​​+ *​
  5. 读入左括号​​(​​,直接进栈,输出​​2 3​​,栈为​​+ * (​
  6. 读入数字​​5​​,直接将其输出,输出为​​2 3 5​​,栈为​​+ * (​
  7. 读入减号​​-​​,栈顶元素为左括号,进栈,输出为​​2 3 5​​,栈为​​+ * ( -​
  8. 读入数字​​1​​,直接将其输出,输出为​​2 3 5 1​​,栈为​​+ * ( -​
  9. 读入右括号,依次输出栈顶元素,直到左括号,括号不输出,输出​​2 3 5 1 -​​,栈为​​+ *​
  10. 已经无元素可读,依次输出栈顶元素,直到栈为空,输出​​2 3 5 1 - * +​​,栈为空

这样可以仅仅使用栈,首先将中缀表达式转换为逆波兰式,然后用本文第3节中的方法对后缀表达式进行求值,整个过程使用栈来完成即可。

5. 表达式树与逆波兰式

还可以通过另外一种方法来将一个表达式转换成波兰式和逆波兰式,这种方法依赖与树,首先需要根据表达式构建成树,仍然以​​2 + 3 * (5 - 1)​​为例,下图是其表达式树波兰式、逆波兰式与表达式求值_后缀表达式 0%

我们发现这个树的后序遍历结果为​​2 3 5 1 - * +​​,刚好是其逆波兰式;而其先序遍历结果为​​+ 2 * 3 - 5 1​​刚好为其波兰式;中序遍历就不用说了,就是我们常见的中缀表达式。我们也可以通过这种特性来实现表达式的各种表示方法的转换。