1. 程式人生 > 實用技巧 >資料結構:四 棧和佇列

資料結構:四 棧和佇列

第四章:棧和佇列

1. 棧的定義

棧(stack):是限定僅在表尾進行插入和刪除操作的線性表

棧頂(top)允許插入和刪除的一端稱為棧頂;另一端稱為棧底(bottom)

空棧:不包含任何資料元素的棧

棧又被稱為後進先出(Last In First Out)的線性表,簡稱 LIFO 結構

棧的插入操作,叫做進棧,也稱壓棧,入棧

棧的刪除操作,叫做出棧,也有的叫作彈棧

2. 棧的抽象資料型別

ADT 棧(stack)

Data
    同線性表。元素具有相同的型別,相鄰元素具有前驅和後堆關係。

Operation
    InitStack ( *S ):初始化操作.建立一個空棧S。
    DestroyStack ( 
*S ):若棧存在,則銷燬它。 ClearStack (*S):將棧清空。 StackEmpty ( S ):若棧為空,返回true,否則返回 false。 GetTop (S,*e):若棧存在且非空,用e返回S的棧頂元素。 Push (*S,e):若棧S存在,插入新元素e到棧S中併成為棧頂元素。 Pop (*S,*e):刪除棧S中棧頂元素,並用e返回其值。 StackLength (S):返回回棧S的元素個數。 endADT

3. 棧的順序儲存結構及實現

定義一個 top 變數來指示棧頂元素在陣列中的位置

進棧操作

  • 棧頂指標加一
  • 將新插入元素賦值給棧頂空間

出棧操作

  • 將要刪除的棧頂元素賦值給 e
  • 棧頂指標減一

進棧和出棧都未涉及迴圈語句,時間複雜度為 O(1)

4. 兩棧共享空間

陣列有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底為陣列的始端,即下標為 0 處,另一個棧為棧的末端,即下標為陣列長度 n-1 處

這樣,兩個棧如果增加元素,就使兩端點向中間延伸

關鍵思路

  • 它們是在陣列的兩端,向中間靠攏
  • top1 和 top2 是棧 1 和棧 2 的棧頂指標

5. 棧的鏈式儲存結構及實現

儲存結構

  • 把棧頂放在單鏈表的頭部,已經有了棧頂在頭部了,單鏈表中常用的頭結點就失去了意義
  • 通常對於鏈棧來說,是不需要頭結點的
  • 對於空棧來說,連結串列原定義是頭指標指向空,那麼鏈棧的空其實就是 top=NULL 的時候

進棧操作

  • 假設元素值為 e 的新結點是 s,top 為棧頂指標
  • 插入元素 e 為新的棧頂元素
  • 把當前的棧頂元素賦值給新結點的直接後繼,如圖中 1
  • 將新的結點 s 賦值給棧頂指標,如圖中 2

出棧操作

  • 假設變數 p 用來儲存刪除的棧頂結點,將棧頂執政下移一位,最後釋放 p 即可
  • 將棧頂結點賦值給 p ,如圖 3
  • 使得棧頂指標下移一位,指向後一節點,如圖 4
  • 釋放結點 p

鏈棧的進棧 push 和出棧 pop,時間複雜度均為 O(1)

順序棧,需先確定一個固定的長度,可能會存在記憶體空間浪費問題;優勢是存取時定位方便

鏈棧,要求每個元素都有指標域,這同時也增加了一些記憶體開銷,但對於棧的長度無限制

選擇

  • 如果棧的使用過程中元素變化不可預料,有時很小,有時非常大,那麼最好是用鏈棧
  • 反之,如果他的變化在可控範圍內,建議使用順序棧會更好一些

6. 棧的作用

棧的引用簡化了程式設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決的問題核心

7. 棧的應用——遞迴

遞迴定義

  • 一個直接呼叫自己或通過一系列的呼叫語句間接地呼叫自己的函式

  • 每個遞迴定義必須至少有一個條件,滿足時遞迴不再進行,即不再引用自身而是返回值退出

  • 迭代和遞迴的區別

    • 迭代使用的時迴圈結構,不需要反覆呼叫函式和佔用額外的記憶體
    • 遞迴能使程式更清晰,更簡潔,更易理解,從而減少讀懂程式碼的時間
    • 大量的遞迴呼叫會建立函式的副本,會耗費大量的時間和記憶體

8. 棧的應用——四則運算表示式求值

字尾(逆波蘭)表示法定義

  • 一種不需要括號的字尾表示法
  • 所有的符號都是在要運算數字的後面出現
  • ”9+(3-1)*3+10/2“ 的字尾表示式:”9 3 1 - 3 * + 10 2 / +“

字尾表示式計算結果

  • 字尾表示式:9 3 1 - 3 * + 10 2 / +

  • 規則

    • 從左到右遍歷表示式的每個數字和符號,遇到數字就進棧;遇到符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果

中綴表示式轉字尾表示式

  • ”9+(3-1)*3+10/2“,平時所用的這種標準四則運算表示式,叫做中綴表示式

  • 中綴轉字尾規則

    • 從左到右遍歷中綴表示式的每個數字和符號,若是數字就輸出,即成為字尾表示式的一部分;
    • 若是符號,則判斷其與棧頂符號的優先順序,是右括號或優先順序低於棧頂符號(乘除優先於加減)則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出字尾表示式為止
  • 中綴轉化為字尾,棧用來進出運算的符號

  • 字尾轉化為中綴,棧用來進出運算的數字

9. 佇列的定義

佇列(queue)是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表

佇列是一種先進先出(First In First Out)的線性表,簡稱FIFO

允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭

10. 佇列的抽象資料型別

ADT 佇列(Queue)

Data
    同線性表。元素具有相同的型別,相鄰元素具有前驅和後繼關係。

Operation
    InitQueue(*Q):初始化操作,建立一個空佇列Q。
    DestroyQueue(*Q):若佇列Q存在,則銷毀它。
    ClearQueue(*Q):將佇列 Q 清空。
    QueueEmpty(Q):若佇列Q為空,送回true,否則退回false。
    GetHead(Q, *e):若佇列Q存在且非空,用e返因佇列Q的隊頭元素。
    EnQueue(*Q,e):若佇列Q存在,插入新元素e到佇列Q中併成為隊尾元素。 
    DeQueue(*Q, *e):刪除佇列Q中隊頭元素,並用e返回其值。    
    QueueLength(Q):送回佇列Q的元素個教。

endADT

11. 迴圈佇列

不足

  • 為了避免當只有一個元素時,隊頭和隊尾重合使處理變得麻煩,所以引入兩個指標
  • front 指標指向隊頭元素,rear 指標指向隊尾元素的下一個位置
  • 當 front 等於 rear 時,此佇列不是還剩一個元素,而是空佇列

定義

  • 我們把佇列的頭尾相接的順序儲存結構稱為迴圈佇列

問題:當 front 等於 rear 時,如何判斷此時佇列是空還是滿?

  • 辦法一

    • 設定一個標誌變數 flag,當 front == rear,且 flag = 0時為佇列空,當 front == rear,且 flag = 1 時為佇列滿
  • 辦法二

    • 當佇列孔時,條件就是 front = rear,當佇列滿時,我們修改其條件,保留一個元素空間。
    • 也就是說,佇列滿時,陣列中還有一個空閒單元
    • 若佇列的最大長度為 QueueSize ,佇列滿的條件是:(reat + 1) % QueueSize == front

12. 佇列的鏈式儲存結構及實現

佇列的鏈式儲存結構,其實就是線性表的單鏈表,只不過它只能尾進頭出而已,簡稱鏈佇列

為了操作上的方便,我們將隊頭指標指向鏈佇列的頭結點

入隊操作

  • 在連結串列尾部插入結點
  • 把擁有元素 e 新結點 s 賦值給原隊尾結點的後繼,如圖 1
  • 把當前的 s 設定為隊尾結點,rear 指向 s,如圖 2

出隊操作

  • 出隊操作時,就是頭結點的後繼結點出隊,將頭結點的後繼改為它後面的結點
  • 若連結串列除頭結點外只剩一個元素時,則需將 rear 指向頭結點