1. 程式人生 > >棧的抽象資料型別(abstract data type,ADT)

棧的抽象資料型別(abstract data type,ADT)

棧(stack)

  棧是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫做棧的頂(top)。對棧的基本操作有push(進棧)和pop(出棧),前者相當於插入,後者則是刪除最後插入的元素。最後插入的元素可以通過使用top例程在執行pop之前進行考察。對空棧進行的pop或top操作一般被認為是棧ADT中的一個錯誤。另一方面,當執行push時空間用盡是一個實現限制,但不是ADT錯誤。

  棧有時又叫做LIFO(後進先出)表。一般的模型是,存在某個元素位於棧頂,而該元素是唯一的可見元素。



棧模型:只有棧頂元素是可訪問的。

棧的實現

  由於棧是一個表,因此任何實現表的方法都可以實現棧。棧操作是常數時間操作。

棧的連結串列實現

  棧的第一種實現方法是使用單鏈表。通過在表的頂端插入來實現push,通過刪除表頂端元素來實現pop。top操作只是考察表頂端元素並返回它的值。有時pop操作和top操作合二為一。

棧的陣列實現

  模仿ArrayList的add操作。與每個棧相關聯的操作是theArray和topOfStack,對於空棧它是-1(這就是空棧初始化的做法)。為將某個元素x推入棧中,我們使topOfStack增1然後置theArray[topOfStack]=x。為了彈出棧元素,我們置返回值為theArray[topOfStack]然後使topOfStack減-1。

  注意,這些操作不僅以常數時間執行,而且是以非常快的常數時間執行。

應用

平衡符號

  例如:{}、[]符號等,有一個“{”符號就需要一個“}”符號與之對應。這個簡單的演算法用到一個棧,敘述如下:

  做一個空棧。讀入字元直到檔案結尾。如果字元是一個開放符號,則將其推入棧中。如果字元是一個封閉符號,則當棧空時報錯。否則,將棧元素彈出。如果彈出的符號不是對應的開放符號,則報錯。在檔案結尾,如果棧非空則報錯。很清楚,它是線性的,事實上它只需對輸入進行一趟檢驗。因此,它是聯機(on-line)的,是相當快的。當報錯時決定如何處理需要做一些附加的工作--例如判斷可能的原因。

字尾表示式

  字尾(postfix)記法或逆波蘭(reverse Polish)記法。計算一個字尾表示式花費的時間是o(N)。注意,當一個表示式以後綴記號給出時,沒有必要知道任何優先的規則,這是一個明顯的優點。

中綴到字尾的轉換

  標準形式的表示式(或叫做中綴表示式(infix))轉換成字尾表示式。

  當讀到一個運算元的時候,立即把它放到輸出中。操作符不立即輸出,從而必須先存放某個地方。正確的做法是將已經見到過但尚未放到輸出中的操作符推入棧中。當遇到左圓括號時我們也要將其推入棧中。計算從一個空棧開始。

  如果見到一個一個右括號,那麼就將棧元素彈出,將彈出的符號寫出直到遇到一個(對應的)左括號,但是這個左括號只被彈出並不輸出。

  如果我們見到任何其它的符號(+,*,(),那麼我們從棧中彈出棧元素直到發現優先順序更低的元素為止。有一個例外:除非是在處理一個)的時候,否則我們絕不從棧中移走(。對於這種操作,+的優先順序最低,而(的優先順序最高。當從棧彈出元素的工作完成後,我們再將操作符壓人棧中。

  最後,如果讀到輸入的末尾,我們將棧元素彈出直到該棧變成空棧,將符號寫到輸出中。

  這個演算法的想法是,當看到一個操作符的時候,把它放到棧中。棧代表掛起的操作符。然而,棧中有些具有高優先順序的操作符現在知道當它們不再被掛起時要完成使用,應該被彈出。這樣,在把當前操作符放入棧中之前,那些在棧中並在當前操作符之前要完成使用的操作符被彈出。

  當左括號是一個輸入符號時我們可以把它看成是一個高優先順序的操作符(使得掛起的操作符仍是掛起的),而當它在棧中時把它看成是低優先順序的操作符(從而不會被操作符意外地刪除)。右括號被處理成特殊的情況。

例子:中綴表示式:a + b * c + ( d * e + f ) * g 轉成字尾表示式:abc * + de * f + g * +


方法呼叫

  當呼叫一個新方法時,主調例程的所有區域性變數需要由系統儲存起來,否則被呼叫的新方法將會重寫由主調例程的變數所使用的記憶體。不僅如此,該主調例程的當前位置也必須要儲存,以便在新方法執行完後知道向哪裡轉移。這些變數一般由編譯器指派給機器的暫存器,但存在某些衝突(通常所有的方法都是獲取指定給1號暫存器的某些變數),特別是涉及到遞迴的時候。該問題類似於平衡符號的原因在於,方法呼叫和方法返回基本上類似於開括號和閉括號。

 當存在方法呼叫的時候,需要儲存的所有重要資訊,諸如暫存器的值(對應變數的名字)和返回地址 (它可從程式計數器得到,一般情況是在一個暫存器中)等, 都要以抽象的方法存在“一張紙上”並被置於一個堆(pile)的頂部。然後控制轉移到新方法,該方法自由地用它的一些值替代這些暫存器。如果它又進行其它的方法呼叫,那麼它也遵循相同的過 程。當該方法要返回時,它檢視堆頂部的那張“紙”並復原所有的暫存器,然後進行返回轉移。

  顯然,所有全部工作均可由一個棧來完成,而這正是在實現遞迴的每一種程式設計語言中實際發生的事實。所儲存的資訊或稱為活動記錄(activation record),或叫做幀棧(stack frame)

  在實際計算機中的棧常常是從記憶體分割槽的高階向下增長,而在許多非Java系統中是不檢測溢位的。失控遞迴可能導致棧溢位。

  尾遞迴(tail recursion):涉及在最後一行的遞迴呼叫。尾遞迴可以通過將程式碼放到一個while迴圈中並用每個方法引數的一次賦值代替遞迴呼叫而被手工清除。

  遞迴總是能夠被徹底去除(編譯器是在轉變成組合語言時完成遞迴去除的),但是這麼做是相當乏味冗長的。一般方法是要求使用一個棧,而且僅當你能夠把最低限度的最小值放到棧上時這個方法才值得一用。