標籤:turn 入棧 沒有 分割 連續 技術分享 通過 pre 技術
前幾天有個面試題目:計算字串"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()判斷數字,但這個函數不能判斷小數和負數。所以,我們自己寫一個函數判斷是否是運算子:
# 判斷是否是運算子,如果是返回Truedef 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) # 以 ‘橫杠數字‘ 分割, 其中Regex:(\-\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 redef 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# 判斷是否是運算子,如果是返回Truedef 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) # 以 ‘橫杠數字‘ 分割, 其中Regex:(\-\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_formuladef 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 -1def 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_stackif __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
我們看一下Google運算的結果:
說明咱們算對了,不妨多測試一些算式看看。
本篇完。
Python實現計算機