棧(stack)在電腦科學中是限定僅在表尾進行插入或刪除操作的線性表。
棧是一種資料結構,它按照後進先出的原則儲存資料,先進入的資料被壓入棧底,最後的資料在棧頂,需要讀資料的時候從棧頂開始彈出資料。
在各種語言環境下,比如C++,JAVA或者C#中,對於棧都會提供兩個基本的函數,即push和pop函數,分別用於向棧中壓入對象和從棧中彈出對象。
棧的實現可以有兩種方式,如下所示:
(1)鏈表實現。該方法使用鏈表格儲存體資料,壓棧的操作相當於在鏈表的開頭插入一個新的節點,出棧的操作相當於在鏈表的開頭刪除開始的節點。
(2)數組實現。棧的另外一種實現方式就是使用數組,也是最常用的實現方式。我們使用一個數組對象來儲存資料,並定義一個變數指示棧頂的位置。
我們知道,在JAVA中,數組對象時在初始化之後是不能改變大小的,但是對於棧來說,其大小應該能夠根據實際情況自行變動的,這就需要我們在棧的實現過程中自己判斷
數組是否已經裝滿,並自行擴充堆棧的大小。在JDK中,我們可以看到棧的實現是通過數組實現的,在Stack.java檔案中,我們可以看到
class Stack<E> extends Vector<E>
在上面,我們看到Stack繼承Vector,而Vector中的資料的實現是通過數組來儲存的。
棧的應用有很多,下面講述兩個個經典的棧的應用。
(1)符號匹配
大家都知道,編譯器在編譯器時都會為我們檢查文法的錯誤。其中一項主要的內容就是檢查符號是否是對應出現的。如[{}]就是一個符合文法的,而{[}]是不符合文法的。
使用棧實現對於檔案的符號匹配的檢測演算法可以描述如下:
首先,建立一個空的棧,然後從檔案中讀取字元,如果符號是一個開符號,如“{”,“[”和“(”,則將該符號壓入棧,如果是閉符號,此時如果棧是空的,則報告一個錯誤,
如果不是空的,則從棧中彈出一個開字元,如果彈出的開字元是與原閉字元相對應,則繼續從檔案中讀取字元,如果不相對應,則報告一個錯誤。當讀到檔案的末尾時,檢查棧
是不是空的,如果是空的,則沒有語法錯誤,如果不是空的,則報告語法錯誤。
(2)尾碼運算式
當我們計算一個數學計算運算式時,包含+,-,*,/,(,)和0-9十個數字,首先使用棧將數學計算運算式轉換為尾碼運算式,然後使用棧計算尾碼運算式。
首先我們說一下如何將計算運算式轉換為尾碼運算式。在轉換的過程中需要考慮到運算子的優先順序,下面列出如下:
| 優先順序 |
運算子 |
| 1 |
括弧() |
| 2 |
負號- |
| 3 |
乘方** |
| 4 |
乘*,除/,求餘% |
| 5 |
加+,減- |
| 6 |
小於<,小於等於<=,大於>,大於等於>= |
| 7 |
等於==,不等於!= |
| 8 |
邏輯與&& |
| 9 |
邏輯或|| |
利用棧解析算數運算式的步驟如下:
(1)從左至右依次取得一個字元ch
(2)如果ch是一個字元,則直接輸出該字元
(3)如果ch是一個運算子,則:
a.如果ch=‘(',放入棧中;
b.如果ch=‘)',依次輸出棧中的運算子,直到遇到')';
c.如果ch不是'('或者')',那麼需要將ch和棧頂的字元top進行優先順序的比較,如果ch的優先順序比top的優先順序高,那麼將ch壓入棧中,如果ch的優先順序低於或者等於top的
優先順序,那麼輸出top,並將ch壓入棧中。
(4)如果運算式已經讀取完成,而棧中還有運算子,則將運算子從棧中依次輸出。
如果我們有運算式(A-B)*C+D-E/F,要翻譯成尾碼運算式,並且把尾碼運算式儲存在一個名叫output的字串中,可以用下面的步驟。
(1)讀取'(',壓入堆棧,output為空白
(2)讀取A,是運算數,直接輸出到output字串,output = A
(3)讀取'-',此時棧裡面只有一個'(',因此將'-'壓入棧,output = A
(4)讀取B,是運算數,直接輸出到output字串,output = AB
(5)讀取')',這時候依次輸出棧裡面的運算子'-',然後就是'(',直接彈出,output = AB-
(6)讀取'*',是運算子,由於此時棧為空白,因此直接壓入棧,output = AB-
(7)讀取C,是運算數,直接輸出到output字串,output = AB-C
(8)讀取'+',是運算子,它的優先順序比'*'低,那麼彈出'*',壓入'+",output = AB-C*
(9)讀取D,是運算數,直接輸出到output字串,output = AB-C*D
(10)讀取'-',是運算子,和'+'的優先順序一樣,因此彈出'+',然後壓入'-',output = AB-C*D+
(11)讀取E,是運算數,直接輸出到output字串,output = AB-C*D+E
(12)讀取'/',是運算子,比'-'的優先順序高,因此壓入棧,output = AB-C*D+E
(13)讀取F,是運算數,直接輸出到output字串,output = AB-C*D+EF
(14)原始字串已經讀取完畢,將棧裡面剩餘的運算子依次彈出,output = AB-C*D+EF/-
通過上面的演算法之後我們可以得到一個運算運算式相對應的尾碼運算式,接下來就是對尾碼運算式進行解析,算得算數計算式的值。
解析過程如下所示:
(1)從左至右掃描運算式,一次取出一個資料ch;
(2)如果ch是運算元,則壓入棧中;
(3)如果ch是操作符,就從棧中彈出操作符所需要的個數的運算元進行操作,並將運算結果壓入棧中;
(4)如果資料處理完畢,則棧中最終剩下的資料就是最終運算的結果。
比如我們要處理一個尾碼運算式1234+*+65/-,那麼具體的步驟如下。
(1)首先1,2,3,4都是運算元,將它們都壓入堆棧
(2)取得'+',為運算子,彈出資料3,4,得到結果7,然後將7壓入堆棧
(3)取得'*',為運算子,彈出資料7,2,得到資料14,然後將14壓入堆棧
(4)取得'+',為運算子,彈出資料14,1,得到結果15,然後將15壓入堆棧
(5)6,5都是資料,都壓入堆棧
(6)取得'/',為運算子,彈出資料6,5,得到結果1.2,然後將1.2壓入堆棧
(7)取得'-',為運算子,彈出資料15,1.2,得到資料13.8,這就是最後的運算結果
執行個體代碼:
public class PosfixParser
{
private static string expression = "1+4/(1+1)+2*(3+4)-6/3+5/(1/2+2/1)";
private static Stack myStack = new Stack();
private static StringBuilder posfixExpression = new StringBuilder();
public static void Main()
{
Console.WriteLine("This Midfix expression is: {0}", expression);
Console.WriteLine("The Posfix expression is: {0}", Parse());
Console.WriteLine("The result is {0}", Calculate());
Console.Read();
}
//將中綴運算式解析成尾碼運算式
public static string Parse()
{
int i, j = 0;
char ch, ch1;
char[] A = expression.ToCharArray(); //將字串轉成字元數組,要注意的是,不能有大於10的數存在
char[] B = new char[A.Length]; //最後產生的尾碼運算式會小於這個長度,因為有括弧
int length = A.Length;
for(i=0; i<length; i++)
{
ch = A[i];
if( IsOperand( ch ) ) //如果是運算元,直接放入B中
{
B[j++] = ch;
}
else
{
if( ch == '(' ) //如果是'(',將它放入堆棧中
myStack.Push(ch);
else if( ch == ')') //如果是')'
{
while( !IsEmpty(myStack) ) //不停地彈出堆棧中的內容,直到遇到'('
{
ch = (char)myStack.Pop();
if( ch == '(' )