算法與數據結構(四)棧與隊列

上次聊到數組與鏈表,它們都是線性表,數組與鏈表的本質區別是內存是否連續,進而得出結論:數組可以在 O(1) 時間複雜度進行隨機訪問,但是對內存要求嚴苛;鏈表訪問元素時間複雜度爲 O(n),但是對內存要求低。

今天來介紹另外兩個線性表中的數據結構:棧和隊列。

棧 Stack

棧是一種線性表,只有前後關係,但是相對於數組和鏈表來說,其元素的操作是受限的。棧只允許在一端進行元素的插入、刪除操作,往棧中放入元素我們稱之爲壓棧操作,從棧中取出元素我們稱之爲出棧。我們可以把棧看作是一個一端開口的羽毛球筒,我們壓棧、出棧操作數據的過程,就是我們從筒中放入、取出羽毛球的過程。當然,棧是一種抽象的數據結構,我們在實現它時,既可以使用數組,也可以使用鏈表。

接下來我們看一下棧的使用情景。

瀏覽器網頁前進後退

我們經常會使用瀏覽器瀏覽網頁,網頁中有很多超鏈接可以跳轉到下一個頁面。而有時我們希望能夠返回上一個頁面繼續瀏覽,再返回上一個頁面之後,我們還可能再回到剛纔的子頁面。

使用兩個棧就可以實現這種能夠隨意前進後退的網頁瀏覽了。

我們把一開始打開的網頁壓棧放入棧 a,如果此時點擊網頁中的鏈接查看下一個頁面,那我們把子頁面繼續放入棧 a。當我們返回時,把子頁面從 a 中出棧,放入棧 b。這個時候我們關閉頁面,繼續從 a 中出棧放入棧 b 即可。如果我們點擊前進按鈕,則去棧 b 中出棧即可。也就是說,如果我們把依次打開過的網頁放入棧 a 維護,回退的界面放入棧 b 維護。點擊後退按鈕時,查詢棧 a,有頁面則出棧,放入棧 b,沒有則無法後退;點擊前進按鈕時,查詢棧 b,有頁面則出棧,沒有則無法前進。

函數調用棧

基本每一個編程語言中,都存在函數的概念。一個函數可以調用另外一個函數,那麼函數之間的調用是如何實現的呢?也是用棧。

以 Java 爲例,我們編寫 Java 代碼之後,是編譯爲 class 字節碼,然後交給 Java 虛擬機執行。在 Java 虛擬機中,是使用一個棧來完成函數調用的,每一個函數在棧中所佔有的空間稱爲棧幀。Java 入口函數爲 main 函數,執行時先將 main 函數入棧,此時棧幀中會保存該函數的局部變量、操作數棧、動態連接、方法返回地址等內容。當 main 函數調用一個子函數 add 時,虛擬機會繼續將 add 函數相關內容壓入棧頂,add 函數執行完畢後會出棧,此時 main 函數處於棧頂,會繼續執行 main 函數。

這個例子我們可以看到,對於基礎數據結構的理解,可以幫助我們瞭解平時常用語言、框架的實現細節,對於我們深入學習計算機知識很有幫助。除了以上兩個例子之外,棧還可以用來做表達式求值、括號匹配等,感興趣的話可以自行了解。

隊列 Queue

隊列和棧一樣,也是一種操作受限的線性表數據結構。棧只能在一端進行插入和刪除操作,隊列則是在一端進行刪除操作,稱爲出隊,在另一端進行插入操作,稱爲入隊。

和棧一樣,隊列也可以使用數組或者鏈表來實現。當我們使用數組來實現時,需要維護兩個變量 head、tail 標識隊列的頭部和尾部。入隊時,元素放入數組現有元素的末尾,tail 往後移動一位。出隊時,首個元素刪除,head 往後移動一位。此時會出現這樣一種情形:當我們經過不斷的入隊和出隊之後,head、tail 會不斷的往後漂移,當 tail 到達數組尾部時,沒辦法再進行入隊操作,但是很可能 head 之前還有空間。這就導致空間浪費。此時我們可以通過每次出隊都把所有數據往前搬移一位,也可以通過 tail 到達數組末尾時,搬移所有數據到數組頭部解決問題,但是頻繁的搬移操作會浪費性能。

由此導致了循環隊列的產生。循環隊列的關鍵是,把數組看成一個環狀結構。head、tail 到達數組末尾時,仍可以回到頭部的位置。此時使用 tail 尾部這個詞就不太合適,改用 rear 後部更好。如下圖,入隊出隊的過程,就是操作 head、rear 位置的過程。在這個環中,我們可以不斷的轉圈修改其位置,而不會出現上述局面,也就把我們從頻繁搬移數據中解綁,減少性能消耗。

但是我們的底層實現還是數組,這裏的關鍵問題有兩個:

  1. 如何在 rear 到達數組尾部時,處理 rear 的位置?
  2. 如何判斷隊空和隊滿?

對於第一個問題我們可以使用求餘操作來解決問題。普通隊列我們是直接 rear++ 操作位置,現在我們改成 rear = (rear + 1) % array.length - 1 來操作即可。當然 head 也是如此。

對於第二個問題,普通隊列我們通過 head == 0 && tail == array.length 來判斷隊滿,head == tail 來判斷隊空。循環隊列還是使用 head == tail 判斷隊空,隊滿的條件就要改成 (tail + 1) % array.length == head

另外一種隊列是阻塞隊列。阻塞隊列就是在隊列基礎上增加了阻塞操作。當隊空的時候,從隊列取出數據時會阻塞;當隊滿時,在隊列中插入數據會阻塞。可以發現,阻塞隊列跟我們常說的 生產者-消費者模型 邏輯基本一致。

還有一種比較常用的隊列是優先隊列,即優先級更高的元素優先出隊,該隊列的實現涉及到完全二叉樹、大頂堆、小頂堆相關知識,我們後續會涉及到。

總結

我們可以看到棧和隊列是一種抽象的數據結構,定義一種操作規則,然後應用到對應的場景之中。這兩種數據結構的操作規則和其應用場景是契合的。我們在學習算法和數據結構時,瞭解其優點和缺點,進而瞭解其應用場景是十分重要的。沒有最好的數據結構和算法,只有在某種場景下更加適合的數據結構與算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章