資料結構(c++)(2)--棧的應用
接著上一篇部落格中的棧,這次介紹下棧的一些應用。
在看到的棧的這個特性後進先出的性質時,第一感覺就是這樣做有什麼用呢?把一個表的操作限制成這個樣子,不是在削減嗎?然而,在實際的應用中,這些存在於棧中的少數的操作卻是非常的強大和重要。下面給出三個棧的應用 :
一、平衡符號
在我們平時的程式設計過程中,常常由於缺少一個括號(如遺漏一個花括號或是註釋齊起始符)導致編譯過程中編譯器列出上百行的錯誤,而真正的錯誤並沒有找出來。當然,由於編譯器軟體的迅猛發展,現在很多編譯器都會用紅色的波浪線提示我們這些丟失的東西。
那編譯器是如何做到幫助我們檢查這個錯誤的呢?我們先來思考一下,在我們平時的程式設計中,每一個花括號、右括號及右圓括號必然對應其相應的左半部分。[()]是合法的,但[(])就是錯誤的了。顯然,不值得為此編寫一個大型程式,但這說明了這樣的檢驗是很容易實現的。為了簡單起見,我們僅就圓括號、方括號和花括號進行檢驗並忽略出現的任何其他的字元。
簡單的演算法描述如下:
(1)在演算法的開始,我們準備一個空的棧。
(2)讀取字元直至到達檔案的末尾。
(3)如果讀入的字元是一個開放符號,則將此字元壓入棧中;
(4)如果讀入的字元m是一個封閉符號,那麼檢視棧是否為空。如果棧為空,則提示錯誤;如果棧不為空,則將棧頂的元素彈出。如果此棧頂元素和m不是相對應的,則提示錯誤。
(5)在檔案讀完的時候,如果棧是非空的,則提示錯誤。
可以確信,這個演算法是可以正確執行的。很清楚,演算法是線性的,事實上它只需要對輸入進行一次檢查。因此,它是聯機的並且相當的快。並且,我們可以做一些附加的工作來決定當檢測出錯誤的時候應該如何進行處理。
二、字尾表示式
其實,在用到棧的時候,讓我感覺最有用的就是這個棧處理字尾表示式的能力了。在我們平時進行數學公式中簡單的加、減、乘、除的時候,對於我們人腦而言,這方面的數學運算是很簡單的,因為各種規則我們可以處理的很好。但是,當我們用程式設計處理數學計算的時候,那麼,我們就要想了,如何在程式中處理加、減、乘、除及帶括號的這些情況呢?在沒有看到字尾表示式的時候,說實話,我是懵逼的,因為數學表示式的長度是可以無限的,那麼如果進行列舉之類的方法的時候,我想這是會讓人崩潰的,即便用迴圈處理,我想也是很鬧大的,至少我是沒有理過來,知道看到字尾表示式,我一下子就頓悟了。
下面我們來具體看看關於字尾表示式的東西。
舉個簡單的例子,在計算12*3-4*3的時候,我們人腦的計算過程是先計算出12*3的值放入a,然後計算4*3的值放入b,最後我們會計算a-b。對於這種操作的順序,我們可以按如下的順序書寫:
12 3 * 4 3 * +
這種記法叫做字尾或者逆波蘭記法,其求值的過程恰好就是上面所述我們人腦的計算過程。計算這個問題最容易使用的就是使用棧,這也正是我們的重頭戲。這個方法的執行過程很簡單:
(1)當遇到一個數時,將這個數壓入棧中。
(2)當遇到一個操作符的時候,則從棧中彈出兩個數,然後進行相應的運算,並將運算的結果壓入棧中。
(3)持續上述的兩個操作直至處理結束,最後棧中剩餘的數字即為結果。
例如,我們計算這個字尾表示式:6 5 2 3 + 8 * + 3 + *,這個表示式的正常表示式為6*(5+(2+3)*8+3)。處理過程如下:
(1)準備一個空的棧,如下:
(2)將前4個數字放入棧中,此時棧的情況如下:
(3)下面讀到了一個“+”號,所以從棧中彈出3和2,並且進行加法運算,將結果5壓入棧中,如下圖所示:
(4)接著,將數字8壓入棧中,如下:
(5)接著讀到的是“*”號,因此將棧頂的8和5彈出,並且計算5*8=40(這裡請注意,我們是將5作為第一個操作的數的,這個在減法和除法裡尤為重要,運算元的順序要仔細),並將40壓入棧中,如下所示:
(6)接下來督導的是一個“+”號,此時將棧頂的40和5彈出,並且計算5+40=45,將45壓入棧中,如下所示:
(7)將讀入的3壓入棧中,如下所示:
(8)接著讀到了“+”號,將棧頂的3和45彈出,並計算45+3=48,並將48壓入棧中,如下所示:
(9)最後,讀到一個“*”號,將棧頂的48和6彈出,並計算6*48=288,並將288壓入棧中,如下所示:
在上述的過程中,我們可以看出,計算一個字尾表示式所花費的時間是O(N),因為對輸入中的每個元素的處理都是由一些棧操作組成,從而花費常數時間。在上述執行的過程中,我們可以發現,在計算字尾表示式的時候,我們不需要關注運算子中的有限規則,這是一個很明顯的有點,它讓我們的計算變得非常的簡單了。
在看到字尾表示式這麼簡單便能處理數學計算的時候,是不是很心動了,但是是不是又會糾結於如何得到這個字尾表示式呢?那麼下面我們就不做歇息了,直奔下一個站點。
三、中綴表示式到字尾表示式的轉換
在見識了棧對於字尾表示式後,我想說的是,如何得到字尾表示式棧也是可以幫助我們實現的,這簡直就是一條龍服務嘛,超級棒!
對於一個標準形式的表示式(或叫做中綴表示式),我們從簡單的方面入手,這裡我們暫時只處理操作符“+”、“-”、“*”、“\”、“(”、“)”,並保持我們平時數學計算中的優先規則。
此演算法的過程如下:
1、準備一個空的棧,用於存放操作符。
2、當讀到一個運算元的時候,立即將它放到輸出中。
3、當讀到操作符的時候,我們不會立即將它輸出,而是將操作符放入到棧中,這個放入操作也是有一定的要求的:
(1)如果見到一個右括號,那麼將棧中的元素彈出,將彈出的操作符放到輸入中直到遇到一個(對應的)左括號,但是這個左括號只被彈出,並不放到輸出中。
(2)如果遇到任何其他的操作符(如“+”、“-”、“*”、“\”、“(”),那麼從棧中彈出棧元素直到發現優先順序更低的元素為止。有一個例外:除非是在處理“)”的時候,否則決不從棧中移走“(”。對於這種操作,“+”、“-”的優先順序最低,而“(”的優先順序最高。當從棧中彈出元素的工作完成後,將讀到的操作符壓入棧中。
4、如果讀到輸入的末尾了,那麼將棧中的元素彈出直到棧變為空,並將這些操作符放到輸出中。
對於上述的規則,剛看到的時候可能有點雲裡霧裡的,這個沒關係,我們也沒有必要去咬字眼,實踐出真知嘛,那麼下面我們在例子中來熟悉這些規則。
例如,對於一箇中綴表示式:a+b*c+(d*e+f)*g,下面我們來將它處理成字尾表示式:
(1)首先,讀入的是運算元a,將它送到輸出中,如下:
(2)然後將“+”讀入並壓入到棧中,如下所示:
(3)將b讀入並送到輸出中,如下所示:
(4)將“*”號讀入,此時我們來看下規則,操作符棧中棧頂的元素為“+”,顯然比“*”號的優先順序低,故此時“+”不需要彈出,我們將“*”號壓入棧中,如下所示:
(5)將運算元c讀入,並放到輸出中,如下所示:
(6)讀入操作符“+”號,此時我們又要看一下規則了,檢查棧頂可以發現,棧頂的操作符為“*”號,顯然優先順序比“+”號高,則我們需要將棧頂的“*”彈出並放入到輸出中;繼續檢查棧頂,發現棧頂的元素為“+”號,顯然優先順序一樣,而規則中是要求我們彈出棧頂元素知道發現優先順序更低的元素,那麼此時我們是可以將棧頂的“+”號彈出的,並且將它放到輸出中,最後將讀入的“+”號壓入棧中,如下所示:
(7)將操作符“(”讀入,由於它具有最高的優先順序且具有特殊性,故直接將它放入到棧中,如下所示:
(8)將運算元d讀入並放到輸出中,如下所示:
(9)將操作符“*”讀入,看下規則可以發現,由於除非正在處理的操作符是右括號,否則左括號是不會從棧中彈出的,因此沒有輸出,直接將“*”壓入棧中,如下所示:
(10)將運算元e讀入並放入到輸出中,如下所示:
(11)將操作符“+”號讀入,這個時候根據規則,我們檢查棧頂的元素,棧頂元素為“*”號,優先順序比“+”號高,則將“*”彈出並放入到輸出中,然後繼續檢查棧頂元素,此時棧頂元素為“(”,因為此符號只有在處理右括號的時候才會彈出,故此時可以將讀入的“+”好壓入棧中了,如下所示:
(12)將運算元f讀入,並放入到輸出中,如下所示:
(13)將操作符“)”讀入,由規則可知,此時可以彈出棧頂的元素知道“(”被彈出,因此可以輸出一個“+”號,如下所示:
(14)將操作符“*”讀入,由規則可知,棧頂元素“+”號優先順序比“*”低,直接將“*”號壓入棧中,如下所示:
(15)將運算元f讀入,並放入到輸出中,如下所示:
(16)讀入為空了,因此我們將棧中的所有操作符彈出並放到輸出中,如下所示:
與前面相同,這種轉換隻需要O(N)的時間並可以通過一次輸入便可以完成工作。
好了,這次就只寫這些吧,寫太多了,有點累了,關於程式設計方面的具體實現就留待下一篇部落格再和大家分享吧!