棧的定義
棧是限定僅在表尾進行插入和刪除操作的線性表
我們把允許插入和刪除的一端稱為棧頂 (top) ,另一端稱為棧底 (bottom) ,不含任何資料元素的棧稱為空棧。 棧又稱為後進先出 (Last In Filrst Out) 的線性表,簡稱LIFO結構。
理解棧的定義需要注意:
首先它是一個線性表,也就是說,棧元素具有線性關係,即前驅後繼關係。只不過它是一種特殊的線性表而已。定義中說是線上性表的表尾進行插入和刪除操作,這裡表尾是指棧頂,而不是棧底。它的特殊之處就在於限制了這個線性表的插入和刪除位置,它始終只在棧頂進行。這就使得:棧底是固定的,最先進棧的只能在棧底。
棧的插入操作,叫做進棧,也稱壓棧、入棧。類似子彈入彈夾,如圖4-2-2所示。
棧的刪除操作,叫做出棧,也有叫做彈棧。如同彈夾中的子彈出夾,如圖4-2-3所示。
進棧出棧變化形式
現在我要問問大家,這個最先進棧的元素,是不是就只能是最後出戰呢?
答案是不一定,要看什麼情況。棧對線性表的插入和刪除的位置進行了限制 ,並沒有對元素進出的時間進行限制,也就是說,在不是所有元素都進棧的情況下,事先進去的元素也可以出棧,只要保證是棧頂元素出棧就可以。
舉例來說,如果我們現在是有 3 個整型數字元素 1、 2、 3 依次進棧,會有哪些出棧次序呢?
有沒有可能是 312 這樣的次序出棧呢?答案是肯定不會。因為 3 先出棧,就意味著,3曾經進棧,既然 3 都進棧了,那也就意味著, 1 和 2 已經進棧了,此時, 2 一 定是在 1 的上面,就是更接近棧頂,那麼出棧只可能是 321,不然不滿足 123 依次進棧的要求,所以此時不會發生1比2先出棧的情況。
從這個簡單的例子就能看出,只是 3 個元素,就有 5 種可能的出棧次序,如果元素數量多,其實出棧的變化將會更多的。這個知識點一定要弄明白。
棧的抽象資料型別
對於棧來講,理論上線性表的操作特性它都具備,可由於它的特殊性,所以針對它在操作上會有些變化。特別是插入和刪除操作,我們改名為push 和pop,英文直譯的話是壓和彈,更容易理解。你就把它當成是彈夾的子彈壓入和彈出就好記憶了,我 們一般叫進棧和出棧。
由於棧本身就是一個線性表,那麼上一章我們討論了線性表的順序儲存和鏈式儲存,對於棧來說,也是同樣適用的。
棧的順序儲存結構及實現
棧的順序儲存結構
棧的順序儲存其實也是線性表順序儲存的簡化,我們簡稱為順序棧。線性表是用陣列來實現的,用下標0的一端作為棧底。因為首元素都在棧底,變化最小,所以讓它作棧底。
定義一個top變數來指示棧頂元素在陣列中的位置。儲存棧的長度胃StackSize,則棧頂位置top必須小於StackSize。當棧存在一個元素時,top等於0,因此通常把空棧的判斷條件定為top=-1。
若現在有一個棧,StackSize是5,則棧普通情況、空棧和棧滿的情況示意圖如圖4-4-2所示。
棧的順序儲存結構一一進棧操作
棧的順序儲存結構一一出棧操作
兩者沒有涉及到任何迴圈語句,因此時間複雜度O(1)。
兩棧共享空間
棧的順序儲存是很方便,因為它只准棧頂進出元素,所以不存線上性表插入和刪除時需要移動元素的問題。不過它有一個很大的缺陷,就是必須實現確定陣列儲存空間大小,萬一不夠用了,就需要編碼手段來擴充套件陣列的容量,非常麻煩。對於一個棧,我們也只能儘量考慮周全,設計出合適大小的陣列來處理,但對於兩個相同型別的棧,我們卻可以做到最大限度地利用其事先開闢的儲存空間來進行操作。
如果我們有兩個相同型別的棧,我們為它們各自開闢了陣列空間,極有可能是第一個棧已經滿了,再進棧就溢位了,而另一個棧還有很多儲存空間空閒。這又何必呢?我們完全可以用一個陣列來儲存兩個棧,只不過需要一點技巧。
我們的做法如圖4-5-1,陣列有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底為陣列的始端,即下標為0處,另一個棧為棧的末端,即下標為陣列長度n-1處。這樣,兩個棧如果增加元素,就是兩端點向中間延伸。
關鍵思路是:它們是在陣列的兩端,向中間靠攏。top1和top2是棧1和棧2的棧頂指標,可以想象,只要它們倆不見面,兩個棧就可以一直使用。
從這裡也可以分析出,棧1為空時,就是top1=-1時;而當top2=n時,即是棧2為空時,那什麼時候棧滿呢?
想想極端的情況,若棧2是空棧,棧1的 top1 等於 n-1 時,就是棧1滿了。 反之,當棧1為空棧時, top2等於0時,為棧2滿。但更多的情況,其實就是我剛才說的,兩個棧見面之時,也就是兩個指標之間相差 1 時,即top + 1 == top2為棧滿。
對於兩棧共享空間的 push 方法,我們除了要插入元素值引數外,還需要有一個判斷是棧1還是棧2的棧號引數 stackNumber。
使用這樣的資料結構,通常都是當兩個棧的空間需求有相反關係時,也就是一個棧增長時另一個棧在縮短的情況。這樣使用兩棧共享儲存方法才有比較大的意義。否則兩個棧都在不停地增長,那很快就會因棧滿而溢位。
當然這隻針對兩個具有相同資料型別的棧的一個設計上的技巧,如果是不相同資料型別的棧,這種辦法不但不能更好地處理問題,反而會使問題變得更復雜,要注意這個前提。
棧的鏈式儲存結構及實現
棧的鏈式儲存結構
棧的鏈式儲存結構,簡稱為鏈棧。
想想看,棧只是棧頂來做插入和刪除操作,棧頂放在連結串列的頭部還是尾部呢?由於單連結串列有頭指標,而棧頂指標也是必須的,那幹嗎不讓它倆合二為一呢,所以比較好的辦法是把棧頂放在單連結串列的頭部(如圖 4-6-1 所示)。另外,都已經有了棧頂在頭部了,單連結串列中比較常用的頭結點也就失去了意義,通常對於鏈棧來說,是不需要頭結點的。
對於鏈棧來說,基本不存在棧滿的情況,除非記憶體已經沒有可以使用的空間,如果真的發生,那此時的計算機作業系統已經面臨當機崩潰的情況,而不是這個鏈棧是否溢位的問題。
但對於空棧來說,連結串列原定義是頭指標指向空,那麼鏈棧的空其實就是 top=NULL的時候。
棧的鏈式儲存結構一一進棧操作
對於鏈棧的進棧 push 操作,假設元素值為 e 的新結點是 S, top 為棧頂指標,示意圖如圖 4-6-2 所示程式碼如下。
棧的鏈式儲存結構一一出棧操作
假設變數 p 用來儲存要刪除的錢頂結點,將棧頂指標下移一位,最後釋放 p 即可,如圖 4-6-3 所示。
鏈棧的進棧push和出棧pop操作都很簡單,沒有任何迴圈操作,時間複雜度均為O(1)。
對比一下順序棧與鏈棧,它們在時間複雜度上是一樣的,均為O(1)。對於空間效能,順序棧需要事先確定一個固定的長度,可能會存在記憶體空間浪費的問題,但它的優勢是存取時定位很方便,而鏈棧則則要求每個元素都有指標域,這同時也增加了一些記憶體開銷,但對於棧的長度無限制。所以它們的區別和線性表中討論的是一樣,如果棧的使用過程中元素變化不可預料,有時很小,有時非常大,那麼最好是用鏈棧,反之,如果它的變化在可控範圍內,建議使用順序棧會好一些。
棧的作用
棧的引人簡化了程式設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決的問題核心。反之,像陣列等,因為要分散精力去考慮、陣列的下標增減等細節問題,反而掩蓋了問題的本質。
所以現在的許多高階語言,比如 )ava、 C#等都有對棧結構的封裝, 你可以不用關注它的實現細節,就可以直接使用 Stack 的 push 和 pop 方法,非常方便。
棧的應用一一遞迴
遞迴的定義
我們把一個直接呼叫自己或通過一系列的呼叫語句間接地呼叫自己的函式,稱做遞迴函式。
佇列(queue)是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
佇列是一種先進先出(First In First Out)的線性表,簡稱FIFO。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。
當然,寫遞迴程式最怕的就是陷入永不結束的無窮遞迴中 , 所以, 每個遞迴定義必須至少有一個條件,滿足時遞迴不再進行,即不再引用自身而是返回值退出。 比如剛才的例子,總有一次遞迴會使得 i<2 的,這樣就可以執行return i 的語句而不用繼續遞迴了。
選代和遞迴的區別是:迭代使用的是迴圈結構,遞迴使用的是選擇結構。遞迴能使程式的結構更清晰、更簡潔、更容易讓人理解,從而減少讀懂程式碼的時間。但是大量的遞迴呼叫會建立函式的副本,會耗費大量的時間和記憶體。選代則不需要反覆呼叫函式和佔用額外的記憶體。因此我們應該視不同情況選擇不同的程式碼實現方式。
遞迴與棧之間有什麼關係?
前面我們已經看到遞迴是如何執行宮的前行和退回階段的。遞迴過程退回的順序是它前行順序的逆序。在退回過程中,可能要執行某些動作,包括恢復在前行過程中儲存起來的某些資料。
這種儲存某些資料,並在後面又以儲存的逆序恢復這些資料,以提供之後使用的 需求,顯然很符合錢這樣的資料結構,因此, 編譯器使用植實現遞迴就沒什麼好驚訝的了。
簡單的說,就是在前行階段,對於每一層遞迴,函式的區域性變數、引數值以及返 回地址都被壓入棧中。在退回階段,位於棧頂的區域性變數、 引數值和返回地址被彈 出,用於返回撥用層次中執行程式碼的其餘部分,也就是恢復了呼叫的狀態。
棧的應用一一四則運算表示式求值
字尾(逆波蘭)表示法定義
我們舉個例子,對於”9+ (3-1) X3+10-:-2″,如果要用字尾表示法應該是什麼樣子:”931-3*+102/+”,這樣的表示式稱為字尾表示式,叫字尾的原因在於所有的符號都是在要運算數字的後面出現。顯然,這裡沒有了括號。
字尾表示式計算結果
為了解釋字尾表示式的好處,我們先來看看,計算機是如何應用字尾表示式計算最終的結果20的。
字尾表示式:931-3*+102/+
規則::從左到右遍歷表示式的每個數字和符號,遇到是數字就進枝,遇到是符號,就將處於桔頂兩個數字出攏,進行運算,運算結果進錢,一直到最終獲得結果。
- 初始化一個空技J 此錢用來對要運算的數字進出使用。如圖4-9-1的左圖所示
- 字尾表示式中前三個都是數字,所以9、3、1進棧,如圖4-9-1的右圖所示。
- 接下來是”-“,所以將棧中的1出棧作為減數,3出棧作為被減數,並運算3-1得到2,再將2進棧,如圖4-9-2的左圖所示。
- 接著是數字3進棧,如圖4-9-2的右圖所示。
- 後面是”*”,也就意味著棧中3和2出棧, 2與3相乘,得到6,並將 6進棧,如圖4-9-3的左圖所示。
- 下面是”+”,所以棧中6和9出棧,9與6相加,得到15,將15進棧,如圖4-9-3的右圖所示。
- 接著10和2兩數字進棧,如圖4-9-4的左圖所示。
- 接下來是符號”/”,因此,棧頂的2與10出棧,10與2相除,得到5,將5進棧,如圖4-9-4右圖所示。
- 最後一個是符號”+”,所以15與5出棧並相加,得到20,將20進棧,如圖4-9-5的左圖所示
- 結果是20出棧,棧變為空,如圖4-9-5的右圖所示。
中綴表示式轉字尾表示式(具體百度)
佇列的定義
佇列 ( queue ) 是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
佇列是一種先進先出 (First In First Out) 的線性表,簡稱 FIFO。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。
佇列的抽象資料型別
同樣是線性衰,佇列也有類似線性表的各種操作,不同的就是插入資料只能在隊尾進行,刪除資料只能在隊頭進行。
迴圈佇列
線性表有順序儲存和鏈式儲存,棧是線性表,所以有這兩種儲存方式。同樣,佇列作為一種特殊的線性表,也同樣存在這兩種儲存方式。先來看佇列的順序儲存結構
佇列順序儲存的不足
我們假設一個佇列有 n 個元素,則順序儲存的佇列需建立一個大於 n 的陣列,並把佇列的所有元素儲存在陣列的前 n 個單元,陣列下標為 0 的一端即是隊頭。所謂的 入佇列操作,其實就是在隊尾追加一個元素,不需要移動任何元素,因此時間複雜度為O(1),如圖4-12-1所示。
與棧不同的是,佇列元素的出列是在隊頭,即下標為 0 的位置,那也就意味著, 佇列中的所有元素都得向前移動,以保證佇列的隊頭,也就是下標為 0 的位置不為空,此時時間複雜度為 O(n),如圖 4-12.-2 所示.
可有時想想,為什麼出佇列時一定要全部移動呢,如果不去限制佇列的元素必須 儲存在陣列的前 n 個單元這一條件,出隊的效能就會大大增加。 也就是說,隊頭不需 要一定在下標為 0 的位置, 如圖 4-12-3 所示。
為了避免當只有一個元素時,隊頭和隊尾重合使處理變得麻煩,所以引入兩個指標, front指標指向隊頭元素,rear指標指向隊尾元素的下一個位置,這樣當front等於rear時,此佇列不是還剩一個元素,而是空佇列。
假設是長度為 5 的陣列,初始狀態,空佇列如圖 4-12-4 的左圖所示, front 與 rear 指標均指向下標為 0 的位置。 然後入隊 a1、 a2、 a3、 a4, front 指標依然指向下標 為 0 位置,而 rear 指標指向下標為 4 的位置,如圖 4-12-4 的右圖所示。
出隊 a1、 a2,則 front指標指向下標為 2 的位置, rear 不變,如圖 4-12-5 的左圖所示,再入隊 a5,此時 front 指標不變, rear 指標移動到陣列之外。嗯?陣列之外, 那將是哪裡?如圖 4-12-5 的右圖所示。
問題還不止於此。假設這個佇列的總個數不超過 5 個,但目前如果接著入隊的話,因陣列末尾元素已經佔用,再向後加,就會產生陣列越界的錯誤,可實際上,我 們的佇列在下標為 0 和 1 的地方還是空閒的。 我們把這種現象叫做”假溢位”。
迴圈佇列定義
我們把佇列的這種頭尾相接的順序儲存結構稱為迴圈佇列。
剛才的例子繼續,圖 4-12-5 的 rear 可以改為指向下標為 0 的位置,這樣就不會造成指標指向不明的問題了,如圖 4-12-6 所示。
接著入a6,將它放置於下標為 0 處, rear 指標指向下標為 1 處,如圖 4-12-7 的左圖所示。若再入隊a7,則 rear 指標就與front 指標重合,同時指向下標為 2 的位 置,如圖 4-12-7 的右圖所示。
- 此時問題又出來了,我們剛才說,空佇列時, fronr =rear,現在當佇列滿時,也是 front=rear,那麼如何判斷此時的佇列究竟是空還是滿呢?
- 辦法一是設定一個標誌變數 flag, 當 front == rear,且 flag = 0 時為佇列空, 當 front== rear,且 flag= 1 時為佇列滿。
- 辦法二是當佇列空時,條件就是 front = rear,當佇列滿時,我們修改其條件,保留一個元素空間。也就是說,佇列滿時,陣列中還有一個空閒單元。 例如圖 4-12-8 所示,我們就認為此佇列已經滿了,也就是說,我們不允許圖 4-12-7 的右圖情況出現。
我們重點來討論第二種方法,由於rear可能比 front大,也可能比front 小,所以儘管它們只相差一個位置時就是滿的情況,但也可能是相差整整一圈。 所以若佇列的最大尺寸為 QueueSize,那麼佇列滿的條件是 (rear+1) %QueueSize==front (取模”%” 的目的就是為了整合rear與front 大小為一個問題)。比如上面這個例子,QueueSize = 5,圖 4-12-8 的左圖中 front=0,而 rear=4, (4+1) %5 = 0,所以此時佇列滿. 再比如圖 4-12-8 中的右圖, front = 2 而 rear = 1。 (1 + 1) %5 = 2 ,所以此時 佇列也是滿的。而對於圖 4-12-6, front=2 而 rear= 0 , (0+1) %5 = 1 , 1 ≠ 2,所以此時佇列並沒有滿。
另外,當 rear> front 時,即圖 4-12-4 的右圖和 4-12-5 的左圖,此時佇列的長度 為 rear-front 但當 rear < front 時,如圖 4-12-6 和圖 4-12-7 的左圖,佇列長度分為 兩段, 一段是 QueueSize-front, 另一段是 0 + rear,加在一起,佇列長度為 rear-front + QueueSize。因此通用的計算佇列長度公式為: (rear- front + QueueSize) %QueueSize
佇列的鏈式儲存結構及實現
佇列的鏈式儲存結構,其實就是線性表的單連結串列,只不過它只能尾進頭出而已, 我們把它簡稱為鏈佇列。為了操作上的方便,我們將隊頭指標指向鏈佇列的頭結點,而對尾指標指向終端結點,如圖4-13-1所示
空佇列時, front 和 rear 都指向頭結點,如圖 4-13-2 所示。
佇列的鏈式儲存結構一一入隊操作
人隊操作時,其實就是在連結串列尾部插入結點,如圖 4-13-3 所示。
佇列的鏈式儲存結構一一出隊操作
出隊操作時,就是頭結點的後繼結點出隊,將頭結點的後繼改為它後面的結點, 若連結串列除頭結點外只剩一個元素時, 則需將 rear 指向頭結點,如圖 4-13-4 所示。
對於迴圈佇列與鏈佇列的比較,可以從兩方面來考慮,從時間上,其實它們的基 本操作都是常數時間,即都為 0(1)的,不過迴圈佇列是事先申請好空間,使用期間不釋放,而對於鏈佇列,每次申請和釋放結點也會存在一些時間開銷,如果入隊出隊頻 繁,則兩者還是有細微差異。對於空間上來說,迴圈佇列必須有一個固定的長度,所以就有了儲存元素個數和空間浪費的問題。而鏈佇列不存在這個問題,儘管它需要一個指標域, 會產生一些空間上的開銷,但也可以接受。 所以在空間上,鏈佇列更加靈活。
總的來說,在可以確定佇列長度最大值的情況下 ,建議用迴圈佇列,如果你無法預估佇列的長度時,則用鏈佇列。
總結
棧和佇列,它們都是特殊的線性表, 只不過對插入和刪除操作做了限制。
棧 (stack) 是限定僅在表尾進行插入和刪除操作的線性襲。
佇列 (queue) 是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
它們均可以用線性表的順序儲存結構來實現,但都存在著順序儲存的一些弊端。
因此它們各自有各自的技巧來解決這個問題。
對於棧來說,如果是兩個相同資料型別的棧,則可以用陣列的兩端作棧底的方法來讓兩個棧共享資料,這就可以最大化地利用陣列的空間。
對於佇列來說,為了避免陣列插入和刪除時需要移動資料,於是就引入了迴圈佇列 ,使得隊頭和隊尾可以在陣列中迴圈變化。解決了移動資料的時間損耗,使得本來插入和刪除是 O(n)的時間複雜度變成了O(1)。
它們也都可以通過鏈式儲存結構來實現,實現原則上與線性表基本相同如圖 4-14-1 所示。
參考:《大話資料結構》
感謝你花時間讀到結尾!:D
後端一枚,默默搬磚擼程式碼,如果覺得不錯歡迎關注我的公眾號