表達式轉換
中綴表達式
我們通常看到的表達式像這樣:A*B,很容易知道這是A乘以B,這種操作符介於操作數中間的表示法,稱爲“中綴”表示法。
但有時候,中綴表示法會引起混淆,如“A+B*C”,是A+B然後再乘以C呢?還是B乘以C再加A呢?
爲了解決上面的問題,人們引入了操作符“優先級”的概念來消除混淆,規定高優先級的操作符先計算,相同優先級的操作符從左到右依次計算,這樣A+B*C就沒有疑義,是A加上B與C的乘積。
同時引入了括號來表示強制優先級,括號的優先級最高,而且在嵌套的括號中,內層的優先級更高。這樣(A+B)*C就是A與B的和再乘以C。
雖然人們已經習慣了這種表示法,但計算機處理最好是能明確規定所有的計算順序,這樣無需處理複雜的優先規則。
前綴和後綴表達式
接上面的問題,人們又引入了全括號表達式,即在所有的表達式項兩邊都加上括號,A+B*C+D,應表示爲((A+(B * C))+D)。
那麼可否將表達式中操作符的位置稍微移動一下呢?
例如中綴表達式A+B,我們將操作符移到前面,變爲“+AB”,或者將操作符移動到最後,變爲“AB+”。這樣我們就得到了表達式的另外兩種表示法:“前綴”和“後綴”表示法。(以操作符相對於操作數的位置來定義)
這樣A+BC的前綴表達式爲“+ABC”,後綴表達式爲“ABC*+”。(爲了幫助理解,子表達式加了中劃線)。
再來看中綴表達式“(A+B)C”,按照轉換的規則,前綴表達式是“ * + ABC”,後綴表達式是“AB+C”。神器的事情發生了,在中綴表達式裏必須的括號,在前綴和後綴表達式中消失了?
在前綴和後綴表達式中,操作符的次序完全決定了運算的次序,不再有混淆,所以在很多情況下,表達式的計算機表示都避免用複雜的中綴形式。
目前爲止,我們僅手工轉換了幾個中綴表達式到前綴和後綴的形式,一定得有個算法來轉換任意複雜的表達式,爲了分解算法的複雜度,我們從“全括號”中綴表達式入手。我們看A+BC,如果寫成全括號形式:(A+(B * C)),顯示錶達了計算次序,我們注意到每一對括號,都包含了一組完整的操作符和操作數,看子表達式(B * C)的右括號,如果把操作符 移動到右括號的位置,替代它,在刪除左括號,得到BC*,這個正好把子表達式轉換爲後綴形式,進一步再把更多的操作符移動到相應的右括號處替代之,再山區左括號,那麼整個表達式就完成了後綴表達式的轉換。
同樣的,如果我們把操作符移動到左括號的位置取代之,然後刪除所有的右括號,也就得到了前綴表達式。
所以說沒無論表達式多複雜,需要轉換爲前綴或者後綴,只需要兩步,將中綴表達式轉換爲全括號形式,將所有的操作符移動到子表達式所在的左括號(前綴)或右括號(後綴)處,再刪除所有的括號。
通用的中綴轉後綴算法
首先我們來看中綴表達式A+B * C,其對應的後綴表達式是ABC*+,操作數ABC的順序沒有改變,操作符的出現順序,在後綴表達式中反轉了,由於 * 的優先級比 + 高,所以後綴表達式中操作符的出現順序與運算次序一致。
在中綴表達式轉換後綴形式的吹過程中,操作符比操作數要晚輸出,所以在掃描到對應的第二個操作數之前,需要把操作符先保存起來,而這些暫存的操作符,由於優先級的規則,還有可能需要反轉次序輸出,在A + B * C中, + 雖然先出現,但優先級比後面這個* 要低,所以它要等 * 處理完,才能處理。
這種反轉特性,我們可以考慮用棧來保存暫時爲處理的操作符。
再看看(A + B)* C,對應的後綴形式是 AB + C*,這裏+的輸出要比 * 要早,主要是因爲括號使得 + 的優先級提升,高於括號之外的 *。回顧“全括號”表達式,後綴表達式中操作符應該出現在左括號對應的右括號位置,所以遇到左括號,要標記下來,其後出現的操作符的優先級提升了,一單掃描到對應的右括號,就可以馬上輸出這個操作符。
總結下,在從左到右掃描逐個字符掃描中綴表達式的過程中,採用一個棧來暫存未處理的操作符。這樣,棧頂的操作符就是最近暫存進去的,當遇到一個新的操作符,就需要跟棧頂的操作符比較下優先級,再進行處理。
通用的中綴轉後綴算法流程
在後面的算法描述中,約定中綴表達式是由空格隔開的一系列單詞(token)構成,操作符單詞包括* / + - (),而操作數單詞則是單字母標識符A、B、C等。
首先,創建空棧s用於暫存操作符,空表li用於保存後綴表達式,將中綴表達式轉換爲單詞列表,從左到右掃描中綴表達式單詞列表。
如果單詞是操作符,則直接添加到後綴表達式列表的末尾,如果單詞是左括號“(”,則壓入s棧頂,如果單詞是右括號“)”,則反覆彈出s棧頂,操作符加入到輸出列表末尾,直到碰到左括號,如果單詞是操作符“* / + -”,則壓入s棧頂,但是在壓入之前,要比較其與棧頂操作符的優先級。
中卓表示單詞列表掃描結束後,把s棧中所有剩餘操作符依次彈出,添加到輸出列表末尾,把輸出列表再用join方法合併成後綴表達式字符串,算法結束。
class Stack:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def push(self, item):
return self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
def infixToPostfix(infixexpr):
# 記錄操作符的優先級
p = {}
p["*"] = 2
p["/"] = 2
p["+"] = 1
p["-"] = 1
p["("] = 0
s = Stack() # 創建棧
li = [] # 用於保存後綴表達式列表
tokenList = infixexpr.split() # 將中綴表達式轉換爲單詞列表
print(tokenList)
for token in tokenList:
if token in "ABCDEFGHIJKMNLOPQRSTUVWXWY" or token in "0123456789":
li.append(token)
elif token == "(":
s.push(token)
elif token == ")":
topToken = s.pop()
while topToken != "(":
li.append(topToken)
topToken = s.pop()
else:
while not s.isEmpty() and p[s.peek()] >= p[token]:
li.append(s.pop())
s.push(token)
while not s.isEmpty():
li.append(s.pop())
return " ".join(li)
後綴表達式求值
作爲棧結構的結束,我們來討論“後綴表達式求值”問題,跟中綴轉換爲後綴問題不同,在對後綴表達式從左到右掃描過程中,由於操作在操作數的後面,所以要暫存操作數,在碰到操作符的時候,再將暫存的兩個操作數進行實際的計算,仍然是棧的特性:操作符只作用於離它最近的兩個操作數。
如“4 5 6 * +”,我們先掃描到4,5兩個操作數,但還不知道對這兩個操作數能做什麼計算,需要繼續掃描後面的符號才能知道,繼續掃描右碰到6,還是不知道如何計算,繼續暫存入棧,直到“ * ”,現在知道是棧頂兩個操作數5、6做乘法。
我們彈出兩個操作數,計算得到結果爲30,需要注意,先彈出的是右操作數,後彈出的是做操作數,這個對於-/很重要。
爲了繼續後續的計算,需要把這個中間結果30壓入棧頂,繼續掃描後面的符號,當所有操作符都處理完畢,棧中只留下一個操作數,就是表達式的值。
def postfixEval(postfixExpr):
s = Stack()
li = postfixExpr.split()
for l in li:
if l in "0123456789":
s.push(int(l))
else:
op2 = s.pop()
op1 = s.pop()
result = doMath(l, op1,op2)
s.push(result)
return s.pop()
def doMath(op, op1, op2):
if op == "*":
return op1 * op2
elif op == "/":
return op1 / op2
elif op == "+":
return op1 + op2
else:
return op1 -op2