算法之算數表達式后序表示
本節內容
- 為啥搞這個
- 樹的三種表示法
- 算數表達式的轉換
- 計算器的實現
1.為啥搞這個
為什么要搞一個算數表達式的后序表示呢?是因為。。。。。。有一個需求是實現簡單計算器表達式的計算,但是不能使用eval實現(PS:這不廢話么,用eval實現,誰還在這看你瞎逼逼呢。。。)然后在分析需求的時候突然想起了之前在某本算法書(別啥某本了,嚴蔚敏老師的書,果斷免費給她打個廣告,向老前輩致敬《數據結構》)中看到過算數表達式的后序表示法,計算算數表達式,突然靈感一現,能不能夠使用python實現里面的原理呢?說干咱就干,先把這東西的邏輯啃了啃,發現確實挺難理解的,需要一步一步的慢慢摸索。。。於是這坑比的周末就交代在研究這東西上面了。
2.樹的三種表示法
這里有個樹的概念,那么,就來畫個樹吧。小弟不才,自己畫的。。。有點丑,大家將就着看吧。。。
如上圖所示,對於一顆樹來說,以不同的順序來遍歷,將會得到不同的結果,那么,這有什么卵用呢?哈哈,既然畫出來了,肯定是有用的,為了后面介紹算數表達式做個引子呀。
因為算數表達式也可以使用一棵樹來表示呀,樹的根節點以及各子樹的根節點里面存儲的是運算符,樹的葉子節點存儲的是操作數。
那么,為什么要用樹來表示一個算數表達式呢?這個問題在下節講解。
3.算數表達式的轉換
好了,前面引入了這些前戲,是為了引出我們現在的重點。對於算數表達式:1+2*3-2/3,可以用下面這棵樹來表示。
下面,我們分別用前序,中序,后序表達式來遍歷這棵樹,看看會發生什么:
1. 前序遍歷:- + 1 * 2 3 / 2 3
2. 中序遍歷:1 + 2 * 3 - 2 / 3
3. 后序遍歷:1 2 3 * + 2 3 / -
仔細看看,發生了什么?原來中序遍歷的結果就是我們平常使用的算數表達式,那么,后序表達式呢?后序表達式雖然人看起來很費勁,但是對於計算機來說,計算起來就很方便了。從左到右讀取數據,只要讀取到操作數,就壓到棧中,讀取到操作符,就取出棧中的最上面兩個數進行計算,再將計算中間結果壓回棧中,這樣,遍歷結束之后,棧中就只剩下一個元素了,也就是最終的計算結果。。。
好,那么,我們應該怎么將一個中序表達式轉換成一個后序表達式呢?
將一個中序表達式轉換成后續表達式,首先,我們需要定義好一個算數操作符的優先級:
1 isp = {"#": 0, "(": 1, ")": 6, "+": 3, "-": 3, "*": 5, "/": 5} # 定義棧內優先級 2 icp = {"#": 0, "(": 6, ")": 1, "+": 2, "-": 2, "*": 4, "/": 4} # 定義棧外優先級
上面兩個字典定義了兩種不同的算數表達式優先級。
要將一個中序表達式轉換成后序表達式,需要遵循下面的原則:
1.通過之前定義的站內和棧外優先級的兩個字典,確定操作符的優先級 2.如果是操作數,直接壓到后序表達式列表中 3.如果是操作符,則遵循如下原則: 3.1.如果取到的操作符棧外優先級比棧頂操作符的棧內優先級高,則直接將該操作符入棧 3.2.如果取到的操作符棧外優先級比棧頂元素棧內優先級低,則將棧頂元素壓到后序列表中,再將該操作符與棧頂元素 進行比較,如果棧頂元素棧內優先級低,則繼續壓到后序列表中,直到不滿足條件,這時判斷如果該操作符是右括號, 那么將棧頂元素丟棄(此時的棧頂元素是左括號),並丟棄該操作符,取下一個操作符。如果該操作符不是右括號, 則將該操作符壓入到棧頂。 3.3.如果取到的操作符棧外優先級和棧頂元素棧內優先級相同,則丟棄棧頂操作符。 (PS:這種情況很少出現,只有在括號匹配出現的時候) 4.最后將棧中剩余的其他操作符順序反轉之后丟棄之前存入的#操作符,之后追加到后序表達式列表中。
接下來就是python的實現代碼了:
1 def postfix_expression(expression): 2 postfix_list, stack = [], [] # 定義后序表達式列表以及操作符棧 3 stack.append("#") # 先在棧底壓入操作符#(PS:因為#操作符的優先級最低,不會被退出棧) 4 for char in expression: # 遍歷表達式 5 if char not in isp: # 獲取到的字符是操作數,則將操作數直接壓到后序表達式列表中 6 postfix_list.append(char) 7 elif isp[stack[-1]] < icp[char]: # 如果棧頂元素棧內優先級比操作符棧外優先級低,將操作符壓入棧中 8 stack.append(char) 9 elif isp[stack[-1]] > icp[char]: # 如果棧頂元素棧內優先級比操作符棧外優先級高 10 while isp[stack[-1]] > icp[char]: # 將棧頂元素循環追加到后序表達式列表中,直到不滿足條件 11 postfix_list.append(stack.pop()) 12 if char == ")": # 如果操作符是右括號,則不滿足條件時棧頂元素肯定是左括號,丟棄棧頂元素及該操作符,取下一個字符 13 stack.pop() 14 continue 15 stack.append(char) # 否則將該操作符壓入棧中 16 else: 17 stack.pop() # 如果操作操作符棧外優先級和棧頂元素棧內優先級相同,則丟棄棧頂元素,獲取下一個字符(在優先級表中只有括號的棧外優先級和站內優先級存在相等情況) 18 stack.reverse() # 將棧中剩余操作符反轉 19 stack.pop() # 丟棄之前存入的#操作符 20 postfix_list.extend([i for i in stack]) # 將反轉並丟棄#操作符的站內操作符全部追加到后序表達式列表中
上面那段代碼就能夠將一個中序算數表達式轉換成一個后序表達式,並且還能把括號去掉,這樣,計算這個表達式的結果將變得輕而易舉了。
4.計算器的實現
第三節的表達式轉換,就是實現一個計算器的核心部件了,有了這個神器,其他的只不過是圍繞這個神器實現的功能罷了。下面,就是我實現的計算器的源代碼,里面做了詳細的注釋,僅供各位參考。。。

1 #!/usr/bin/env python 2 # encoding:utf-8 3 # __author__: huxianglin 4 # date: 2016-09-11 5 # blog: http://huxianglin.cnblogs.com/ http://xianglinhu.blog.51cto.com/ 6 import re 7 import time 8 9 isp = {"#": 0, "(": 1, ")": 6, "+": 3, "-": 3, "*": 5, "/": 5} # 定義棧內優先級 10 icp = {"#": 0, "(": 6, ")": 1, "+": 2, "-": 2, "*": 4, "/": 4} # 定義棧外優先級 11 12 """裝飾器函數,計算消耗時間""" 13 14 15 def show_time(func): 16 def wrapper(): 17 start_time = time.time() 18 func() 19 end_time = time.time() 20 print("計算器計算時間為:%ss" % (end_time - start_time)) 21 22 return wrapper 23 24 25 """定義檢查函數,檢查計算表達式是否含有非法字符以及判斷括號是否匹配""" 26 27 28 def checkout(expression): 29 char_flag, parenthesis_flag = False, False # 定義非法字符標記及括號檢查標記 30 char_list = re.findall("[^0-9\+\-\*/\(\)\.]", expression) # 獲取表達式中的非法字符列表 31 char_flag = True if not char_list else print("輸入表達式中含有非法字符%s..." % char_list) # 列表為空,則沒有非法字符 32 left_parenthesis_list = [] # 定義存儲左括號列表 33 for i in expression: # 遍歷表達式,獲取每一個字符 34 if i == ")": # 如果獲取到的是右括號 35 if left_parenthesis_list: # 判斷左括號列表中是否有內容 36 left_parenthesis_list.pop() # 如果有內容,彈出一個左括號 37 else: # 否則,說明右括號和左括號不匹配(可能是右括號在左括號之前出現) 38 print("左右括號不匹配...") 39 break 40 elif i == "(": # 如果獲取到的是左括號,則將左括號添加到左括號列表中 41 left_parenthesis_list.append(i) 42 parenthesis_flag = True if not left_parenthesis_list else print( 43 "左右括號不匹配...") # 遍歷完成之后判斷左括號列表中是否還有括號,如果有,則說明左括號多了,否則左右括號匹配 44 if char_flag and parenthesis_flag: # 沒有非法字符並且括號匹配時,返回True,檢查通過,否則檢查不通過 45 return True 46 else: 47 return False 48 49 50 """將表達式中的所有操作數取出,生成一個列表,並將所有操作數使用字符S替換,再判斷在最開始位置的操作數 51 以及左括號后面第一個操作數是否為負數,如果是負數,則將獲取到的該操作數的值轉換成負數,並把前面的"-"刪除。 52 返回一個使用字符S替換操作數的新的表達式以及一個存儲操作數的列表""" 53 54 55 def replace_expression(expression): 56 negative_number_flag = [] # 定義操作數負數標記列表 57 num_list = re.findall("[0-9]*\.?[0-9]+", expression) # 獲取所有的操作數字符列表 58 num_list = [float(i) for i in num_list] # 將獲取到的字符操作數轉換成浮點型數字 59 expression = re.sub("[0-9]*\.?[0-9]+", "S", expression) # 將所有的操作數替換成字符S 60 if expression[0:2] == "-S": # 判斷第一個操作數是否是負數 61 negative_number_flag.append(0) # 如果是負數,則標記該位置的操作數 62 expression = re.sub("^-S", "S", expression) # 將前面的負號刪除 63 s_num = 0 # 定義操作數計數位 64 for i in range(2, len(expression)): # 前面已經判斷了前兩位所存儲的操作數,之后從第三位開始查找操作數 65 if expression[i] == "S": # 找到操作數時 66 s_num += 1 # 操作數計數+1 67 if expression[i - 2:i + 1] == "(-S": # 判斷操作數及其前面的字符串組合是否是(-S 68 negative_number_flag.append(s_num) # 如果判斷成功,則將該計數位添加到負數標記列表中 69 expression = re.sub("\(-S", "(S", expression) # 將該位置的操作數前面的負號刪除 70 if negative_number_flag: # 如果負數標記列表中含有值,則將操作數列表中相應位置的操作數變成負數 71 num_list = [num_list[i] * -1 if i in negative_number_flag else num_list[i] for i in range(len(num_list))] 72 return expression, num_list # 返回新的表達式以及操作數列表 73 74 75 """將中序表達式轉換成后序表達式,並將之前使用的S占用的操作數位置轉換成實際的操作數。這里的將中序表達式轉換成 76 后序表達式的原則遵循如下原則: 77 1.通過之前定義的站內和棧外優先級的兩個字典,確定操作符的優先級 78 2.如果是操作數,直接壓到后序表達式列表中 79 3.如果是操作符,則遵循如下原則: 80 3.1.如果取到的操作符棧外優先級比棧頂操作符的棧內優先級高,則直接將該操作符入棧 81 3.2.如果取到的操作符棧外優先級比棧頂元素棧內優先級低,則將棧頂元素壓到后序列表中,再將該操作符與棧頂元素 82 進行比較,如果棧頂元素棧內優先級低,則繼續壓到后序列表中,直到不滿足條件,這時判斷如果該操作符是右括號, 83 那么將棧頂元素丟棄(此時的棧頂元素是左括號),並丟棄該操作符,取下一個操作符。如果該操作符不是右括號, 84 則將該操作符壓入到棧頂。 85 3.3.如果取到的操作符棧外優先級和棧頂元素棧內優先級相同,則丟棄棧頂操作符。 86 (PS:這種情況很少出現,只有在括號匹配出現的時候) 87 4.最后將棧中剩余的其他操作符順序反轉之后丟棄之前存入的#操作符,之后追加到后序表達式列表中。 88 這樣最后生成的就是一個后序表達式列表,再將替換出來的操作數替換到后序表達式列表中,真正完成了后序表達式列表的生成。 89 """ 90 91 92 def postfix_expression(expression, num_list): 93 postfix_list, stack = [], [] # 定義后序表達式列表以及操作符棧 94 stack.append("#") # 先在棧底壓入操作符#(PS:因為#操作符的優先級最低,不會被退出棧) 95 for char in expression: # 遍歷表達式 96 if char not in isp: # 獲取到的字符是操作數占位符,則將操作數占位符直接壓到后序表達式列表中 97 postfix_list.append(char) 98 elif isp[stack[-1]] < icp[char]: # 如果棧頂元素棧內優先級比操作符棧外優先級低,將操作符壓入棧中 99 stack.append(char) 100 elif isp[stack[-1]] > icp[char]: # 如果棧頂元素棧內優先級比操作符棧外優先級高 101 while isp[stack[-1]] > icp[char]: # 將棧頂元素循環追加到后序表達式列表中,直到不滿足條件 102 postfix_list.append(stack.pop()) 103 if char == ")": # 如果操作符是右括號,則不滿足條件時棧頂元素肯定是左括號,丟棄棧頂元素及該操作符,取下一個字符 104 stack.pop() 105 continue 106 stack.append(char) # 否則將該操作符壓入棧中 107 else: 108 stack.pop() # 如果操作操作符棧外優先級和棧頂元素棧內優先級相同,則丟棄棧頂元素,獲取下一個字符(在優先級表中只有括號的棧外優先級和站內優先級存在相等情況) 109 stack.reverse() # 將棧中剩余操作符反轉 110 stack.pop() # 丟棄之前存入的#操作符 111 postfix_list.extend([i for i in stack]) # 將反轉並丟棄#操作符的站內操作符全部追加到后序表達式列表中 112 num_list.reverse() # 將操作數列表反轉 113 postfix_list = [num_list.pop() if i == "S" else i for i in postfix_list] # 將翻轉后的操作數列表通過pop方式替換后序表達式中的操作數占位符 114 return postfix_list # 將生成的真正的后序表達式列表返回 115 116 117 """計算四則運算函數,傳入后序表達式列表,使用一個棧保存計算結果,從左到右讀取后序表達式列表,如果碰到操作符, 118 將操作符及其前面的兩個操作數取出,進行計算,將計算的中間結果重新壓入棧中,這樣,遍歷完后序表達式列表之后,棧中 119 存下的就是最后的計算結果了,將計算結果返回給調用函數。""" 120 121 122 def caculate_result(postfix_list): 123 stack = [] # 定義存儲中間計算結果的棧 124 for i in postfix_list: # 遍歷后序表達式列表 125 stack.append(i) # 將后序表達式中的元素壓到棧頂 126 if i == "+": # 假如得到的操作符是+號,將剛壓入到棧頂的+丟棄,並取出其前面兩個操作數,相加,將得到的中間計算結果再壓入棧中 127 stack.pop() 128 right, left = stack.pop(), stack.pop() 129 stack.append(left + right) 130 elif i == "-": # 假如得到的操作符是-號,將剛壓入到棧頂的-丟棄,並取出其前面兩個操作數,相減,將得到的中間計算結果再壓入棧中 131 stack.pop() 132 right, left = stack.pop(), stack.pop() 133 stack.append(left - right) 134 elif i == "*": # 假如得到的操作符是*號,將剛壓入到棧頂的*丟棄,並取出其前面兩個操作數,相乘,將得到的中間計算結果再壓入棧中 135 stack.pop() 136 right, left = stack.pop(), stack.pop() 137 stack.append(left * right) 138 elif i == "/": # 假如得到的操作符是/號,將剛壓入到棧頂的/丟棄,並取出其前面兩個操作數,相除,將得到的中間計算結果再壓入棧中,注意這時候要判斷除數為0的情況 139 stack.pop() 140 right, left = stack.pop(), stack.pop() 141 if right != 0: # 如果除數不為0,則按照正常操作計算 142 stack.append(left / right) 143 else: # 否則,爆出錯誤,並將返回值置為false,程序結束 144 print("除數不能為0...") 145 return "false" 146 return stack[0] # 計算完成之后,棧中就只剩下一個元素了,這個元素就是最終得到的計算結果。 147 148 149 """主調函數,獲取到表達式之后,先對表達式進行合法性檢查,通過之后,再將表達式替換成字符占位表達式和操作數列表, 150 之后再將獲取到的內容通過調用后序表達式生成函數生成后序表達式,再調用計算函數計算最終結果。""" 151 152 153 @show_time # 裝飾器函數,計算該計算過程花費的時間 154 def main(): 155 expression = "1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10.5 * 568/14 )) - " \ 156 "(-4*3)/ (16-3*2) )".strip().replace(" ", "") 157 if checkout(expression): # 表達式合法化檢查通過 158 new_expression, num_list = replace_expression(expression) # 調用表達式替換函數 159 postfix_list = postfix_expression(new_expression, num_list) # 調用后序表達式列表生成函數 160 result = caculate_result(postfix_list) # 調用計算函數獲取最終結果 161 if result == "false": # 如果除數為0的話,打印錯誤,並結束程序 162 print("表達式邏輯錯誤,即將退出...") 163 else: # 否則打印計算結果以及調用eval內置方法計算表達式結果,對比兩種計算方式結果是否一致。 164 print("計算結果:%s" % result) 165 print("eval計算結果:%s" % eval(expression)) 166 else: 167 print("本程序即將退出...") # 表達式未通過合法化檢查,退出程序 168 169 170 if __name__ == "__main__": # 內部測試函數 171 main()