Infix Expression to Expression Tree
中序表達式轉後序表達式。
主要想處理的功能是:把算式多餘的 Parentheses(括號)給移除,例如:1+(2+3)
可變成:1+2+3
;1*(2+3)
則不變。
剛開始看感覺還好,開始做了之後才發現好像遠沒想像的簡單,用一般字串處理,頂多搭配個 Stack 來做,好像很多情況沒有辦法做到,因此有個想法,將 Infix 的表達式(就是我們一般看四則運算的方式)轉成 Postfix,因為 Postfix 就不含括號。
但轉換完 Postfix 後,要怎麼轉回來又是另外一個問題了!因為要加上括號:
Postfix +12
的 Infix 形式為 1+2
,而 Postfix 123+*
要轉成 Infix 1*(2+3)
要加上括號,怎麼直接從字串的 123+*
判斷什麼時候要加括號,著實不容易,於是就想到畫出這棵 Expression Tree
1 | * |
因此只要判斷 * 下方的 Operator 是不是優先級比自己低即可,低的話就下面加括號。
總而言之,整個流程大略上是將「四則運算算式」轉換成「Expression Tree」,再轉換回來
每一步的細節其實蠻多的,但大方向是這樣,只要轉成的 Tree 正確,轉回來的過程不加多餘括號,這個問題就解決了。
將 Infix expression 轉成 tree
重要名詞
- Infix, Prefix, Postfix
- Precedence - 優先級
- Associative property - 結合律
- Operand - 運算元(被運算的單元,例如
1+2
中的數字) - Operator - 運算子(運算什麼,例如
1+2
中的+
)
先轉成 Postfix(without parentheses)
- 遇到 Operand 就輸出至 Postfix 的結果
- 遇到 Operator,看 Stack 最頂元素
2.1. 如果 Stack 的頂元素 Precedence 較高(例如頂元素 * > 現在元素 -),則 Pop 頂元素至 Postfix 的結果
2.2. 反之,如果 Stack 的頂元素 Precedence 較低、等於或是 Stack 為空,則 Push 此元素至 Stack
(大方向,Stack 中的元素,越上面的元素 Precedence 一定比較高,且不會等於) - Traverse Infix 元素至盡頭,則 Pop Stack 的元素
1 | A + B * C - D * E |
先轉成 Postfix(with parentheses)
和有 Prentheses 的情況比,多了以下粗體字的規則
- 遇到 Operand 就輸出至 Postfix 的結果
- 遇到 Operator,看 Stack 最頂元素 (頂元素如果為
(
則視為空的)
2.1. 如果 Stack 的頂元素 Precedence 較高(例如頂元素 * > 現在元素 -),則 Pop 頂元素至 Postfix 的結果
2.2. 反之,如果 Stack 的頂元素 Precedence 較低、等於或是 Stack 為空,則 Push 此元素至 Stack
(大方向,Stack 中的元素,越上面的元素 Precedence 一定比較高,且不會等於)
2.3. 如果現在元素是(
,Push 至 Stack
2.4. 如果現在元素是)
,Pop Stack 的元素直到第一個(
被 Popped 出來 - Traverse Infix 元素至盡頭,則 Pop Stack 的元素
1 | ( ( A + B ) * C - D * E ) |
從 Infix 轉成 Expression Tree
開始進入到正題,將直接印出來成 Postfix 的行為,換成樹的資料結構。
要產生一個 Expression Tree,我們還需要另一個 Stack 存每個 Sub-Expression。例如 (a+b)*c
就會被切成 (a+b)
這個 Expression 乘以 c
。
所以從原本的 Stack 切成兩種:OpStack (Operator Stack) 和 ExprStack (Expression Stack),其中 ExprStack 存的就是 Tree 的 Node,在此要先定義一下 TreeNode:
1 | TreeNode { |
- 遇到 Operand 就產生一個節點 Push 到 ExprStack
- 遇到 Operator,看 OpStack 最頂元素(頂元素如果為
(
則視為空的)
2.1. 如果 OpStack 的頂元素 Precedence 較高(例如頂元素 * > 現在元素 -),則
—– Pop OpStack 的頂元素,產生新節點
—– Pop 兩次 ExprStack 得到兩個 Expression,將新節點的 right 接到第一個 popped 的 Expression,left 接第二個
—– 將新節點 Push 回 ExprStack
2.2. 反之,如果 OpStack 的頂元素 Precedence 較低、等於或是 OpStack 為空,則 Push 此元素至 OpStack
(大方向,OpStack 中的元素,越上面的元素 Precedence 一定比較高,且不會等於)
2.3. 如果現在元素是(
,Push 至 OpStack
2.4. 如果現在元素是)
,Pop OpStack 的元素(並照 2.1 的流程,將新 Expression Push 進 ExprStack),直到第一個(
被 Popped 出來 - Traverse Infix 元素至盡頭,則 Pop Stack 的元素
1 | ( A + B ) * C |
Expression Tree to Infix Expression
如果要把 Expression Tree 給印出來的話,就要考慮加括號這件事了。
1 | * |
以上面這棵樹來看,如果做 Inorder 的走訪可以直接印出 A+B*C
,但顯然不是正確的結果,應該要輸出成 (A+B)*C
才對。
所以要在走到 Opertor 時做條件判斷,這邊的做法是走訪時把 Parent Node 的 Precedence 傳下去,例如 *
的左節點是 +
,顯然 *
的 Precedence 較高,那麼輸出 [A+B]
這個 Expression 時就要前後加上括號。
另外一種情況是減法或除法,例如 A-(B-C)
或 A/(B/C)
這兩種情況的括號是不能拿掉的,因為 A-(B-C) != A-B-C
,我們把減法的這棵 Expression Tree 畫出來看看
1 | - |
所以這邊要判斷 -
的右節點是否為 -
、/
的右節點是否為 /
,如果是的話就要將右節點輸出時括號括起來。那如果在左邊呢?例如 (A-B)-C
,就不需要特別判斷了,因為 (A-B)-C == A-B-C
,四則運算的結合率都是從左到右的。
規則總結
整體做 Inorder traversal
- 遇到 Operand 直接印出來
- 遇到 Operator
2.1. 子節點也是 Operator,且 Precedence 比較低的話,幫他們前後加上括號
2.2. 如果 Operator 是-
或/
,判斷其右節點是否和自己 Precedence 一樣,一樣的話也幫他們節後加上括號- E.g.,
2-(3-4)
或2-(3+4)
括號都要保留
- E.g.,
Traverse 範例
以 Inorder 的方式 Traverse 整棵樹
- 子節點為 Operand 則直接輸出至 Infix 結果
- 子節點為 Operator
2.1. 子節點 Precedence 比現節點低,輸出(
,遞迴 Function 後輸出)
2.2. 反之,繼續 Traverse
1 | Postfix: AB+CD+* |
其它注意事項
以上的解法都是基於 Operator 只有 +
, -
, *
, /
以及 (
, )
來做的。四則運算的部分都是所謂 Binary 的,意思就是一個 Operator 配合兩個 Operand。
但假設現在多了一個 -
Operator 是 Unary 的,也就是取負值這個 Operator,我們就得加上其 Precedence(理論上要比四則運算都高,但又小於括號),而且這棵 Expressoin Tree 就可能會有只有一個子節點的節點了。
例如 2+(-1)
,畫成 Tree 就會長成
1 | + |
此時整個樹在做 Inorder 時要處理例外條件,遇到 Unary -
時得先印 -
,還要考慮何時需要把自己或子節點包起來。像是 5/(-1)
, 5-(-1)
兩種情況都要包嗎?還是 /
和 *
後的 Unary -
不用包,等等情況需要考慮。