棧是限定僅在表尾進行插入和刪除操做的線性表後端
咱們把容許插入和刪除的一端稱爲棧頂 (top) ,另外一端稱爲棧底 (bottom) ,不含任何數據元素的棧稱爲空棧。 棧又稱爲後進先出 (Last In Filrst Out) 的線性表,簡稱LIFO結構。數組
理解棧的定義須要注意: 首先它是一個線性表,也就是說,棧元素具備線性關係,即前驅後繼關係。只不過它是一種特殊的線性表而已。定義中說是在線性表的表尾進行插入和刪除操做,這裏表尾是指棧頂,而不是棧底。數據結構
它的特殊之處就在於限制了這個線性表的插入和刪除位置,它始終只在棧頂進行。這就使得:棧底是固定的,最早進棧的只能在棧底。函數
棧的插入操做,叫作進棧,也稱壓棧、入棧。相似子彈入彈夾,如圖4-2-2所示。性能
棧的刪除操做,叫作出棧,也有叫作彈棧。如同彈夾中的子彈出夾,如圖4-2-3所示。 編碼
如今我要問問你們,這個最早進棧的元素,是否是就只能是最後出戰呢? 答案是不必定,要看什麼狀況。棧對線性表的插入和刪除的位置進行了限制 ,並無對元素進出的時間進行限制,也就是說,在不是全部元素都進棧的狀況下,事先進去的元素也能夠出棧,只要保證是棧頂元素出棧就能夠。操作系統
舉例來講,若是咱們如今是有 3 個整型數字元素 一、 二、 3 依次進棧,會有哪些出棧次序呢?設計
有沒有多是 312 這樣的次序出棧呢?答案是確定不會。由於 3 先出棧,就意味着,3曾經進棧,既然 3 都進棧了,那也就意味着, 1 和 2 已經進棧了,此時, 2 一 定是在 1 的上面,就是更接近棧頂,那麼出棧只多是 321,否則不知足 123 依次進棧的要求,因此此時不會發生1比2先出棧的狀況。3d
從這個簡單的例子就能看出,只是 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/+ 規則::從左到右遍歷表達式的每一個數字和符號,遇到是數字就進枝,遇到是符號,就將處於桔頂兩個數字出攏,進行運算,運算結果進錢,一直到最終得到結果。
中綴表達式轉後綴表達式(具體百度)
隊列 ( 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 的位置。 而後入隊 a一、 a二、 a三、 a4, front 指針依然指向下標 爲 0 位置,而 rear 指針指向下標爲 4 的位置,如圖 4-12-4 的右圖所示。
出隊 a一、 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,那麼如何判斷此時的隊列到底是空仍是滿呢?
咱們重點來討論第二種方法,因爲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
後端一枚,默默搬磚擼代碼,若是以爲不錯歡迎關注個人公衆號