用Python解決資料結構與演算法問題(三):線性資料結構之棧

我是管小亮發表於2020-01-29

歡迎關注WX公眾號:【程式設計師管小亮】

python學習之路 - 從入門到精通到大師

〇、寫在前面

哼哼,勤勞的日更博主又回來了,我的小粉絲們期待一下吧 ?

一、線性資料結構

我們這個系列的前兩期:

新的開始準備從四個 簡單但重要 的概念開始研究資料結構,分別是:

  • 佇列
  • deques
  • 列表

這四個是一類資料的容器,資料項之間的順序由新增或刪除的順序決定。一旦一個資料項被新增,它相對於前後元素一直保持該位置不變,諸如此類的資料結構被稱為 線性資料結構

線性資料結構 有兩端,有時被稱為 左右,某些情況被稱為 前後,也可以稱為 頂部底部,名字這個東西不重要,只要你能知道是什麼意思就行哈。將兩個 線性資料結構 區分開的方法是新增和移除項的方式,特別是新增和移除項的位置,例如一些結構允許從一端新增項,另一些允許從另一端移除項。這些變種的形式產生了電腦科學最有用的資料結構。

二、什麼是棧

在這裡插入圖片描述
(有時稱為 後進先出棧)是一個項的有序集合,其中新增移除新項總髮生在同一端,這一端通常稱為 頂部,與 頂部 對應的端稱為 底部

底部 很重要,因為在棧中靠近 底部 的項是儲存時間最長的,相對的最近新增的項是最先會被移除的,所以很多時候這種排序原則被稱為 LIFO,即 後進先出

為什麼會出現這種情況?主要是因為 基於在集合內的時間長度做的排序,即較新的項靠近頂部,較舊的項靠近底部。

的例子很常見,就用一個通俗的例子來說明吧,現在想象桌上有一堆書,如下圖:
在這裡插入圖片描述
只有頂部的那本書的封面是可見的,如果想要看到其他書的封面,只有先移除它們上面的書。

或者看一個稍稍專業一丟丟的例子,下圖展示了另一個 ,包含了很多 Python 物件。
在這裡插入圖片描述
如果我們想要對這個 進行操作的話,移除的順序跟放置的順序是剛好相反的,下圖展示了 Python 資料物件建立和刪除的過程,注意觀察它們的順序:
在這裡插入圖片描述
棧之所以重要的原因之一,也是它能 反轉項的順序。想想這種反轉的屬性,你可以想到使用計算機的時候所碰到的例子,例如,每個 web 瀏覽器都有一個返回按鈕,當瀏覽網頁時,這些網頁被放置在一個 中(實際是網頁的網址)。當前檢視的網頁是放在頂部的,而第一個檢視的網頁在放在底部的,這個時候如果按 返回 按鈕,將會按照相反的順序瀏覽剛才的頁面,也就是先出來的是剛才的頁面,然後才是再之前的頁面!!!

三、抽象資料型別

在這裡插入圖片描述
的抽象資料型別由以下結構和操作定義:

如上面所述, 被構造為項的有序集合,如果想要對 進行新增和移除項,則操作如下:

  • Stack() 建立一個空的新棧。 它不需要引數,並返回一個空棧
  • push(item) 將一個新項新增到棧的頂部。它需要 item 做引數並不返回任何內容
  • pop() 從棧中刪除頂部項。它不需要引數並返回 item,棧被修改
  • peek() 從棧返回頂部項,但不會刪除它。它不需要引數,不修改棧
  • isEmpty() 測試棧是否為空。它不需要引數,並返回布林值
  • size() 返回棧中的 item 數量。它不需要引數,並返回一個整數

例如,s 是已經建立的空棧,下表展示了 操作序列的結果,其中 的頂部項在項列的最右邊。
在這裡插入圖片描述

四、用Python實現棧

在這裡插入圖片描述
到現在,到這裡,已經將 清楚地解釋並定義了,並且還定義了 抽象資料型別,那麼接下來讓我們將把注意力轉向使用 Python 實現

回想一下,當給 抽象資料型別 一個物理實現時,這個實現可以稱為 資料結構。而在 Python 中,與任何物件導向程式語言一樣,抽象資料型別(如 )的選擇的實現就是建立一個新類,即用類的方法來實現 操作。此外,為了實現作為元素集合的 ,使用由 Python 提供的原語集合的能力是有意義的,最後,我們將使用 列表 作為底層實現。

Python 中的列表類提供了有序集合機制和一組方法。例如,如果有列表 [2, 5, 3,6, 7, 4],只需要確定列表的哪一端將被認為是 頂部。一旦確定,可以使用諸如 appendpop 的列表方法來實現操作。

假定列表的結尾將儲存 頂部 元素,隨著 增長(push 操作),新項將被新增到列表的末尾,pop 也操作列表末尾的元素。以下程式碼為 的實現:

class Stack:
     def __init__(self):
         self.items = []

     def isEmpty(self):
         return self.items == []

     def push(self, item):
         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)

記住只需要定義類的實現,從而建立了一個 ,然後使用它。

下面的程式碼展示了通過例項化 Stack 類執行上表中的操作。注意,Stack 類的定義是從 pythonds 模組匯入的。

Note
pythonds 模組包含本書中討論的所有資料結構的實現,它根據以下部分構造:基本資料型別,樹和圖。
該模組可以從 pythonworks.org下載。

from pythonds.basic.stack import Stack

s=Stack()

print(s.isEmpty())
s.push(4)
s.push('dog')
print(s.peek())

s.push(True)
print(s.size())
print(s.isEmpty())

s.push(8.4)
print(s.pop())
print(s.pop())
print(s.size())

在這裡插入圖片描述

五、簡單括號匹配

現在把注意力繼續轉向使用 去解決一個真正的計算機問題——算術表示式,你大概會這麼寫:(5+6)*(7+8)/(4+3),其中括號用於命令操作的執行。可以看到在這個例子中,括號必須以匹配的方式出現。括號匹配意味著每個開始符號具有相應的結束符號,並且括號能被正確巢狀。

然後考慮下面正確,

  • 匹配的括號字串:
(()()()())

(((())))

(()((())()))
  • 對比那些不匹配的括號:
((((((())

()))

(()()(()

區分括號是否匹配的能力是識別很多程式語言結構的重要部分。最具挑戰的是如何編寫一個演算法,能夠從左到右讀取一串符號,並決定符號是否平衡。

為了解決這個問題,需要做一個認真地觀察——從左到右處理符號時,最近開始的符號必須與下一個關閉符號相匹配(見下圖)。
在這裡插入圖片描述
此外,處理的第一個開始符號必須等待,直到其匹配最後一個符號。結束符號以相反的順序匹配開始符號,它們從內到外匹配,這是一個可以用 解決問題的線索。

一旦你認為 是儲存括號的恰當的資料結構,那麼演算法就是很直接的了,即從 空棧 開始,由左到右地處理括號字串。如果一個符號是一個開始符號,將其作為一個訊號,表示對應的結束符號稍後會出現;如果一個符號是一個結束符號,將其作為一個訊號,對應前面的開始符號,表示彈出

只有彈出 的開始符號可以匹配對應的結束符號,否則括號將保持匹配狀態。如果任何時候 上沒有出現符合開始符號的結束符號,則字串不匹配。最後,當所有符號都被處理後, 應該是空的。

實現此演算法的 Python 程式碼如下:

from pythonds.basic.stack import Stack

def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol == "(":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                s.pop()

        index = index + 1

    if balanced and s.isEmpty():
        return True
    else:
        return False

print(parChecker('((()))'))
print(parChecker('(()'))

在這裡插入圖片描述

六、符號匹配

上面講到的括號匹配問題是許多程式語言都會出現的,是一種符號匹配的特定情況。匹配和巢狀不同種類的開始和結束符號的情況是經常發生的,比如,在 Python 中,方括號 [] 用於列表,花括號 {} 用於字典,括號 () 用於元組和算術表示式,只要每個符號都能保持自己的開始和結束關係,就可以混合符號。

符號字串如:

{ { ( [ ] [ ] ) } ( ) }

[ [ { { ( ( ) ) } } ] ]

[ ] [ ] [ ] ( ) { }

這些符號被恰當的匹配了,因為不僅每個開始符號都有對應的結束符號,而且符號的型別也匹配。

相反下面這些字串是沒法匹配的:

( [ ) ]

( ( ( ) ] ) )

[ { ( ) ]

其實上一節的簡單括號檢查程式可以輕鬆地擴充套件處理這些新型別的符號。回想一下,每個開始符號被簡單的壓入 中,等待匹配的結束符號出現。當出現結束符號時,唯一的區別 是必須檢查確保它能正確地匹配 頂部 的開始符號的型別:

  • 如果兩個符號不匹配,整個字串沒都被處理完而是有什麼留在 中,則字串不匹配;
  • 如果兩個符號匹配,整個字串都被處理完並且沒有什麼留在 中,則字串匹配。

Python 程式如下:

from pythonds.basic.stack import Stack

def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol in "([{":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                top = s.pop()
                if not matches(top,symbol):
                       balanced = False
        index = index + 1
    if balanced and s.isEmpty():
        return True
    else:
        return False

def matches(open,close):
    opens = "([{"
    closers = ")]}"
    return opens.index(open) == closers.index(close)


print(parChecker('{{([][])}()}'))
print(parChecker('[{()]'))

在這裡插入圖片描述
唯一的變化是 16 行,我們稱之為 輔助函式匹配,即必須檢查棧中每個刪除的符號,以檢視它是否與當前結束符號匹配。如果不匹配,則布林變數 balanced 被設定為 False


小結一下

這兩個例子表明, 是計算機語言結構處理非常重要的資料結構,幾乎你能想到的任何巢狀符號必須按照平衡匹配的順序。當然了, 還有其他重要的用途,接下來會繼續討論。。。


七、十進位制轉換成二進位制

在學習計算機的過程中,你可能或者說是一定已經接觸過了二進位制。可以這麼肯定地說,二進位制在電腦科學中是非常重要的,因為儲存在計算機內的所有值都是以 0 和 1 儲存的。所以如果沒有能力在二進位制數和普通字串之間轉換,那麼我們與計算機之間的互動就會非常棘手。

其實我們平時最常用到的應該是整數值,它是常見的資料項,它一直用於計算機程式和計算。尤其記得的是在數學課上學習它,進行加減乘除以及平方開方等等,當然最後是用十進位制或者基數是 10 來表示它們的。

十進位制 23310233_{10} 以及對應的二進位制表示 11101001211101001_2,如果想要用計算公式去分別解釋的話,是:

23310=2×102+3×101+3×100233_{10} = 2\times10^{2} + 3\times10^{1} + 3\times10^{0}

111010012=1×27+1×26+1×25+0×24+1×23+0×22+0×21+1×2011101001_2 = 1\times2^{7} + 1\times2^{6} + 1\times2^{5} + 0\times2^{4} + 1\times2^{3} + 0\times2^{2} + 0\times2^{1} + 1\times2^{0}

但是如何能夠容易並迅速地將整數值轉換為二進位制呢? 答案是 除 2 演算法(如果你還記得計算機基礎和數位電子電路等等這些課的話,應該會很容易地想起它來),不過在這裡我們使用這個演算法是因為它用 來跟蹤二進位制結果的數字(其實原理和以前學過的類似,不過這裡我們是使用演算法來實現這個過程)。

除 2 演算法假定我們從大於 0 的整數開始,不斷地進行迭代,將十進位制數除以 2,並跟蹤該算式的餘數,具體操作如下:第一個除以 2 的餘數說明了這個值是偶數還是奇數,偶數有 0 的餘數,記為 0;奇數有餘數 1,記為 1。記錄下這些餘數,它們就是我們想要的二進位制數的元素,把它們構建為一個數字序列,要記得這裡的排序是一個倒序!!!也就是說第一個餘數實際上是序列中的最後一個數字,如下圖:
在這裡插入圖片描述
看到這裡沒學過的小夥伴估計是懵逼max,學過的小夥伴也可能懵逼ing,我來解釋一下怎麼回事,上圖左側的計算過程顯示了最終的結果中的元素應該是什麼,但是如果從上到下寫下這些餘數的話,那麼應該是:

1001011110010111

到這裡沒問題是吧,但是實際的結果應該是反著讀,別問我為什麼,記住就完事了。

1110100111101001

到這裡,我們再次看到了反轉的屬性,這表示 可能是解決這個問題的資料結構,或者說它可能非常適合解決這個問題。

開始實現這個演算法,最重要的 除 2 演算法,函式 divideBy2 傳入了一個十進位制的引數,並重復除以 2。

from pythonds.basic.stack import Stack

def divideBy2(decNumber):
    remstack = Stack()

    while decNumber > 0:
        rem = decNumber % 2
        remstack.push(rem)
        decNumber = decNumber // 2

    binString = ""
    while not remstack.isEmpty():
        binString = binString + str(remstack.pop())

    return binString

print(divideBy2(42))

在這裡插入圖片描述
其中第 7 行使用內建的模運算子 % 來提取餘數,第 8 行將餘數壓到棧上,當除到 0 後,11-13 行構造了一個二進位制字串。

這個用於二進位制轉換的演算法可以很容易的擴充套件以執行任何基數的轉換。在電腦科學中,通常會使用很多不同的編碼,其中最常見的是二級制,八進位制和十六進位制。

十進位制 233233 和它對應的八進位制和十六進位制 3518351_8, E916E9_{16},解釋如下:

3518=3×82+5×81+1×80351_8 = 3\times8^{2} + 5\times8^{1} + 1\times8^{0}

E916=14×161+9×160E9_{16} = 14\times16^{1} + 9\times16^{0}

我們可以通過修改 divideBy2 函式,使它不僅能接受十進位制引數,還能接受預期轉換的基數。除 2 的概念被簡單的替換成更通用的 除基數。所以在下面的程式碼中展示的是一個名為 baseConverter 函式,採用十進位制數和 2 到 16 之間的任何基數作為引數,餘數部分仍然入棧,直到被轉換的值為 0,同時建立一組數字,用來表示超過 9 的餘數,十進位制以上的基數需要這些數字來表示。

from pythonds.basic.stack import Stack

def baseConverter(decNumber,base):
    digits = "0123456789ABCDEF"

    remstack = Stack()

    while decNumber > 0:
        rem = decNumber % base
        remstack.push(rem)
        decNumber = decNumber // base

    newString = ""
    while not remstack.isEmpty():
        newString = newString + digits[remstack.pop()]

    return newString

print(baseConverter(25,2))
print(baseConverter(25,16))
print(baseConverter(31,16))

在這裡插入圖片描述

八、中綴,字首和字尾表示式

在這裡插入圖片描述
當編寫一個算術表示式如 B*C 時,表示式的形式使你能夠正確理解它。在這種情況下,你知道它的意思是 B 乘以 C, 因為乘法運算子 * 出現在表示式中,這種型別的符號稱為 中綴,因為運算子在它處理的兩個運算元之間。再來看另外一個 中綴 示例,A+B*C,運算子 +* 仍然出現在運算元之間。這裡面有個問題是,它們分別作用於哪個運算數上,+ 作用於 A 和 B,還是 * 作用於 B 和 C?表示式似乎有點模糊。

不過事實上這是個非常 easy 的問題,因為你已經讀寫過這些型別的表示式很長一段時間,所以它們不會導致什麼問題。這是因為你知道它們分別是運算子 +*,而每個運算子都有一個計算的優先順序,優先順序較高的運算子在優先順序較低的運算子之前使用,而 唯一改變順序 的是括號的存在。算術運算子的優先順序是將乘法和除法置於加法和減法之上,而如果出現具有相等優先順序的兩個運算子,則使用從左到右的順序 排序關聯

現在來使用運算子優先順序解釋下幾個表示式:

  • 表示式 A+B*C,B 和 C 首先相乘,然後將 A 與該結果相加;
  • 表示式 (A+B)*C,將強制在乘法之前執行 A 和 B 的加法;
  • 表示式 A+B+C,最左邊的 + 會首先使用。

到這裡了,你可能會覺得這一切對你來說都很明顯,但請記住,計算機需要準確知道要執行的操作符和順序!!!

一種保證不會出問題(對操作順序產生混淆)的方法是建立一個稱為 完全括號表示式 的表示式,這種型別的表示式對每個運算子都使用一對括號,因為括號沒有歧義的指示操作順序,也沒有必要記住任何優先規則,所以只要按照括號有限就行了。舉個例子:表示式 A+B*C+D 可以重寫為 ((A + (B * C)) + D),表明先乘法,然後是左邊的加法,A + B + C + D 可以寫為 (((A + B) + C) + D),因為加法操作從左向右相關聯。


有兩種非常重要的表示式格式,你可能一開始不會很明顯的看出來,以中綴表示式 A+B 為例:如果我們將運算子移動到開頭會發生什麼?結果表示式變成 + A B;同樣,我們也可以將運算子移動到結尾,得到 A B +,這樣看起來真的有點奇怪,但是並不是沒有意義的啊。改變操作符的位置得到了兩種新的表示式格式,字首和字尾:字首表示式符號要求所有運算子在它們處理的兩個運算元之前;字尾則要求其操作符在相應的運算元之後。

具體例子可以看下錶:
在這裡插入圖片描述
其中:

  • A+B*C 將在字首中寫為 + A * B C,乘法運算子緊接在運算元 B 和 C 之前,表示 * 優先於 +,然後加法運算子出現在 A 和乘法的結果之前。
  • 在字尾中,表示式將是 A B C * +,再次,操作的順序被保留,因為 * 緊接在 B 和 C 之後出現,表示 * 具有高優先順序,+ 優先順序低。

雖然操作符在它們各自的運算元前後移動,但是順序相對於彼此保持完全相同。

現在考慮中綴表示式 (A + B) * C,回想下,在這種情況下,中綴需要括號在乘法之前強制執行加法。然而,當 A+B 寫到字首中時,加法運算子簡單的移動到運算元 + A B 之前。這個操作的結果成為乘法的第一個運算元,乘法運算子移動到整個表示式的前面,得出 * + A B C;同樣,在字尾 A B +中,強制先加法,可以直接對該結果和剩餘的運算元 C 相乘,然後,得出字尾表示式為 A B + C *

再次考慮這三個表示式(見下表),為什麼在字首和字尾的時候就不需要括號了呢?答案是操作符對於它們的運算元不再模糊,只有中綴才需要括號,字首和字尾表示式的操作順序完全由操作符的順序決定。
在這裡插入圖片描述

下表展示了一些其他的例子:
在這裡插入圖片描述
小結一下:其實這個改寫,無論是字首還是字尾,離字母越近的,運算子的優先順序就越高。。。我個人是這麼理解的哈 ?

8.1、中綴表示式轉換字首和字尾

到目前為止,我們已經使用特定方法在中綴表示式和等效字首和字尾表示式符號之間進行轉換。不過你一定有些吐槽,這個改寫實在是。。。我懂得,所以正如你期望的一樣,是有一些演算法來執行轉換的,允許任何複雜表示式的轉換。

考慮的第一種技術使用前面討論的 完全括號表示式 的概念,回想一下,A + B * C 可以寫成 (A +(B * C)),以明確標識乘法優先於加法。然而,仔細觀察,你可以看到每個括號對還表示運算元對的開始和結束,中間有相應的運算子。

來看看上面的子表示式 (B * C) 中的右括號,如果將乘法符號移動到那個位置,並刪除匹配的左括號,得到 B C *,實際上已經將子表示式轉換為字尾符號。 如果加法運算子也被移動到其相應的右括號位置並且匹配的左括號被去除,則將得到完整的字尾表示式(如下圖)。
在這裡插入圖片描述
如果不是將符號移動到右括號的位置,而是將它向左移動,就可以得到字首符號(如下圖)。
在這裡插入圖片描述
圓括號對應的位置實際上是包含運算子最終位置的線索的,或許這就是括號生效的方式呢?

所以為了轉換表示式,無論是對字首還是字尾符號,先根據操作的順序把表示式轉換成 完全括號表示式。然後將包含的運算子移動到左或右括號的位置,具體取決於需要字首或是字尾符號。

這裡面有個更復雜的例子——(A + B) * C - (D - E) * (F + G),下圖顯示瞭如何將其轉換為字首和字尾:
在這裡插入圖片描述
就是把對應的運算子移動到前面或者後面的括號上。。。你懂了嗎???

8.2、中綴轉字尾通用法

在使用中,我們需要開發一個演算法來將任何中綴表示式轉換為字尾表示式,而不是像上面說的那樣去一個一個的去轉換。 為了做到這一點,來仔細看看這個轉換過程。

再次考慮這個例子,也就是表示式 A + B * C,如上所示,A B C * + 是其等價的字尾表示式。 你應該已經注意到,運算元 A,B 和 C 保持在它們的相對位置,只有操作符改變位置。再看中綴表示式中的運算子,從左到右出現的第一個運算子為 +,然而在字尾表示式中,+ 在結束位置,因為下一個運算子 * 的優先順序高於加法,即原始表示式中的運算子的順序在生成的字尾表示式中相反。

當處理表示式時,操作符必須儲存在某處,因為它們相應的右運算元還沒有看到。 此外,這些儲存的操作符順序可能由於它們的優先順序而需要反轉,這是在該示例中的加法和乘法的情況,由於加法運算子在乘法運算子之前,並且具有較低的優先順序,因此需要在使用乘法運算子之後出現。 由於這種順序的反轉,考慮使用 來儲存運算子直到用到它們是有意義的。

再考慮一個複雜一些的例子,(A + B)* C 的情況會是什麼樣呢? 回想一下,A B + C * 是等價的字尾表示式。從左到右處理此中綴表示式,先看到的是 +,在這種情況下,當我們看到 *+ 已經放置在結果表示式中,由於括號的優先順序高於 *,所以當看到左括號時,儲存它,表示高優先順序的另一個運算子將出現。該操作符需要等到相應的右括號出現以表示其位置(回憶完全括號的演算法),當右括號出現時,可以從 中彈出操作符。

當從左到右掃描中綴表示式時,我們將使用 來保留運算子,這將提供在第一個例子中注意到的反轉。 堆疊頂部 將始終是最近儲存的運算子。每當讀取一個新的運算子時,我們需要考慮該運算子如何與已經在 上的運算子(如果有的話)比較優先順序。

假設中綴表示式是一個由空格分隔的標記字串。 操作符標記是 * / +-,以及左右括號,運算元是單字元 A,B,C 等,以下步驟將字尾順序生成一個字串:

  1. 建立一個名為 opstack 的空棧以儲存運算子,給輸出建立一個空列表;
  2. 通過使用字串方法拆分,將輸入的中綴字串轉換為標記列表;
  3. 從左到右掃描標記列表;
    • 如果標記是運算元,將其附加到輸出列表的末尾
    • 如果標記是左括號,將其壓到 opstack
    • 如果標記是右括號,則彈出 opstack,直到刪除相應的左括號。將每個運算子附加到輸出列表的末尾
    • 如果標記是運算子,* / +- ,將其壓入 opstack。但是,首先刪除已經在 opstack 中具有更高或相等優先順序的任何運算子,並將它們加到輸出列表中
  4. 當輸入表示式被完全處理時,檢查 opstack,仍然在 上的任何運算子都可以刪除並加到輸出列表的末尾。

下圖展示了對錶達式 A * B + C * D 的轉換演算法:
在這裡插入圖片描述
注意,第一個 * 在看到 + 運算子時被刪除,另外,當第二個 * 出現時, + 保留在 中,因為乘法優先順序高於加法。在中綴表示式的末尾, 被彈出兩次,刪除兩個運算子,並將 + 作為字尾表示式中的最後一個運算子。

為了在 Python 中編寫演算法,我們使用一個名為 prec 的字典來儲存操作符的優先順序,這個字典將每個運算子對映到一個整數,可以與其他運算子的優先順序(使用整數3,2和1)進行比較。左括號將賦予最低的值,這樣,與其進行比較的任何運算子將具有更高的優先順序,將被放置在它的頂部,第15行將運算元定義為任何大寫字元或數字。

完整的轉換函式如下:

from pythonds.basic.stack import Stack

def infixToPostfix(infixexpr):
    prec = {}
    prec["*"] = 3
    prec["/"] = 3
    prec["+"] = 2
    prec["-"] = 2
    prec["("] = 1
    opStack = Stack()
    postfixList = []
    tokenList = infixexpr.split()

    for token in tokenList:
        if token in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" or token in "0123456789":
            postfixList.append(token)
        elif token == '(':
            opStack.push(token)
        elif token == ')':
            topToken = opStack.pop()
            while topToken != '(':
                postfixList.append(topToken)
                topToken = opStack.pop()
        else:
            while (not opStack.isEmpty()) and \
               (prec[opStack.peek()] >= prec[token]):
                  postfixList.append(opStack.pop())
            opStack.push(token)

    while not opStack.isEmpty():
        postfixList.append(opStack.pop())
    return " ".join(postfixList)

print(infixToPostfix("A * B + C * D"))
print(infixToPostfix("( A + B ) * C - ( D - E ) * ( F + G )"))
print(infixToPostfix("( A + B ) * ( C + D )"))
print(infixToPostfix("( A + B ) * C"))
print(infixToPostfix("A + B * C"))

執行結果如下
在這裡插入圖片描述

8.3、字尾表示式求值

作為最後 的示例,我們考慮對字尾符號中的表示式求值。在這種情況下, 再次是我們選擇的資料結構。但是,在掃描字尾表示式時,它必須等待運算元,而不像上面的轉換演算法中的運算子。 解決問題的另一種方法是,每當在輸入上看到運算子時,計算兩個最近的運算元。

要詳細的瞭解這一點,考慮字尾表示式 4 5 6 * +, 首先遇到運算元 45,此時,你還不確定如何處理它們,直到看到下一個符號時,將它們放置到 上,確保它們在下一個操作符出現時可用。在這種情況下,下一個符號是另一個運算元,所以,像先前一樣,壓入 中並檢查下一個符號。現在我們看到了操作符 *,這意味著需要將兩個最近的運算元相乘,通過彈出 兩次,我們可以得到正確的兩個運算元,然後執行乘法(這種情況下結果為 30)。現在可以通過將其放回棧中來處理此結果,以便它可以表示為表示式後面的運算子的運算元。當處理最後一個操作符時,棧上只有一個值,彈出並返回它作為表示式的結果。

下圖展示了整個示例表示式的棧的內容:
在這裡插入圖片描述
下圖是一個稍微複雜的示例——7 8 + 3 2 + /
在這裡插入圖片描述
在這個例子中有兩點需要注意:

  • 首先, 的大小增長收縮,然後再子表示式求值的時候再次增長。
  • 第二,除法操作需要謹慎處理。

回想下,字尾表示式的操作符順序沒變,僅僅改變操作符的位置。當用於除法的操作符從 中彈出時,它們被反轉,由於除法不是交換運算子,換句話說 15/55/15 不同,因此必須保證運算元的順序不會交換。

假設字尾表示式是一個由空格分隔的標記字串。 運算子為* / +-,運算元假定為單個整數值,輸出將是一個整數結果,步驟如下:

  1. 建立一個名為 operandStack 的空棧;
  2. 拆分字串轉換為標記列表;
  3. 從左到右掃描標記列表;
    • 如果標記是運算元,將其從字串轉換為整數,並將值壓到 operandStack
    • 如果標記是運算子 * / +-,它將需要兩個運算元,故彈出 operandStack 兩次,第一個彈出的是第二個運算元,第二個彈出的是第一個運算元,執行算術運算後,將結果壓到運算元棧中
  4. 當輸入的表示式被完全處理後,結果就在棧上,彈出 operandStack 並返回值。

用於計算字尾表示式的完整函式如下:

from pythonds.basic.stack import Stack

def postfixEval(postfixExpr):
    operandStack = Stack()
    tokenList = postfixExpr.split()

    for token in tokenList:
        if token in "0123456789":
            operandStack.push(int(token))
        else:
            operand2 = operandStack.pop()
            operand1 = operandStack.pop()
            result = doMath(token,operand1,operand2)
            operandStack.push(result)
    return operandStack.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

print(postfixEval('7 8 + 3 2 + /'))

為了輔助計算,定義了一個函式 doMath,它將獲取兩個運算元和運算子,執行相應的計算。

執行結果如下:
在這裡插入圖片描述

九、總結

  • 線性資料結構以有序的方式儲存它們的資料。
  • 棧是維持 LIFO,後進先出,排序的簡單資料結構。
  • 棧的基本操作是 pushpopisEmpty
  • 佇列是維護 FIFO(先進先出)排序的簡單資料結構。
  • 佇列的基本操作是 enqueuedequeueisEmpty
  • 字首,中綴和字尾都是寫表示式的方法。
  • 棧對於設計計算解析表示式演算法非常有用。
  • 棧可以提供反轉特性。

參考文章

  • 《problem solving with algorithms and data structure using python》
  • https://github.com/facert/python-data-structure-cn

相關文章