前幾天有個面試題目:計算字符串"1 + (5 - 2) * 3",結果為10,不能用eval()。今天介紹一下用壓棧的方法解一解這個題目,事實上我們的計算器原理也是如此。
1 分析題目
(1)如果計算“1+2”這種兩個數之間的運算,比較簡單,可直接將“字符數字”1,2分解出來,強制轉換為float類型,然后根據中間的運算符加減乘除就行。這題難在需要再復雜的算式中考慮運算符有優先級。
(2)通常我們在計算的時候,實際上也是不斷進行兩個數之間運算,並將算完的結果再和其他數進行運算。比如“1 + 2 + 4”,第一步先算出1+2=3之后,再用算出的結果3和4相加,得到最終結果7。
(3)如果我們能夠將算式“1+2+4”,看做是一個處理好的列表:
將字符串算式 "1+2+4" 處理成: ['1', '+', '2', '+', '4']
那么我們可以通過壓棧的方式計算出結果。首先設置兩個列表(棧),分別存放 數字 和 運算符,然后遍歷 ['1', '+', '2', '+', '4']:
遍歷 處理過的算式列表:['1', '+', '2', '+', '4'] 第一次: 得到數字'1', 轉換成float, 放入數字棧: 數字棧: [1.0, ] 運算符棧: [ ] 第二次: 得到運算符'+',放入運算符棧: 數字棧:[1.0, ] 運算符棧:['+', ] 第三次: 得到數字'2',轉換成float, 放入數字棧: 數字棧:[1.0, 2.0] 運算符棧:['+', ] 第四次: 得到運算符'+', 此時應注意: 運算符棧的最后一位也是'+'號, 現在又來了一個'+'號,說明相鄰兩個運算符的優先級別是一樣的。 既然優先級別是一樣的,四則運算法則告訴我們應該從左往右計算對吧?所以,此處不再一味地將運算符'+'入棧。而是: (1)彈出數字棧中的最后兩位數字,即 2.0 和 1.0 ; (2)彈出運算符棧中的最后一個運算符'+'; (3)將彈出的數字和運算之間進行計算,即計算 2.0 + 1.0 = 3.0; (4)將3.0放入數字棧,代替之前的 1.0 和 2.0; 即: 數字棧:[3.0, ] 運算符棧:[ ] 別忘了,我們第四次得到的運算符'+'號,此時, 如果運算符棧中,彈出上一次運算過的運算符'+'之后,還有別的運算符, 那么我們還應該將運算符棧的最后一個運算符 和 本次得到的運算符 '+' 進行比較,判斷是否是同一級別。 如果同一級別還得繼續彈棧,繼續運算。不在同一級別那就應該將運算符入棧。 而現在,我們的運算符棧已經空了,那么應該把運算符'+'放入運算符棧,即: 數字棧:[3.0, ] 運算符棧:['+', ] 這樣第四次才算大功告成。 第五次: 得到數字'4',轉換成float, 放入數字棧: 數字棧:[3.0, 4.0] 運算符棧:['+', ] 至此我們已經遍歷完算式列表:['1', '+', '2', '+', '4'],但在數字棧和運算符棧中還有元素。 那么我們應該依次彈出最后兩個數字4.0 和3.0,以及最后一個運算符'+',然后進行運算,得到7.0,並代替原來數字棧中的4.0 和3.0,即: 數字棧:[7.0, ] 運算符棧:[ ] 最后得到的最終結果就是數字棧中的第一位元素:7.0。
通過描述計算 "1+2+4" 的過程,我們總結出這一方法計算的兩個重點:
第一個重點:把算式處理成列表形式。如:'-1-2*((-2+3)+(-2/2))' 應該處理成:['-1', '-', '2', '*', '(', '(', '-2', '+', '3', ')', '+', '(', '-2', '/', '2', ')', ')'] 。
第二個重點:建立兩個棧,數字棧和運算符棧。遍歷算式列表,(從前往后取出列表中的元素),將數字放入數字棧,將運算符放入運算符棧。但是,需要通過運算符棧中的最后一個運算符 與 當前拿到的運算符 比較,判斷出應該彈棧進行運算還是直接入棧。
2 總結算法
通過1中的分析我們大致可以整理出如下算法:
1 將算式整理成列表formula_list。 2 循環[為方便描述,我們把此處循環叫循環1],依次取出列表中的元素 e (element縮寫)。 if e 是數字: 加入數字棧num_stack,獲取下一個元素e。 else e 不是數字(即是運算符): while True:(不斷循環,此處是為了不斷比較從算式列表中拿到的運算符和運算符棧中的最后一個運算符的優先級) 如果運算符棧op_stack 為空: 運算符 e 無條件加入運算符棧,並獲取下一個元素e 如果運算符棧不為空: 取出運算符棧最后一個運算符 和 當前運算符e比較,得出一個決策。決策分為[入棧,出棧,0]三種。 如果決策是入棧: 將運算符加入運算符棧,並獲取下一個元素e 如果決策是出棧: 彈出數字棧中的最后兩位數字,運算符棧中的最后一個運算符。 計算,把結果代替原來的數字。 回退到while True循環 如果決策是0,這種情況專門表示運算符棧的最后一個元素是"(" 而當前獲取到的元素是 ")": 彈出運算符棧中的最后一個運算符"(",並丟掉當前元素是")",獲取下一個元素 3 上述處理完之后可能會存在一個問題: 當決策是0的時候,我們 彈出運算符棧中的最后一個運算符"(",並丟掉當前元素是")",獲取下一個元素; 如果這個時候算式列表沒有下一個元素了呢?此時既沒有下一個元素,也沒有繼續運算,【循環1】結束了,但數字棧和運算符棧中可能還有元素。且運算符一定是同一級別的。 所以應該不斷彈棧做運算,直到運算符棧中沒有運算符為止。最后得到的結果就是數字棧的第一位元素。
以上分析我們抽象出幾個函數:
(1)彈棧時計算‘兩個數字和運算符組成的算式’結果的函數。
(2)判斷元素是數字還是運算符的函數。
(3)把算式處理成列表形式的函數。如:'-1-2*((-2+3)+(-2/2))' 應該處理成:['-1', '-', '2', '*', '(', '(', '-2', '+', '3', ')', '+', '(', '-2', '/', '2', ')', ')'] 。
(4)決策函數,決定應該是入棧,彈棧運算,還是彈棧丟棄。
(5)主函數,遍歷算式列表,計算最終結果。
3 兩數運算函數
傳入兩個數字,一個運算符,根據運算符不同返回相應結果。即計算加減乘除:
def calculate(n1, n2, operator): ''' :param n1: float :param n2: float :param operator: + - * / :return: float ''' result = 0 if operator == "+": result = n1 + n2 if operator == "-": result = n1 - n2 if operator == "*": result = n1 * n2 if operator == "/": result = n1 / n2 return result
4 判斷是運算符還是數字
這里可能會想到isdigit()判斷數字,但這個函數不能判斷小數和負數。所以,我們自己寫一個函數判斷是否是運算符:
# 判斷是否是運算符,如果是返回True def is_operator(e): ''' :param e: str :return: bool ''' opers = ['+', '-', '*', '/', '(', ')'] return True if e in opers else False
5 格式化算式為列表
這個步驟需要處理的是區分橫杠‘-’是代表負數還是減號。詳細參見下例,注釋已經十分明了:
# 將算式處理成列表,解決橫杠是負數還是減號的問題 def formula_format(formula): # 去掉算式中的空格 formula = re.sub(' ', '', formula) # 以 '橫杠數字' 分割, 其中正則表達式:(\-\d+\.?\d*) 括號內: # \- 表示匹配橫杠開頭; \d+ 表示匹配數字1次或多次;\.?表示匹配小數點0次或1次;\d*表示匹配數字1次或多次。 formula_list = [i for i in re.split('(\-\d+\.?\d*)', formula) if i] # 最終的算式列表 final_formula = [] for item in formula_list: # 第一個是以橫杠開頭的數字(包括小數)final_formula。即第一個是負數,橫杠就不是減號 if len(final_formula) == 0 and re.search('^\-\d+\.?\d*$', item): final_formula.append(item) continue if len(final_formula) > 0: # 如果final_formal最后一個元素是運算符['+', '-', '*', '/', '('], 則橫杠數字不是負數 if re.search('[\+\-\*\/\(]$', final_formula[-1]): final_formula.append(item) continue # 按照運算符分割開 item_split = [i for i in re.split('([\+\-\*\/\(\)])', item) if i] final_formula += item_split return final_formula
6 決策彈棧還是入棧
這個函數比較難,也比較抽象。比較連續兩個運算符來判斷是入棧還是彈棧:
def decision(tail_op, now_op): ''' :param tail_op: 運算符棧的最后一個運算符 :param now_op: 從算式列表取出的當前運算符 :return: 1 代表彈棧運算,0 代表彈運算符棧最后一個元素, -1 表示入棧 ''' # 定義4種運算符級別 rate1 = ['+', '-'] rate2 = ['*', '/'] rate3 = ['('] rate4 = [')'] if tail_op in rate1: if now_op in rate2 or now_op in rate3: # 說明連續兩個運算優先級不一樣,需要入棧 return -1 else: return 1 elif tail_op in rate2: if now_op in rate3: return -1 else: return 1 elif tail_op in rate3: if now_op in rate4: return 0 # ( 遇上 ) 需要彈出 (,丟掉 ) else: return -1 # 只要棧頂元素為(,當前元素不是)都應入棧。 else: return -1
7 主函數
主函數負責遍歷算式列表中的字符,決定入數字棧或運算符棧或彈棧運算。
def final_calc(formula_list): num_stack = [] # 數字棧 op_stack = [] # 運算符棧 for e in formula_list: operator = is_operator(e) if not operator: # 壓入數字棧 # 字符串轉換為符點數 num_stack.append(float(e)) else: # 如果是運算符 while True: # 如果運算符棧等於0無條件入棧 if len(op_stack) == 0: op_stack.append(e) break # decision 函數做決策 tag = decision(op_stack[-1], e) if tag == -1: # 如果是-1壓入運算符棧進入下一次循環 op_stack.append(e) break elif tag == 0: # 如果是0彈出運算符棧內最后一個(,丟掉當前),進入下一次循環 op_stack.pop() break elif tag == 1: # 如果是1彈出運算符棧內最后一個運算符,彈出數字棧內后兩個元素。 op = op_stack.pop() num2 = num_stack.pop() num1 = num_stack.pop() # 執行計算 # 計算之后壓入數字棧 num_stack.append(calculate(num1, num2, op)) # 處理大循環結束后 數字棧和運算符棧中可能還有元素 的情況 while len(op_stack) != 0: op = op_stack.pop() num2 = num_stack.pop() num1 = num_stack.pop() num_stack.append(calculate(num1, num2, op)) return num_stack, op_stack
8 終極代碼與測試
import re def calculate(n1, n2, operator): ''' :param n1: float :param n2: float :param operator: + - * / :return: float ''' result = 0 if operator == "+": result = n1 + n2 if operator == "-": result = n1 - n2 if operator == "*": result = n1 * n2 if operator == "/": result = n1 / n2 return result # 判斷是否是運算符,如果是返回True def is_operator(e): ''' :param e: str :return: bool ''' opers = ['+', '-', '*', '/', '(', ')'] return True if e in opers else False # 將算式處理成列表,解決橫杠是負數還是減號的問題 def formula_format(formula): # 去掉算式中的空格 formula = re.sub(' ', '', formula) # 以 '橫杠數字' 分割, 其中正則表達式:(\-\d+\.?\d*) 括號內: # \- 表示匹配橫杠開頭; \d+ 表示匹配數字1次或多次;\.?表示匹配小數點0次或1次;\d*表示匹配數字1次或多次。 formula_list = [i for i in re.split('(\-\d+\.?\d*)', formula) if i] # 最終的算式列表 final_formula = [] for item in formula_list: # 第一個是以橫杠開頭的數字(包括小數)final_formula。即第一個是負數,橫杠就不是減號 if len(final_formula) == 0 and re.search('^\-\d+\.?\d*$', item): final_formula.append(item) continue if len(final_formula) > 0: # 如果final_formal最后一個元素是運算符['+', '-', '*', '/', '('], 則橫杠數字不是負數 if re.search('[\+\-\*\/\(]$', final_formula[-1]): final_formula.append(item) continue # 按照運算符分割開 item_split = [i for i in re.split('([\+\-\*\/\(\)])', item) if i] final_formula += item_split return final_formula def decision(tail_op, now_op): ''' :param tail_op: 運算符棧的最后一個運算符 :param now_op: 從算式列表取出的當前運算符 :return: 1 代表彈棧運算,0 代表彈運算符棧最后一個元素, -1 表示入棧 ''' # 定義4種運算符級別 rate1 = ['+', '-'] rate2 = ['*', '/'] rate3 = ['('] rate4 = [')'] if tail_op in rate1: if now_op in rate2 or now_op in rate3: # 說明連續兩個運算優先級不一樣,需要入棧 return -1 else: return 1 elif tail_op in rate2: if now_op in rate3: return -1 else: return 1 elif tail_op in rate3: if now_op in rate4: return 0 # ( 遇上 ) 需要彈出 (,丟掉 ) else: return -1 # 只要棧頂元素為(,當前元素不是)都應入棧。 else: return -1 def final_calc(formula_list): num_stack = [] # 數字棧 op_stack = [] # 運算符棧 for e in formula_list: operator = is_operator(e) if not operator: # 壓入數字棧 # 字符串轉換為符點數 num_stack.append(float(e)) else: # 如果是運算符 while True: # 如果運算符棧等於0無條件入棧 if len(op_stack) == 0: op_stack.append(e) break # decision 函數做決策 tag = decision(op_stack[-1], e) if tag == -1: # 如果是-1壓入運算符棧進入下一次循環 op_stack.append(e) break elif tag == 0: # 如果是0彈出運算符棧內最后一個(, 丟掉當前),進入下一次循環 op_stack.pop() break elif tag == 1: # 如果是1彈出運算符棧內最后兩個元素,彈出數字棧最后兩位元素。 op = op_stack.pop() num2 = num_stack.pop() num1 = num_stack.pop() # 執行計算 # 計算之后壓入數字棧 num_stack.append(calculate(num1, num2, op)) # 處理大循環結束后 數字棧和運算符棧中可能還有元素 的情況 while len(op_stack) != 0: op = op_stack.pop() num2 = num_stack.pop() num1 = num_stack.pop() num_stack.append(calculate(num1, num2, op)) return num_stack, op_stack if __name__ == '__main__': formula = "1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2))" print("算式:", formula) formula_list = formula_format(formula) result, _ = final_calc(formula_list) print("計算結果:", result[0]) # 算式: 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2)) # 計算結果: 2776672.6952380957
我們看一下谷歌運算的結果:
說明咱們算對了,不妨多測試一些算式看看。
本篇完。