數據結構の學習記錄(二):如何給中綴表達式加括號

對算法類的問題,最大的忌諱就是,想都不想直接寫代碼

如果你的這樣的程序猿,那麼狠抱歉,要麼就是你會花上數十倍的時間修改你的簡單STUPID錯誤,要麼就是你很短時間就能得到正確的結果,如果是這樣那麼恭喜你,你進化了!

說上述言論,筆者的區分點是你的目標究竟是一個碼農還是一個算法工程師。兩者的區別從工資上看不說你應該也懂:-)

(等不及的你可以迅速下拉到分界線以下尋找乾貨)

下面附上筆者的coding習慣。首先將思路以母語(中文可以但推薦英語)的形式寫在草稿紙上,覺得沒有問題之後,再將其用鉛筆轉換爲代碼,之後將其寫在你青睞的有調試功能的開發平臺上(對於筆者是Spyder,PyCharm)。如果你發現的開發平臺沒有調試功能,那麼你需要立刻更換!對於剛寫好的代碼,第一步永遠是調試,而不是直接運行。最後一步是不斷優化,考慮所有的情況,並且優化時間複雜度和空間複雜度,永遠不是喊着“哈利路亞”,然後發郵件告訴你老闆任務完成了。

                                                                              第一步:將算法寫在草稿紙上 

                                                                            第二步:將其轉換爲鉛字版 代碼

 

                                                               第三步:轉化爲實際的代碼,這一步基本上是調試


不幸的是,筆者的博客將不會有完整代碼(示範代碼除外),所有代碼均已上傳到碼雲上。如果時間和精力允許的話,強烈建議你根據思路,手動寫一遍,相信你會感覺到全身毛孔舒張而不是想砸電腦的快感。

術語:
運算符:進行數字運算的字符,如‘+’‘-’等
操作數:數字‘1’,‘2.5’等
棧: 存儲數據的結構,講究“先進後出”,即最先進棧的數據,最後出棧;有順序存儲和鏈式存儲兩種。

正則表達式:通常被用來檢索、替換那些符合某個模式(規則)的文本。

瞭解了基本術語後,你知道所謂構建括號表達式,就是根據優先級和前後順序依次加括號。如2+3*5/2,你應該得到的是2+((3*5)/2)而不是(2+3)*5/2,最外面的括號可以不加,但是不允許出現2+(3*5)/2這種不“完全”的括號表達式,尤其是對於多項的表達式更是如此。

非常糟糕的是,用戶的需求千變萬化,因此你需要永遠修改你的代碼以滿足用戶的需要。就像這個問題一樣,輸入如果是整數表達式那很容易解決,但是對於小數和負數,Damn it,沒有比這更噁心的了。多虧了強大的被稱爲“宇宙最好的語言”的Python ,它如膠水一樣的靈活,容易上手,提供強大的庫。好了,廢話不多說了,你已經猜到我要說什麼了。那就是正則表達式(REGEX)。如果你不熟悉的話,在我之前的博文有講解,下面一張表格記錄了各種語言對正則表達式的支持情況。

可見Java, Python, Perl,C#是完全支持正則表達式的。我們希望構建一個expression_spliter函數來將輸入的字符,轉換爲分割之後的列表。舉個例子input:‘4.2/(-3.5*+2+2^-2.2/4*5%6)+3-(-4.5)’ ,output:[' 4.2', '/', '(', '-3.5', '*', '+2', '+', '2', '^', '-2.2', '/', '4', '*', '5', '%', '6', ')', '+', '3', '-', '(-4.5)']。我們先考慮最簡單的整數情況,(1+2*3)/4-2,你的代碼將會如下:

def expression_spliter(expression):
    #split the expression into list, using POWERFUL REGEX MODULE to ignite your productivity 
    import re
    return re.findall('(?:[0-9\.]+|[\+\-\*\/\(\)\%\^])',expression)

真的是太炫酷了,僅用兩行就解決了。我們稍作解釋,(?A:B)表示匹配A模式或匹配B模式,[0-9\.]表示一個0-9或'.'的字符。反斜槓是轉義字符,就是讓 某些符號失去功能變爲單純的符號。後面的+表示匹配前一個字符一個或多個。因此[0-9\.]+可以表示任何小數,[\+\-\*\/\(\)\%\^] 表示匹配運算符。

但是上述代碼的缺點是不能匹配符號數(包括帶括號的和不帶括號的),另外如果一個無符號數位於表達式最前面也需要注意。聰明的你一定能完成這項任務:-)。無論有符號數有沒有括號我們都要考慮!

然後,你需要考慮如何添加括號,如果你用列表裝表達式,然後用下標來索引,那麼我保證你會崩潰,因爲每加一次括號,下標都會改變。因此,你立馬轉換思路,用鏈表來存儲吧!這是一個不錯的注意我承認,但是你得實現並熟悉一個鏈表,這在我之前的教程裏有。

下面詳細講如何添加括號。 對一個運算符 '?' 而言,它的前面和後面無異乎有四種情況:(1)數字?數字
(2)')'?數字(3)數字?'('(4)')' ? '(',如果前面後面只是數字的話,對於?前面的數字前面一位插入括號即可,後面同理。對於情況(4),我們希望得到    '(''('....')' ? '(' ....')'')'   ,紅色是與?最近的括號匹配的括號,我們希望找到它們並在它們之前或之後插入粗體的括號;... 表示任意數量匹配的左括號或右括號。可以這樣,舉例說在?前面加括號,用一個標誌l_count統計 ?前面左括號的數目,用r_count統計?前面右括號的數目。因爲在最近的 ')' 裏已經將r_count+1了,所以將節點不斷前移p = p.prev, 其終止條件應該爲r_count-l_count == 0 or p.next == None. 在?後面加括號的代碼大同小異。你的代碼應該如下:

def insert_brackets(r,m,ls):
    #if there is already brackets then quit
    
    if r.val == ')':
        l_count=r_count=0
        while True:
            if r.val=='(':l_count+=1
            elif r.val==')': r_count+=1
            if l_count-r_count == 0 or r.prev==None: break
            r = r.prev
    if m.val == '(':
        
        l_count=r_count=0
        while True:
            if m.val=='(':l_count+=1
            elif m.val==')': r_count+=1
            if r_count-l_count == 0 or m.next == None: break
            m = m.next

     ls.insert(r,'(','front')
     ls.insert(m,')','back')

r,m分別表示?前一個節點和後一個節點,ls表示鏈式堆棧。其中insert(pres_node,data,orient)是在pre_node 前或後插入node(data)的函數。 但是很快你會發現一個問題:我如何避免重複加括號,因爲重複加括號沒有必要而且增大計算開銷降低可讀性。很簡單,判斷左右邊是否已經有匹配的括號即可,最後兩行應該爲:

    if r.prev!=None and  r.prev.val== '(' and m.next!=None and m.next.val==')':return
    else:
        ls.insert(r,'(','front')
        ls.insert(m,')','back')

好了,現在我們得到了一個添加括號的函數。下面要做的就是知道給什麼節點添加括號。堆棧擁有LIFO的特性,非常適合來記憶元素,因爲運算符符是添加括號的關鍵,我們新建一個棧s_oper來保存運算符符。現在,我們可以用一個字典P_dict來保存優先級,P_dict = {'^':3,'*':2,'/':2,'%':2,'+':1,'-':1,'(':0},其中'('擁有最低的優先級。我們需要遍歷表達式exp,先把exp轉換爲鏈式存儲的棧,用p指向其頭部。基本的遍歷方法是p = p.next。有以下幾種情況:

(1)如果遇到運算符或'(',則壓入s_oper;

(2)如果遇到')',就說明至少有一個'('在棧內,我們首先要把最近一組括號內的運算符按優先級和進展順序加括號。我們先彈出一個節點oper,然後將之與棧頂的元素top進行比較,直到oper=='('時退出循環,(a) 如果oper優先級大於top,則對oper元素進行加括號,調用之前寫好的insert_brackets函數;(b) 如果oper優先級小於top,我們需要繼續彈棧,直到oper優先級大於top或者棧爲空時停止循環;(c)如果oper優先級等於top,那麼我們必須根據先後順序來添加括號。比如 (3*5/6),如果你添成(3*(5/6))就是錯的。爲了記憶這種順序特性,我們不得不再創建一個棧s_same_pri,如果遇到優先級相等的情況,就把從s__oper彈出的oper_temp壓入堆棧;遇到oper優先級小於oper_temp時,事實上,我們得考慮這種情況,比如2^2*3/4%5,如果你按照入棧順序加括號,得到的是2^(((2*3)/4)%5),這顯然是不對的,爲此我們可以在oper優先級小於oper_temp時,直接對oper_temp加括號,然後停止循環;遇到oper優先級大於oper_temp時,直接停止循環。在s_oper中處於下面的元素在s_same_pri被翻轉了過來,這樣我們再把s_same_pri 彈棧,依次加括號即可。

         很不錯你能看到這裏,但是你不能指望馬上得到漂亮的結果。因爲總有些特殊情況打亂你的計劃。比如括號裏面有很多項,如(1+5*6/3-4^4),你前面的代碼只能得到(1+((5*6)/3)-(4^4)),你希望的是(1+(((5*6)/3)-(4^4)))或者((1+((5*6)/3))-(4^4))。如果你一開始能想到最極端的情況,這對你的調試總是有很大的幫助。連接項與項的要麼是“+”要麼是“-”,因爲它們的優先級爲1,僅高於“(”。這時候,你又想到了堆棧,“不是吧,又要用堆棧?”我猜你肯定會這樣想。但是事實就是如此,你必須反覆使用基礎的數據結構,它們是如此的重要!可以用一個新的棧s_item來存儲項,但其實不用,我們只用“記錄”連接項的“+”或“-”即可。在括號內所有的子項都加完括號後,我們就可以把s_item依次彈出在項之間加括號,直到棧爲空停止。

好了,對exp遍歷完之後,我們已經處理了含括號的部分,但是s_oper內還有一些節點,它們是在括號外面(相對而言)。Smart as you are, 你知道它們可以像括號內的情況一樣處理。我們先彈出一個節點oper,然後將之與棧頂的元素top進行比較,直到棧爲空時退出循環,(a) 如果oper優先級大於top,則對oper元素進行加括號,調用之前寫好的insert_brackets函數;(b) 如果oper優先級小於top,我們需要繼續彈棧,直到oper優先級大於top或者棧爲空時停止循環;(c) 如果oper優先級等於top,那麼我們必須根據先後順序來添加括號。比如 (3*5/6),如果你添成(3*(5/6))就是錯的。爲了記憶這種順序特性,我們不得不再創建一個棧s_same_pri,如果遇到優先級相等的情況,就把從s__oper彈出的oper_temp壓入堆棧;遇到oper優先級小於oper_temp時,事實上,我們得考慮這種情況,比如2^2*3/4%5,如果你按照入棧順序加括號,得到的是2^(((2*3)/4)%5),這顯然是不對的,爲此我們可以在oper優先級小於oper_temp時,直接對oper_temp加括號,然後停止循環;遇到oper優先級大於oper_temp時,直接停止循環。在s_oper中處於下面的元素在s_same_pri被翻轉了過來,這樣我們再把s_same_pri 彈棧,依次加括號即可。項與項之間的處理和上一段相同。

最後將鏈表轉換爲列表輸出即可。


下面是筆者得到的部分結果,考慮了有符號數情況。

是否感覺自己的腦容量增加了呢,希望本文對你有幫助!

PS 筆者3.29日完成V1.0代碼,到今天4.15 V11.0 經過無數測試後仍發現bug,無論如何,筆者在代碼完善之後會上傳。不斷髮現bug,修改bug這大概就是程序猿的宿命吧。


如果你實現了模方^,那麼問題來了。它的優先級最高一定爲3. 我們考慮一種極特殊情況,5/0.5^5%2*3,也就是說在^後仍然有兩個及以上的*或/,那麼上述代碼將失效,在括號裏面和外面都是一樣的。我們必須首先對'^'符號進行加括號,然後依次按照/%*順序加括號。現在請聰明的你想想應該如何解決這個問題。。。

。。。

很棒!你已經得到答案了。問題在於判斷某個運算符X與下個運算符優先級相同情況中,我們遇到了s_oper彈出運算符優先級打於X時最後加上了break。這樣它不會判斷前面的運算符。我們把break去掉。但這樣s_oper有可能彈空,所以處理括號內容時結束條件由oper.val=='('改爲not oper or oper.val=='('更爲合適。 

筆者在測試時發現(5/6^5+1)/0.5^5+2這樣的簡單情況也有問題,原因是在括號外,如果只有一個優先級爲2的運算符則不會對此運算符加括號,我們在判斷s_oper爲空break之前,增加一個條件,若彈出的運算符不爲None,對其加括號。

在 python中,不爲None的變量都可以視爲True。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章