大話數據結構讀書筆記系列(四)棧與隊列

4.1 開場白

同窗們,你們好!咱們又見面了。算法

不知道你們有沒有玩過手槍,估計都沒有。如今和平年代,上哪去玩這種危險的真東西,就是仿真玩具也大都被限制了。我小時候在軍訓時,也算是一次機會,幾個老兵和咱們學生聊天,讓咱們學習了一下關於槍的知識。編程

當時那個老兵告訴咱們,早先軍官們都愛用左輪手槍,而非彈夾式手槍,問咱們爲何,咱們誰也說不上來。如今我要問問大家,知道爲何嗎?(下面一臉惘然)。數組

哈,我聽到下面有同窗說是由於左輪手槍好看,酷呀。嘿,固然不是這個緣由。算了,估計大家也很難猜獲得。他那時告訴咱們說,由於子彈質量不過關,有個別多是臭彈--也就是有問題的、打不出來的子彈。彈夾式手槍(如圖4-1-1所示),若是當中有一顆是卡住了的臭彈,那麼後面的子彈就都打不了了。想一想看,在你準備用槍的時候,那基本到了不是你死就是我亡的時刻,忽然這手槍明明有子彈卻打不出來,這不是要命嗎?而左輪手槍就不存在這問題,這一顆不行,轉到下一顆就能夠了,人總不會倒黴到六顆全是臭彈。固然,後來子彈質量基本過關了,因爲彈夾能夠放8顆甚至20顆子彈,比左輪手槍的只能放6顆子彈要多,因此後來普及率更高的仍是彈夾式的手槍。 瀏覽器

哦,原來如此。我當時自認爲聰明的說道:那很好辦呀,這彈夾不是先放進去的子彈的,最後才能夠打出來嗎?你能夠把臭彈最早放進去,好子彈留在後面,這樣就不會影響了呀。數據結構

他笑罵道,笨蛋,若是真的知道哪一顆是臭彈,還放進去幹嗎,早就扔了。(你們大笑)函數

哎,我其實一直都是有點笨笨的。性能

4.2 棧的定義

4.2.1 棧的定義

好了,說這個例子目的不是要告訴大家我當年有多笨,而是爲了引出今天的主題,就是相似彈夾中的子彈同樣先進去,卻要後出來,然後進的,反而能夠先出來的數據結構--棧。學習

在咱們軟件應用中,棧這種後進先出數據結構的應用是很是廣泛的好比你用瀏覽器上網時,無論什麼瀏覽器都有一個"後退"鍵,你點擊後能夠按訪問順序的逆序加載瀏覽過的網頁。好比你原本看着新聞好好的,忽然看到一個連接說,有個可讓你年薪100萬的工做,你絕不猶豫點擊它,跳轉進去一看,這都是啥呀,具體內容我也就不說了,騙人騙得一點水平都沒有。此時你還想回去繼續看新聞,就能夠點擊左上角的後退鍵。即便你從一個網頁開始,連續點了幾十個連接跳轉,你點"後退"時,仍是能夠像歷史倒退同樣。回到以前瀏覽過的某個頁面,如圖4-2-1所示。 操作系統

不少相似的軟件,好比Word、Photoshop等文檔或圖像編輯軟件中,都有撤銷(undo)的操做,也是用棧這種方式來實現的,固然不一樣的軟件具體實現代碼會有很大差別,不過原理其實都是同樣的。 .net

咱們把容許插入和刪除的一端稱爲棧頂(top),另外一端稱爲棧底(bottom),不含任何數據元素的棧稱爲空棧。棧又稱爲後進先出(Last In First Out)的線性表,簡稱LIFO結構

理解棧的定義須要注意:

首先它是一個線性表,也就是說,棧元素具備線性關係,即前驅後繼關係。只不過它是一種特殊的線性表而已。定義中說是在線性表的表尾進行插入和刪除操做,這裏表尾是指棧頂,而不是棧底。

它的特殊之處就在於限制了這個線性表的插入和刪除位置,它始終只在棧頂進行。這也就使得:棧底是固定的,最早進棧的只能在棧底。

棧的插入操做,叫做進棧,也叫壓棧、入棧。相似子彈入彈夾,如圖4.2.2所示。

棧的刪除操做,叫做出棧,也有的叫做彈棧。如同彈夾中的子彈出夾,如圖4-2-3所示。

進棧出棧變化形式

如今我要問問你們,這個最早進棧的元素,是否是就只能是最後出棧呢

答案是不必定,要看什麼狀況。棧對線性表的插入和刪除的位置進行了限制,並無對元素進出的時間進行限制,也就是說,在不是全部元素都進棧的狀況下,事先進去的元素也能夠出棧,只要保證是棧頂元素出棧就能夠了。

舉例來講,若是咱們如今是有3個整型數字元素一、二、3依次出棧,會有哪些出棧次序呢?

  • 第一種:一、二、3進,再三、二、1出。這是最簡單的最好理解的一種,出棧次序爲321。
  • 第二種:1進,1出,2進,2出,3進,3出。也就是進一個就出一個,出棧次序爲123。
  • 第三種:1進,2進,2出,1出,3進,3出。出棧次序爲213。
  • 第四種:1進,1出,2進,3進,3出,2出。出棧次序爲132。
  • 第五種:1進,2進,2出,3進,3出,1出。出棧次序爲231。

有沒有多是312這種次序出棧呢?答案是確定不會。由於3先出棧,就意味着,3曾經進棧,既然3都進棧了,那也就意味着,1和2已經進棧了,此時,2必定是在1的上面,就是更接近棧頂,那麼出棧只多是321,否則不知足123一次進棧的要求,因此此時不會發生1比2先出棧的狀況。

從這個簡單的例子就能看出,只是3個元素,就有5種可能的出棧次序,若是元素數量多,其實出棧的變化將會更多的。這個知識點必定要弄明白。

4.3 棧的抽象數據類型

對於棧來說,理論上線性表的操做特性它都具有,可因爲它的特殊性,因此針對它在操做上會有些變化。特別是插入和刪除操做,咱們更名爲push和pop,英文直譯的話是壓和彈,更容易理解。你就把它當成是彈夾的子彈壓入和彈出就好記憶了,咱們通常叫進棧和出棧 因爲棧自己就是一個線性表,那麼上一章咱們討論了線性表的順序存儲和鏈式存儲,對於棧來講,也是一樣適用的

4.4 棧的順序存儲結構及實現

4.4.1 棧的順序存儲結構

既然棧是線性表的特例,那麼棧的順序存儲其實也是線性表順序存儲的簡化,咱們簡稱爲順序棧。線性表是用數組來實現的,想一想看,對於棧這種只能一頭插入刪除的線性表來講,用數組哪一端來做爲棧頂和棧底比較好?

對,沒錯,下標爲0的一端做爲棧底比較好,由於首元素都存在棧底,變化最小,因此讓它做棧底。

咱們定義一個top變量來指示棧頂元素在數組中的位置,這top就如同中學物理學過的遊標卡尺的遊標,如圖4-4-1,它能夠來回移動,意味着棧頂的top能夠變大變小,但不管如何遊標不能超出尺的長度。同理,若存儲棧的長度爲StackSize,則棧頂位置top必須小於StackSize。當棧存在一個元素時,top等於0,所以一般把空棧的斷定條件定爲top等於-1。

來看棧的結構定義

若如今有一個棧,StackSize是5,則棧普通狀況、空棧和棧滿的狀況示意圖如圖4-4-2所示。

4.4.2 棧的順序存儲結構--進棧操做

對於棧的插入,即進棧操做,其實就是作了如圖4-4-3所示的處理。

所以對於進棧操做push,其代碼以下:

4.4.3 棧的順序存儲結構--出棧操做

出棧操做pop,代碼以下: 二者沒有涉及到任何循環語句,所以時間複雜度均是O(1)。

4.5 兩棧共享空間

其實棧的順序存儲仍是很方便的,由於它只准棧頂進出元素,因此不存在線性表插入和刪除時須要移動元素的問題。不過它有一個很大的缺陷,就是必須事先肯定數組存儲空間大小,萬一不夠用了,就須要編程手段來擴展數組的容量,很是麻煩。對於一個棧,咱們也只能儘可能考慮周全,設計出合適大小的數組來處理,但對於兩個相同類型的棧,咱們卻能夠作到最大限度地利用其事先開闢的存儲空間來進行操做。

打個比方,兩個大學室友畢業同時到北京工做,開始時,他們以爲住了這麼多年學校的集體宿舍,如今工做了必定要有本身的私密空間。因而他們都但願租房時能找到獨住的一居室,可找來找去卻發現,最便宜的一居室也要每個月1500元,地段還很差,實在是承受不起,最終他倆仍是合租了一套兩居室,一共2000元,各出一半,還不錯。

對於兩個一居室,都有獨立的衛生間和廚房,是私密了,但大部分空間的利用率卻不高。而兩居室,兩我的各有臥室,還共享了客廳、廚房和衛生間,房價的利用率就顯著提升,並且租房成本也大大降低了

一樣的道理,若是咱們有兩個相同類型的棧,咱們爲它們各自開闢了數組空間,極有多是第一個棧已經滿了,再進棧就溢出了,而另外一個棧還有不少存儲空間空閒。這又何須呢?咱們徹底能夠用一個數組來存儲兩個棧,只不過須要點小技巧。

咱們的作法如圖4-5-1,數組有兩個端點,兩個棧又兩個棧底,讓一個棧的棧底爲數組的始端,即下標爲0處,另外一個棧爲棧的末端,即下標爲數組長度n-1處。這樣,兩個棧若是增長元素,就是兩端點向中間延伸。

其實關鍵思路是:它們是在數組的兩端,向中間靠攏。top1和top2是棧1和棧2的棧頂指針,能夠想象,只要它們不見面,兩個棧就能夠一直使用。

想一想極端的狀況,若棧2是空棧,棧1的top1等於n-1時,就是棧1滿了。反之,當棧1爲空棧時,top等於0時,爲棧2滿。單更多的狀況,其實就是我剛纔說的,兩個棧見面之時,也就是兩個指針之間相差1時,即top1+1 == top2爲棧滿。

兩棧共享空間的結構的代碼以下:

對於兩棧共享空間的push方法,咱們除了要插入元素值參數外,還須要有一個判斷是棧1仍是棧2的棧號參數stackNumber。插入元素的代碼以下:

由於在開始已經判斷了是否有棧滿的狀況,因此後面的top1+1或top2-1是不擔憂溢出問題的。

對於兩棧共享空間的pop方法,參數就只是判斷棧1 棧2的參數stackNumber,代碼以下:

事實上,使用這樣的數據結構,一般都是當兩個棧的空間需求有相反關係時,也就是一個棧增加時另外一個棧在縮短的狀況。就像買賣股票同樣,你買入時,必定是有一個你不知道的人在作賣出操做。有人賺錢,就必定是有人賠錢。這樣使用兩棧共享空間存儲方法纔有比較大的意義。不然兩個棧都在不停地增加,那很快就會因棧滿而溢出了

固然,這只是針對兩個具備相同數據類型的棧的一個設計上的技巧,若是是不相同數據類型的棧,這種辦法不但不能更好地處理問題,反而會使問題變得更加複雜,你們要注意這個前提。

4.6 棧的鏈式存儲結構及實現

4.6.1 棧的鏈式存儲結構

講完了棧的順序存儲結構,咱們如今來看看棧的鏈式存儲結構,簡稱爲鏈棧

想一想看,棧只是棧頂來作插入和刪除操做,棧頂放在鏈表的頭部仍是尾部呢?因爲單鏈表有頭指針,而棧頂指針也是必須的,那幹嗎不讓它倆合二爲一呢,因此比較好的辦法是把棧頂放在單鏈表的頭部(如圖4-6-1所示)。另外,都已經有了棧頂在頭部了,單鏈表中比較經常使用的頭結點也就失去了意義,一般對於鏈棧來講,是不須要頭結點的。

對於鏈棧來講,基本不存在棧滿的狀況,除非內存已經沒有可使用的空間,若是真的發生,那此時的計算機操做系統已經面臨死機奔潰的狀況,而不是這個鏈棧是否溢出的問題

但對於空棧來講,鏈表原定義是頭指針指向空,那麼鏈棧的空其實就是top=NULL的時候。

鏈棧的結構代碼以下: 鏈棧的操做絕大部分都和單鏈表相似,只是在插入和刪除上,特殊一些

4.6.2 棧的鏈式存儲結構--進棧操做

對於鏈棧的進棧push操做,假設元素值爲e的新結點是s,top爲棧頂指針,示意圖如圖4-6-2所示代碼以下。

4.6.3 棧的鏈式存儲結構--出棧操做 至於鏈棧的出棧pop操做,也是很簡單的三句操做。假設變量p用來存儲要刪除的棧頂結點,將棧頂指針下移一位,最後釋放p便可,如圖4-6-3所示。

鏈棧的進棧的push和出棧pop操做都很簡單,沒有任何循環操做,時間複雜度均爲O(1)

對比一下順序棧與鏈棧,它們在時間複雜度上是同樣的,均爲O(1)。對於空間性能,順序棧須要事先肯定一個固定的長度,可能會存在內存空間浪費的問題,但它的優點是存取時定位很方便,而鏈棧則要求每一個元素都有指針域,這同時也增長了一些內存開銷,但對於棧的長度無限制。因此他們的區別和線性表中討論的同樣,若是棧的使用過程當中元素變化不可預料,有時候很小,有時候很是大,那麼最好是用鏈棧,反之,若是它的變化在可控範圍內,建議使用順序棧會更好一些

4.7 棧的做用

有的同窗可能會以爲,用數組或鏈表直接實現功能不就好了嗎?幹嗎要引入棧這樣的數據結構呢?這個問題問得好。

其實這和咱們明明有兩隻腳能夠走路,幹嗎還要乘汽車、火車、飛機同樣。理論上,陸地上的任何地方,你都是能夠靠雙腳走到的,可那須要多少時間和精力呢?咱們更關注的是到達而不是如何去的過程。

棧的引入簡化了程序設計的問題,劃分了不一樣關注層次,使得思考範圍縮小,更加聚焦於咱們要解決的問題核心。反之,像數組等,由於要分散精力去考慮數組的下標增減等細節問題,反而掩蓋了問題的本質

因此如今的許多高級語言,好比Java、C#等都有對棧結構的封裝,你能夠不用關注它的實現細節,就能夠直接使用Stack的push和pop方法,很是方便。

4.8 棧的應用--遞歸

棧有一個很重要的應用:在程序設計語言中實現了遞歸。那麼什麼是遞歸呢?

當你往鏡子前面一站,鏡子裏面就有一個你的像。但你試過兩面鏡子一塊兒照嗎?若是A、B兩面鏡子相互面對面放着,你往中間一站,嘿,兩面鏡子裏都有你的千百個"化身"。爲何會有這麼奇妙的現象呢?原來,A鏡子裏有B鏡子的像,B鏡子裏也有A鏡子的像,這樣反反覆覆,就會產生一連串的"像中像"。這是一種遞歸現象,如圖4-8-1所示。

咱們先來看一個經典的遞歸例子:斐波那契數列(Fibonacci)。爲了說明這個數列,這位斐老還舉了一個很形象的例子。

4.8.1 斐波那契數列實現

若是兔子在出生兩個月後,就有繁殖能力,一對兔子每月能生出一對小兔子來。假設全部兔都不死,那麼一年之後能夠繁殖多少對兔子呢?

咱們拿新出生的一對小兔子分析一下:第一個月小兔子沒有繁殖能力,因此仍是一對;兩個月後,生下一對小兔子數共有兩對;三個月之後,老兔子又生下一對,由於小兔子尚未繁殖能力,因此一共是三對...依次類推能夠列出下表(表4-8-1)。

表中 數字1,1,2,3,5,8,13...構成了一個序列。這個數列有個十分明顯的特色,那是:前面相鄰兩項之和,構成了後一項,如圖4-8-2所示。

能夠發現,編號1的一對兔子通過六個月就變成8對兔子了。若是咱們用數學函數來定義就是:

先考慮一下,若是咱們要實現這樣的數列用常規的迭代的辦法如何實現?假設咱們須要打印出前40位的斐波那契數列數。代碼以下:

代碼很簡單,幾乎不用作什麼解釋。但其實咱們的代碼,若是用遞歸來實現,還能夠更簡單。 怎麼樣,相比較迭代的代碼,是否是乾淨不少。嘿嘿,不過要弄懂它得費點腦子。

函數怎麼能夠本身調用本身?聽起來有些難以理解,不過你能夠不要把一個遞歸函數中調用本身的函數看做是在調用本身,而就當它是在調用另外一個函數。只不過,這個函數和本身長得同樣而已。

咱們來模擬代碼中的Fbi(i)函數當i=5的執行過程,如圖4-8-3所示。

4.8.2 遞歸定義

在高級語言中,調用本身和其餘函數並無本質的不一樣。咱們把一個直接調用本身或經過一系列的調用語句間接地調用本身的函數,稱作遞歸函數

固然,寫遞歸程序最怕的就是陷入永不結束的無窮遞歸中,因此,每個遞歸定義必須至少有一個條件,知足時遞歸再也不進行,即再也不引用自身而是返回值退出

好比剛纔的例子,總有一次遞歸會使得i小於2的,這樣就能夠執行return i的語句而不用繼續遞歸了。

對比了兩種實現斐波那契的代碼。迭代何遞歸的區別是:迭代使用的是循環結構,遞歸使用的是選擇結構。遞歸能使程序的結構更清晰、更簡潔、更容易讓人理解,從而減小讀懂代碼的時間。可是大量的遞歸調用會創建函數的副本,會耗費大量的時間和內存。迭代則不須要反覆調用函數和佔用額外的內存。所以咱們應該視不一樣狀況選擇不一樣的代碼實現方式。

那麼咱們講了這麼多遞歸的內容,和棧有什麼關係呢?這得從計算機系統的內部提及。

前面咱們已經看到遞歸是如何執行它的前行和退回階段的。遞歸過程退回的順序是它前行順序的逆序。在退回過程當中,可能要執行某些動做,包括恢復在前行過程當中存儲起來的某些數據。

這種存儲某些數據,並在後面又以存儲的逆序恢復這些數據,以提供以後使用的需求,顯然很符合棧這樣的數據結構,所以,編譯器使用棧實現遞歸就沒什麼好驚訝的了。

簡單的說,就是在前行階段,對於每一層遞歸,函數的局部變量、參數值以及返回地址都被壓入棧中。在退回階段,位於棧頂的局部變量、參數值和返回地址被彈出,用於返回調用層次中執行代碼的其他部分,也就是恢復了調用的狀態。

固然,對於如今的高級語言,這樣的遞歸問題是不須要用戶來管理這個棧的,一切都由系統代勞了。

4.9 棧的應用--四則運算表達式求值

4.9.1 後綴(逆波蘭)表示法定義

棧的現實應用也不少,咱們再來重點講一個比較常見的應用:數學表達式的求值。

咱們小學學數學的時候,有一句話是老師反覆強調的,"先乘除,後加減,從左算到右,先括號內後括號外"。這個你們都不陌生。我記得我小時候,每天作這種加減乘除的數學做業,很煩,因而就偷偷拿了老爸的計算器來幫着算答案,對於單純的兩個數的加減乘除,的確是省心很多,我也所以瀟灑了一兩年。可後來要求要加減乘除,甚至還有帶有大中小括號的四則運算,我發現老爸那個簡陋的計算器很差使了,好比9+(3-1)*3+10/2,這是一個很是簡單的題目,心算也能夠很快算出是20.可就這麼簡單的題目,計算器卻不能在一次輸入後立刻得出結果,非常不方便。

固然,後來出的計算器就高級多了,它引入了四則運算表達式的概念,也能夠輸入括號了,因此如今的00後的小朋友們,更加能夠偷懶、抄近路作數學做業了。

那麼在新式計算器中或者計算機中,它是如何實現的呢?若是讓你用C語言或者其餘高級語言實現對數學表達式的求值,你打算如何作?

這裏面的困難就在於乘除在加減的後面,卻要先運算,而加入了括號後,就變得更加複雜。不知道該如何處理。

但仔細觀察後發現,括號都是成對出現的,有左括號就必定會有右括號,對於多重括號,最終也是徹底嵌套匹配的。這用棧結構正好合適,只要碰到左括號,就將次左括號進棧,無論表達式有多少重括號,反正遇到左括號就進棧,然後面出現右括號時,就讓棧頂的左括號出棧,期間讓數字運算。這樣,最終有括號的表達式從左到右巡查一遍,棧應該是由空到有元素,最終再因所有匹配成功後成爲空棧的結果。

但對於四則運算,括號也只是當中的一部分,先乘除後加減使得問題依然複雜,如何有效地處理它們呢?咱們偉大的科學家想到了好辦法。

20世紀50年代,波蘭邏輯學家Jan Lukasiewicz,當時也和咱們如今的同窗們同樣,困惑與如何才能夠搞定這個四則運算,不知道他是否也像牛頓被蘋果砸到頭而想到萬有引力的原理,或者仍是阿基米德在浴缸中洗澡時想到判斷皇冠是否純金的辦法,總之他也是靈感突現,想到了一種不須要括號的後綴表達法,咱們也把它稱爲逆波蘭(Reverse Polish Notation,RPN)表示。我想多是他的名字太複雜了,因此後人只用他的國籍而不是姓名來命名,實在惋惜。這也告訴咱們,想要流芳百世,名字還要起得朗朗上口才行。這種後綴表示法,是表達式的一種新的顯示方式,很是巧妙地解決了程序實現四則運算的難題。

咱們先來看看,對於"9+(3-1)3+10/2",若是要用後綴表示法應該是什麼樣子:"9 3 1-3 +10 2/+",這樣的表達式稱爲後綴表達式,叫後綴的緣由在於全部的符號都是在要運算數字的後面出現。顯然,這裏沒有了括號。對於歷來沒有接觸事後綴表達式的同窗來說,這樣的表述是很難受的。不過你不喜歡,有機器喜歡,好比咱們聰明的計算機。

4.9.2 後綴表達式計算結果

爲了解釋後綴表達式的好處,咱們先來看看,計算機如何應用後綴表達式計算出最終的結果20的。

後綴表達式: 9 3 1-3 * + 10 2 /+

規則:從左到右遍歷表達式的每一個數字和符號,遇到是數字就進棧,遇到是符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終得到結果。

  1. 初始化一個空棧。此棧用來對要運算的數字進出使用。如圖4-9-1的左圖所示。
  2. 後綴表達式中前三個都是數字,因此九、三、1進棧,如圖4-9-1的右圖所示。
  3. 接下來是"-",因此將棧中的1出棧做爲減數,3出棧做爲被減數,並運算3-1獲得2,再將2進棧,如圖4-9-2的左圖所示。
  4. 接着是數字3進棧,如圖4-9-2的右圖所示。
  5. 後面是"*",也就意味着棧中3和2出棧,2與3相乘,獲得6,並將6進棧,如圖4-9-3的左圖所示。
  6. 下面是"+",因此棧中6和9出棧,9與6相加,獲得15,將15進棧,如圖4-9-3的右圖所示。
  7. 接着是10與2兩數字進棧,如圖4-9-4的左圖所示。
  8. 接下來是符號"/",所以,棧頂的2與10出棧,10與2相除,獲得5,將5進棧,如圖4-9-4的右圖所示。
  9. 最後一個是符號"+",因此15與5出棧並相加,獲得20,將20出棧,如圖4-9-5的左圖所示。
  10. 結果是20出棧,棧變爲空,如圖4-9-5的右圖所示。

果真,後綴表達法能夠很順利解決計算的問題。如今除了睡覺的同窗,應該都有一樣的疑問,就是這個後綴表達式"9 3 1-3 * + 10 2 /+"是怎麼出來的?這個問題不搞清楚,等於沒有解決。因此下面,咱們就來推導如何讓"9+(3-1)x3+10/2"轉化爲"9 3 1-3* + 10 2 /+"。

4.9.3 中綴表達式轉後綴表達式

咱們把平時所用的標準四則運算表達式,即"9+(3-1)x3+10/2"叫作中綴表達式。由於全部的運算符號都在兩數字的中間,如今咱們的問題就是中綴到後綴的轉化。

中綴表達式"9+(3-1)x3+10/2"轉化爲後綴表達式"9 3 1-3* + 10 2 /+"。

規則:從左到右遍歷中綴表達式的每一個數字和符號,如果數字就輸出,即成爲後綴表達式的一部分;如果符號,則判斷其與棧頂符號的優先級,是右括號或優先級低於棧頂符號(乘除優先加減)則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出後綴表達式爲止。

  1. 初始化一空棧,用來對符號進出棧使用。如圖4-9-6的左圖所示。
  2. 第一個字符是數字9,輸出9,後面是符號"+",進棧。如圖4-9-6的右圖所示。
  3. 第三個字符是"(",依然是符號,因其只是左括號,還未配對,故出棧。如圖4-9-7的左圖所示。
  4. 第四個字符是數字3,輸出,總表達式爲9 3,接着是"-",進棧。如圖4-9-7的右圖所示。
  5. 接下來是數字1,輸出,總表達式爲9 3 1,後面是符號")",此時,咱們須要去匹配此前的"(",因此棧頂依次出棧,並輸出,直到"("出棧爲止。此時左括號上方只有"-",所以輸出"-"。總的輸出表達式爲9 3 1 -。如圖4-9-8的左圖所示。
  6. 接着是數字3,輸出,總的表達式爲9 3 1 - 3。緊接着是符號"X",由於此時的棧頂符號爲"+"號,優先級低於"X",所以不輸出,"*"進棧。如圖4-9-8的右圖所示。
  7. 以後是符號"+",此時當前棧頂元素"*"比這個"+"的優先級高,所以棧中元素出棧並輸出(沒有比"+"號更低的優先級,因此所有出棧),總輸出表達式爲9 3 1-3 * +。而後將當前這個符號"+"進棧。也就是說,前6張圖的棧底的"+"是指中綴表達式中開頭的9後面那個"+",而圖4-9-9左圖中的棧底(也是棧頂)的"+"是指"9+(3-1)x3+"中的最後一個"+"。
  8. 緊接着數字10,輸出,總表達式變爲9 3 1-3 * + 10。後是符號"/",因此"/"進棧。如圖4-9-9的右圖所示。
  9. 最後一個數字2,輸出,總的表達式爲9 3 1 - 3 * + 10 2。如圖4-9-10的左圖所示。
  10. 因已經到最後,因此將棧中符號所有出棧並輸出。最終輸出的後綴表達式結果爲9 3 1 - 3 * + 10 2 /+。如圖4-9-10的右圖所示。

從剛纔的推導中你會發現,要想讓計算機具備處理咱們一般的標準(中綴)表達式的能力,最重要的就是兩步:

  1. 將中綴表達式轉化爲後綴表達是吧(棧用來進出運算的符號)。
  2. 將後綴表達式進行運算得出結果(棧用來進出運算的數字)。

整個過程,都充分利用了棧的後進先出特性來處理,理解好它其實也就理解好了棧這個數據結構。

好了,休息一下,一下子咱們繼續,接下來會講隊列。

4.10 隊列的定義

大家在用用電腦時有沒有經歷過,機器有時會處於疑似死機的狀態,鼠標點什麼彷佛都沒用,雙擊任何快捷方式都不動彈。就當你失去耐心,打算reset時。忽然它像酒醒了同樣,把你剛纔點擊的全部操做所有都按順序執行了一遍。這實際上是由於操做系統中的多個程序因須要經過一個通道輸出,而按前後次序排隊等待形成的。

再好比像移動、聯通、電信等客服電話,客服人員與客戶相比老是少數,在全部的客服人員都佔線的狀況下,客戶會被要求等待,直到有某個客服人員空下來,才能讓最早等待的客戶接通電話。這裏也是將全部當前撥打客服電話的客戶進行了排隊處理。

操做系統和客服系統中,都是應用了一種數據結構來實現剛纔提到的先進先出的排隊功能,這就是隊列

隊列是一種先進先出(First In First Out)的線性表,簡稱FIFO。容許插入的一端稱爲隊尾,容許刪除的一端稱爲隊頭。假設隊列是q=(a1,a2,....,an),那麼a1就是隊頭元素,而an是隊尾元素。這樣咱們就能夠刪除時,老是從a1開始,而插入時,列在最後。這也比較符合咱們一般生活中的習慣,排在第一個的優先出列,最後來的固然排在隊伍最後,如圖4-10-1所示。 隊列在程序設計中用得很是頻繁。前面咱們已經舉了兩個例子,再好比用鍵盤進行各類字母或數字的輸入,到顯示器上如記事本軟件上的輸出,其實就是隊列的典型應用,假如你原本和女朋友聊天,想表達你是個人上帝,輸入god,而屏幕上卻顯示出了dog發了出去,這真是要氣死人了。

4.11 隊列的抽象數據類型

一樣是線性表,隊列也有相似線性表的各類操做,不一樣的就是插入數據只能在隊尾進行,刪除數據只能在隊頭進行。

4.12 循環隊列

線性表有順序存儲和鏈式存儲,棧是線性表,因此有着兩種存儲方式。一樣,隊列做爲一種特殊的線性表,也一樣存在這兩種存儲方式。咱們先來看隊列的順序存儲結構。

4.12.1 隊列順序存儲的不足

咱們假設一個隊列有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.2 循環隊列定義

因此解決假溢出的辦法就是後面滿了,就再從頭開始,也就是頭尾相接的循環。咱們把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列

剛纔的例子繼續,圖4-12-5的rear能夠改成指向下標爲0的位置,這樣就不會形成指針指向不明的問題了,如圖4-12-6所示。

接着入隊a6,將它放置於下標爲0處,rear指針指向下標爲1處,如圖4-12-7的左圖所示。若再入隊a7,則rear指針就與front指針重合,同時指向下標爲2的位置,如圖4-12-7的右圖所示。

  • 此時問題又出來了,咱們剛纔說,空隊列時,front等於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的左圖,隊列長度分爲兩段,一段是QueueSuze-front,另外一段是0+rear,加在一塊兒,隊列長度爲rear-front+QueueSize。所以通用的計算隊列長度公式爲:

有了這些講解,如今實現循環隊列的代碼就不難了。

循環隊列的順序存儲結構代碼以下:

循環隊列的初始化代碼以下:

循環隊列求隊列長度代碼以下:

循環隊列的入隊列操做代碼以下:

循環隊列的出隊列操做代碼以下:

從這一段講解,你們應該發現,單是順序存儲,若不是循環隊列,算法的時間性能是不高的,但循環隊列又面臨着數組可能會溢出的問題,因此咱們還須要研究一下不須要擔憂隊列長度的鏈式存儲結構。

4.13 隊列的鏈式存儲結構及實現

隊列的鏈式存儲結構,其實就是線性表的單鏈表,只不過它只能尾進頭出而已,咱們把它簡稱爲鏈隊列。爲了操做上的方便,咱們將隊頭指針指向鏈隊列的頭結點,而隊尾指針指向終端結點,如圖4-13-1所示。

空隊列時,front和rear都指向頭結點,如圖4-13-2所示。

鏈隊列的結構爲:

4.13.1 隊列的鏈式存儲結構--入隊操做

入隊操做時,其實就是在鏈表尾部插入結點,如圖4-13-3所示。

其代碼以下:

4.13.2 隊列的鏈式存儲結構--出隊操做

出隊操做時,就是頭結點的後繼結點出隊,將頭結點的後繼改成它後面的節點,若鏈表除頭結點外只剩一個元素時,則需將rear指向頭結點,如圖4-13-4所示。

代碼以下:

對於循環隊列與鏈隊列的比較,能夠從兩方面來考慮,從時間上,其實它們的基本操做都是常數時間,即都爲O(1)的,不過循環隊列是事先申請好空間,使用期間不釋放,而對於鏈隊列,每次申請和釋放結點也會存在一些時間開銷,若是入隊出隊頻繁,則二者仍是有細微差別。對於空間上來講,循環隊列必須有一個固定的長度,因此就有了存儲元素個數和空間浪費的問題。而鏈隊列不存在這個問題,儘管它須要一個指針域,會產生一些空間上的開銷,但也能夠接受。因此在空間上,鏈隊列更加靈活

總的來講,在能夠肯定隊列長度最大值的狀況下,建議用循環隊列,若是你沒法預估隊列的長度時,則用鏈隊列

4.14 總結回顧

又到了總結回顧的時間。咱們這一章講的是棧和隊列,它們都是特殊的線性表,只不過對插入和刪除操做作了限制。

  • 棧(stack)是限定僅在表尾進行插入和刪除操做的線性表。
  • 隊列(queue)是隻容許在一端進行插入操做,而在另外一端進行刪除操做的線性表。

它們都可以用線性表的順序存儲結構來實現,但都存在着順序存儲的一些弊端。所以它們各自有各自的技巧來解決這個問題。

  1. 對於棧來講,若是是兩個相同數據類型的棧,則能夠用數組的兩端做棧底的方法來讓兩個棧共享數據,這就能夠最大化地利用數組的空間
  2. 對於隊列來講,爲了不數組插入和刪除時須要移動數據,因而就引入了循環隊列,使得隊頭和隊尾能夠在數組中循環變化。解決了移動數據的時間損耗,使得原本插入和刪除是O(n)的時間複雜度變成了O(1)

它們也均可以經過鏈式存儲結構來實現,實現原則上與線性表基本相同,如圖4-14-1所示。

相關文章
相關標籤/搜索