python栈的实现与应用
文章目錄
- 什么是棧?
- 用Python實現棧
- 棧的應用:簡單括號匹配
- 棧的應用:十進制轉換為二進制
- 棧的應用:表達式轉換
什么是棧?
棧有時也被稱作“下推棧”。它是一種有次序的數據項集合,添加操作和移除操作總發生在同一端,即“頂端”,另一端則被稱為“底端”。
棧中的元素離底端越近,代表其在棧中的時間越長,因此棧的底端具有非常重要的意義。最新添加的元素將被最先移除。這種排序原則被稱作LIFO ( last-in first-out ),即后進先出。這是一種基于數據項保存時間的次序,時間越短的離棧頂越近,而時間越長的離棧底越近。最近添加的元素靠近頂端,舊元素則靠近底端。
日常生活中有很多棧的應用,如盤子、托盤、書堆等等。
我們觀察一個由混合的python原生數據對象形成的棧:
進棧和出棧的次序正好相反。
這種訪問次序反轉的特性,我們也在某些計算機操作上碰到過。
比如瀏覽器的“后退back”按鈕,當我們從一個網頁跳轉到另一個網頁時,這些網頁——實際上是 URL—都被存放在一個棧中。當前正在瀏覽的網頁位于棧的頂端,最早瀏覽的網頁則位于底端。如果點擊返回按鈕,便開始反向瀏覽這些網頁。
抽象數據類型“棧”定義為如下的操作:
Stack():創建一個空棧,不包含任何數據項
push(item):將item加入棧頂,無返回值
pop():將棧頂數據項移除,并返回,棧被修改
peek():“窺視”棧頂數據項,返回棧頂的數據項但不移除,棧不被修改
isEmpty():返回棧是否為空棧
size():返回棧中有多少個數據項
抽象數據類型Stack操作樣例:
用Python實現棧
在清楚地定義了抽象數據類型Stack之后,我們看看如何用Python來實現它。
Python的面向對象機制可以用來實現用戶自定義類型,將ADT Stack實現為Python的一個Class,將ADT Stack的操作實現為Class的方法,由于Stack是一個數據集,所以可以采用Python的原生數據集來實現,我們選用最常用的數據集List來實現。
Python列表是有序集合,它提供了一整套方法。舉例來說,對于列表[2,5,3,6,7,4] ,只需要考慮將它的哪一邊視為棧的頂端。一旦確定了頂端,所有的操作就可以利用 append 和pop等列表方法來實現。
可以將List的任意一端(index=0或者-1)設置為棧頂,我們這里選用List的末端(index=-1)作為棧頂,這樣棧的操作就可以通過對list的append和pop來實現:
用Python實現ADT Stack程序示例:
需要注意的是如果我們把List的另一端(首端index=0 )作為Stack的棧頂,同樣也可以實現Stack:
改變抽象數據類型的實現卻保留其邏輯特征,這種能力體現了抽象思想。不過,盡管上述兩種實現都可行,但是二者在性能方面肯定有差異。append方法和 pop( )方法的時間復雜度都是o(1),這意味著不論棧中有多少個元素,棧頂尾端的實現(右)中的push操作和 pop操作都會在恒定的時間內完成。棧頂首端的版本(左)實現的性能則受制于棧中的元素個數,這是因為insert (0)和 pop(0)的時間復雜度都是O(n),元素越多就越慢。顯而易見,盡管兩種實現在邏輯上是相等的,但是它們在進行基準測試時耗費的時間會有很大的差異。
棧的應用:簡單括號匹配
我們都寫過這樣的表達式:
(5+6)?(7+8)/(4+3)(5+6)?(7+8)/(4+3)(5+6)?(7+8)/(4+3)
這里的括號是用來指定表達式項的計算優先級,匹配括號是指每一個左括號都有與之對應的一個右括號,并且括號對有正確的嵌套關系。
對括號是否正確匹配的識別,是很多語言編譯器的基礎算法。
下面看看如何構造括號匹配識別算法:
從左到右掃描括號串,最新打開的左括號,應該匹配最先遇到的右括號,這樣,第一個左括號(最早打開),就應該匹配最后一個右括號(最后遇到)這種次序反轉的識別,正好符合棧的特性!
一旦認識到用棧來保存括號是合理的,算法編寫起來就會十分容易。由一個空棧開始,從左往右依次處理括號。如果遇到左括號,便通過push操作將其加入棧中,以此表示稍后需要有一個與之匹配的右括號。反之,如果遇到右括號,就調用pop操作。只要棧中的所有左括號都能遇到與之匹配的右括號,那么整個括號串就是匹配的;如果棧中有任何一個左括號找不到與之匹配的右括號,則括號串就是不匹配的。在處理完匹配的括號串之后,棧應該是空的。代碼清單3-3展示了實現這一算法的 Python代碼。
parChecker函數假設stack類可用,并且會返回一個布爾值來表示括號串是否匹配。注意,布爾型變量balanced 的初始值是True,這是因為一開始沒有任何理由假設其為False,如果當前的符號是左括號,它就會被壓入棧中(第9-10行)。注意第15行,僅通過pop()將一個元素從棧中移除。由于移除的元素一定是之前遇到的左括號,因此并沒有用到pop()的返回值。在第19~22行,只要所有括號匹配并且棧為空,函數就會返回True,否則返回False。
在實際的應用里,我們會碰到更多種括號,如python中列表所用的方括號“[]”
字典所用的花括號“{}”,元組和表達式所用的圓括號“()” ,這些不同的括號有可能混合在一起使用, 因此就要注意各自的開閉匹配情況。
棧的應用:十進制轉換為二進制
二進制是計算機原理中最基本的概念,作為組成計算機最基本部件的邏輯門電路,其輸入和輸出均僅為兩種狀態:0和1 。
但十進制是人類傳統文化中最基本的數值概念,如果沒有進制之間的轉換,人們跟計算機的交互會相當的困難。
所謂的“進制”,就是用多少個字符來表示整數。
十進制是0~9這十個數字字符,二進制是0、1兩個字符。
我們經常需要將整數在二進制和十進制之
間轉換
如:(233)10(233)_{10}(233)10?的對應二進制數為(11101001)2(11101001)_2(11101001)2?,
具體是這樣:
(233)10=2×102+3×101+3×100(233)_{10}=2×10^2+3×10^1+3×10^0(233)10?=2×102+3×101+3×100
(11101001)2=1×27+1×26+1×25+0×24+1×23+0×22+0×21+1×20(11101001)_2=1×2^7+1×2^6+1×2^5+0×2^4+1×2^3 +0×2^2+0×2^1+1×2^0(11101001)2?=1×27+1×26+1×25+0×24+1×23+0×22+0×21+1×20
十進制轉換為二進制,采用的是除以2求余數的算法,將整數不斷除以2,每次得到的余數就是由低到高的二進制位。
“除以2”算法假設待處理的整數大于0。它用一個簡單的循環不停地將十進制數除以2,并且記錄余數。第一次除以2的結果能夠用于區分偶數和奇數。如果是偶數,則余數為0,因此個位上的數字為0;如果是奇數,則余數為1,因此個位上的數字為1。可以將要構建的二進制數看成一系列數字;計算出的第一個余數是最后一位。如圖3-5所示,這又一次體現了反轉特性,因此用棧來解決該問題是合理的。
十進制轉換為二進制代碼實現:
十進制轉換為二進制的算法,很容易可以擴展為轉換到任意N進制,只需要將除以2求余數算法改為“除以N求余數”算法就可以。
二進制有兩個不同數字0、1
十進制有十個不同數字0、1、2、3、4、5、6、 7、8、9
八進制可用八個不同數字0、1、2、3、4、5、6 、7
十六進制的十六個不同數字則是0、1、2、3、4 、5、6、7、8、9、A、B、C、D、E、F
十進制轉換為十六以下任意進制代碼示例:
為了實現這一方法,第3行創建了一個數字字符串來存儲對應位置上的數字。0在位置0,1在位置1,A在位置10,B在位置11,依此類推。當從棧中移除一個余數時,它可以被用作訪問數字的下標,對應的數字會被添加到結果中。如果從棧中移除的余數是13,那么字母D將被添加到結果字符串的最后。
棧的應用:表達式轉換
我們通常看到的表達式像這樣:B?CB*CB?C,很容易知道這是B乘以C ,這種操作符介于操作數中間的表示法,稱為中綴表示法。但有時候中綴表示法會引起混淆,如 “A+B*C” 是A+B然后再乘以C,還是B?CB*CB?C然后再去加A?
人們引入了操作符優先級的概念來消除混淆,規定高優先級的操作符先計算
相同優先級的操作符從左到右依次計算,這樣A+B?CA+B*CA+B?C就沒有疑義是A加上B與C的乘積,同時引入了括號來表示強制優先級,括號的優先級最高,而且在嵌套的括號中,內層的優先級更高,這樣(A+B)*C就是A與B的和再乘以C。
雖然人們已經習慣了這種表示法,但計算機處理最好是能明確規定所有的計算順序,這樣無需處理復雜的優先規則。
引入全括號表達式:在所有的表達式項兩邊都加上括號,
A+B?C+DA+B*C+DA+B?C+D,應表示為((A+(B?C))+D)((A+(B*C))+D)((A+(B?C))+D)
可否將表達式中操作符的位置稍移動一下?例如中綴表達式A+B,將操作符移到前面,變為“+AB”,或者將操作符移到最后,變為“AB+” 。
我們就得到了表達式的另外兩種表示法:
“前綴”和“后綴”表示法,以操作符相對于操作數的位置來定義。
神奇的事情發生了,在中綴表達式里必須的括號,在前綴和后綴表達式中消失了? 在前綴和后綴表達式中,操作符的次序完全決定了運算的次序,不再有混淆,所以在很多情況下,表達式的計算機表示都避免用復雜的中綴形式。
下面看更多的例子:
目前為止我們僅手工轉換了幾個中綴表達式到前綴和后綴的形式,一定得有個算法來轉換任意復雜的表達式。
為了分解算法的復雜度,我們從“全括號”中綴表達式入手,
我們看A+B*C,如果寫成全括號形式:
(A+(B?C))(A+(B*C))(A+(B?C)),顯式表達了計算次序
我們注意到每一對括號,都包含了一組完整的操作符和操作數。
看子表達式(B?C)(B*C)(B?C)的右括號,如果把操作符?*?移到右括號的位置,替代它,再刪去左括號,得到BC?BC*BC?,這個正好把子表達式轉換為后綴形式,進一步再把更多的操作符移動到相應的右括號處替代之,再刪去左括號,那么整個表達式就完成了到后綴表達式的轉換。
同樣的,如果我們把操作符移動到左括號的位置替代之,然后刪掉所有的右括號,也就得到了前綴表達式:
所以說,無論表達式多復雜,需要轉換成前綴或者后綴,只需要兩個步驟:
我們來討論下通用的中綴轉后綴算法,首先我們來看中綴表達式A+B?CA+B*CA+B?C,其對應的后綴表達式是ABC?+ABC*+ABC?+,操作數ABC的順序沒有改變。
操作符的出現順序,在后綴表達式中反轉了,由于*的優先級比+高,所以后綴表達式中操作符的出現順序與運算次序一致。
在中綴表達式轉換為后綴形式的處理過程中,操作符比操作數要晚輸出,所以在掃描到對應的第二個操作數之前,需要把操作符先保存起來,而這些暫存的操作符,由于優先級的規則,還有可能要反轉次序輸出。 在A+B?CA+B*CA+B?C中,+雖然先出現,但優先級比后面這個?*?要低,所以它要等*處理完后,才能再處理。
這種反轉特性,使得我們考慮用棧來保存暫時未處理的操作符。
再看看(A+B)?C(A+B)*C(A+B)?C,對應的后綴形式是AB+C?AB+C*AB+C?
這里+的輸出比?*?要早,主要是因為括號使得+的優先級提升,高于括號之外的?*? ,后綴表達式中操作符應該出現在左括號對應的右括
號位置,所以遇到左括號,要標記下,其后出現的操作符。優先級提升了,一旦掃描到對應的右括號,就可以馬上輸出這個操作符。
總結下,在從左到右掃描逐個字符掃描中綴表達式的過程中,采用一個棧來暫存未處理的操作符,這樣,棧頂的操作符就是最近暫存進去的,當遇到一個新的操作符,就需要跟棧頂的操作符比較下優先級,再行處理。
中綴表達式單詞列表掃描結束后,把opstack棧中的所有剩余操作符依次彈出
,添加到輸出列表末尾,把輸出列表再用join方法合并成后綴表達式字符串,算法結束。
Python實現從中序表達式到后序表達式的轉換:
以上成功實現了從中序表達式到后序表達式,下面我們試著計算后綴表達式,跟中綴轉換為后綴問題不同, 在對后綴表達式從左到右掃描的過程中, 由于操作符在操作數的后面, 所以要暫存操作數,在碰到操作符的時候
,再將暫存的兩個操作數進行實際的計算。仍然是棧的特性:操作符只作用于離它最近的兩個操作數。
如“4 5 6 * +”,我們先掃描到4、5兩個操作數,但還不知道對這兩個操作數能做什么計算,需要繼續掃描后面的符號才能知道,繼續掃描,又碰到操作數6 ,還是不能知道如何計算,繼續暫存入棧直到*,現在知道是棧頂兩個操作數5 、6做乘法。我們彈出兩個操作數,計算得到結果30。
需要注意:
先彈出的是右操作數,后彈出的是左操作數,這個對于-/很重要!
為了繼續后續的計算,需要把這個中間結果30壓入棧頂,繼續掃描后面的符號,當所有操作符都處理完畢,棧中只留下1個操作數,就是表達式的值。
后綴表達式求值流程:
(1) 創建空棧operandstack。
(2) 使用字符串方法 split將輸入的后序表達式轉換成一個列表。
(3) 從左往右掃描這個標記列表。如果標記是操作數,將其轉換成整數并且壓入operandstack棧中。如果標記是運算符,從operandstack棧中取出兩個操作數。第一次取出右操作數,第二次取出左操作數。進行相應的算術運算,然后將運算結果壓入 operandstack棧中。
(4) 當處理完輸入表達式時,棧中的值就是結果。將其從棧中返回。
計算后綴表達式程序實現:
總結
以上是生活随笔為你收集整理的python栈的实现与应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web前端几个小知识点笔记
- 下一篇: 蓝桥杯 Python 杨辉三角形