波蘭式、逆波蘭式是《數據結構》課程中講解關於棧的時候提到的,棧是很簡單的一種數據結構。但是這些理論的提出卻是計算機早期發展領域的重大突破,值得仔細回味。
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)
為例:
- 首先讀入數字
2
,直接將其輸出,輸出為2
,棧為空 - 接着讀入加號
+
,由於棧為空,因此將其進棧,輸出為2
,棧為+
- 接着讀入數字
3
,直接將其輸出,輸出為2 3
,棧為+
- 接着讀入乘號
*
,比棧頂元素優先級高,進棧,輸出為2 3
,棧為+ *
- 讀入左括號
(
,直接進棧,輸出2 3
,棧為+ * (
- 讀入數字
5
,直接將其輸出,輸出為2 3 5
,棧為+ * (
- 讀入減號
-
,棧頂元素為左括號,進棧,輸出為2 3 5
,棧為+ * ( -
- 讀入數字
1
,直接將其輸出,輸出為2 3 5 1
,棧為+ * ( -
- 讀入右括號,依次輸出棧頂元素,直到左括號,括號不輸出,輸出
2 3 5 1 -
,棧為+ *
- 已經無元素可讀,依次輸出棧頂元素,直到棧為空,輸出
2 3 5 1 - * +
,棧為空
這樣可以僅僅使用棧,首先將中綴表達式轉換為逆波蘭式,然后用本文第3節中的方法對后綴表達式進行求值,整個過程使用棧來完成即可。
5. 表達式樹與逆波蘭式
還可以通過另外一種方法來將一個表達式轉換成波蘭式和逆波蘭式,這種方法依賴與樹,首先需要根據表達式構建成樹,仍然以2 + 3 * (5 - 1)
為例,下圖是其表達式樹。
我們發現這個樹的后序遍歷結果為2 3 5 1 - * +
,剛好是其逆波蘭式;而其先序遍歷結果為+ 2 * 3 - 5 1
剛好為其波蘭式;中序遍歷就不用說了,就是我們常見的中綴表達式。我們也可以通過這種特性來實現表達式的各種表示方法的轉換。