使用抽象資料型別可以幫助我們更好的理解資料所需的操作,之後再進行具體的資料型別實現。實際上,往往是操作影響著我們決定資料型別該如何實現,這裡有兩種典型的資料結構-棧和佇列。
本質上,棧和佇列都是線性表,只是根據操作的需求我們人為地線上性表上加上限制,形成了兩種具有獨特功能的資料結構。
1、棧
首先,普通的線性表實現是有兩個埠可以訪問的,但是如果作為棧就要封閉一端,只能訪問另一端。這當然不是自討苦吃,棧是一種抽象資料結構,是對現實世界物件的模擬。比如,自助餐廳中的一疊盤子,新盤子放在這一疊盤子的最上面,取得時候也是從最上面取。將其抽象出來就是棧,這是最合適的抽象方式。
基於棧的操作非常簡單:
- 將資料壓入棧頂-push
- 將棧頂資料彈出-pop
- 檢視棧頂資料-top
棧的實現不是難點,基於棧的操作也很簡單,重點是棧的運用。
動態圖:
棧的先進後出規則,本質上代表著資料的次序,常見的二叉樹先序、中序、後序非遞迴遍歷,其實就是借用這種規則實現的。想要深入理解棧,沒有取巧的方法,見多識廣用在這裡再合適不過了,這裡使用一個簡單的例子來加深對棧的理解。
大整數加法,現在有個需求,將1856845129568452684和8948756841235879相加,怎麼處理?這兩個整數太大了,尋常的整數型別根本無法儲存他們,更別說他們相加的結果。為了解決這個問題,可以將這種非常大的數看成一串數字,分別存到兩個棧中,然後從棧中彈出數,進行加法操作。
虛擬碼如下:
largeNumAdd()
{
讀第一個數的數字,並將這些數字壓入到一個棧中;
讀第二個數的數字,並將這些數字壓入到另一個棧中;
carry = 0; //代表進位
while(至少有一個棧不為空)
從每個非空的棧中彈出一個數,將這兩個數字與進位相加;
將和的個位數字壓入到結果棧中;
將和的進位存到carry中;
如果進位不為0,將其壓入到結果棧中;
從結果棧中彈出數字並顯示;
}複製程式碼
簡單起見,這裡給出456和7891相加時棧的結構:
這不就是我們學過的加法計算公式嘛,是的,這裡使用棧模擬了加法過程。
將數字壓入棧中,其實維持了千位、百位、十位、個位之間的次序,正是這個原因才能保證棧彈出的時候數字相加是合理的。這只是棧簡單的一種運用,在現實生活中,所有需要保持次序的資料,都可以使用棧這種先進後出的結構,通過巧妙的設計完成演算法邏輯。
2、佇列
佇列是一種簡單的等待序列,在尾部加入元素時佇列加長,在前端刪除資料時佇列縮短。與棧不同,佇列是一種使用兩端的結構:一端用來加入新元素,另一端用來刪除元素。佇列是先進先出的結構。
佇列的操作與棧操作相似:
- 在佇列尾部加入元素-enqueue(el)
- 取出佇列的第一個元素-dequeue()
- 檢視佇列頭部元素-firstEI()
動態圖:
佇列的實現:
佇列的一種可能實現方式是使用陣列,但這並非最佳選擇。元素從隊尾加入而從隊首刪除,這會釋放陣列中的某些單元,這些單元不應該浪費。一種可能的做法是使用迴圈陣列,如果隊尾已滿而隊首有空的單元,可以將新加元素放入隊首,形成迴圈陣列,這種做法是空間比較緊張時的無奈之舉,因為它破壞了佇列的簡單易用性,所以不推薦。
佇列的另一種可能實現是使用雙向連結串列,那麼執行入佇列和出佇列操作僅需要常數時間,並且沒有陣列實現中空間的浪費,因此,推薦這種方法。
佇列的變種:
- 優先佇列
在許多情況下,簡單的佇列結構是不夠的,先入先出機制需要使用某些優先規則來完善。在郵政局中,殘疾人應該比其他人享有一定的優先權。在程式佇列中,由於系統的功能需求,即使在等待佇列中程式P1在P2之前,P2也需要在P1之前執行。以此類推,需要一種修正的佇列,這就是所謂的優先佇列。
優先佇列可以用兩種連結串列的變種實現。一種變種是所有的元素按照進入順序排序,出隊時按照優先順序。另一種是根據元素的優先順序決定新增元素的位置。在這兩種情況下,總的執行時間都是O(n)
,在標準庫中使用後一種方式實現的,因為我們希望在元素出隊時可以儘可能的快。
- 雙端佇列
顧名思義,雙端佇列就是可以在佇列的兩端壓入、彈出元素。這就有問題了,雙端佇列和普通的陣列、連結串列有什麼區別?不都可以兩端訪問嘛。當然是有區別的,雙端佇列的產生是基於以下需求的。眾所周知,陣列和連結串列是線性表的兩種實現方式,陣列的優勢在於可以常數時間內隨機訪問元素,連結串列的優勢在於可以常數時間內在兩端插入資料。那麼,有沒有一種實現方式可以綜合這兩個特點呢?答案是雙端佇列。
一切的奧妙在於雙端佇列的實現方式。首先從陣列講起,我們定義了陣列A,陣列A本身是支援常數時間內隨機訪問元素的,但是如果在頭部插入資料,就會造成大量元素後移,這是不能容忍的。怎麼解決呢?那就再定義一個陣列B,如果在A頭部插入新元素a,就將a放到陣列B的尾端,這時候陣列A和陣列B都是被封裝在雙端佇列中的,並且雙端佇列維護了一段鏈式結構,其中每個節點指向一個陣列。看到這裡想必大家已經明白,雙端佇列通過維護多個陣列來避免頭部插入操作造成的大量資料後移,儘管雙端佇列的實現比較複雜,但是作為使用者,既可以常數時間內隨機訪問元素,又可以常數時間內在佇列兩端插入資料,這對於某些場景下非常合適。
雙端佇列並不能取代陣列和連結串列,因為陣列和連結串列的實現簡單、直觀,可以滿足大部分需求,只有在特殊場景下才去考慮雙端佇列,這就是所謂的對症下藥。
3、標準庫實現
這裡簡單介紹下標準庫中的棧和佇列。
在標準庫中棧和佇列是一種容器介面卡。什麼叫做容器介面卡呢?其實就是拿一種已有的容器,在上面重新封裝對外暴漏的介面,拼裝成一種新的特殊容器。
標準庫首先實現了雙端佇列,它是一種真正的容器,不是容器介面卡。
標準庫中的棧是一種容器介面卡,預設是基於雙端佇列實現的,我們在使用過程中可以指定新的底層容器,比如向量或者連結串列。
標準庫中的佇列也是一種容器介面卡,預設也是基於雙端佇列實現的,但是我們只能選擇連結串列作為新的底層容器,不能選擇向量。這是因為佇列是允許在頭部刪除資料的,而向量沒有實現這種操作。
標準庫中的優先佇列也是一種容器介面卡,預設是基於向量實現的,但是我們也能選擇使用雙端佇列作為底層容器。注意,這裡不能選擇連結串列,因為標準庫中的優先佇列要求底層容器提供隨機訪問迭代器,而連結串列並沒有提供。所謂的隨機訪問迭代器是指通過該迭代器可以訪問容器中任一元素,而連結串列的迭代器只能自增或者自減,並不能隨機訪問任一元素。
到此為止,棧和佇列的相關概念已經探討完畢,本文也只是淺嘗輒止,更加深入的知識需要在實踐中摸索獲取,畢竟,成就在於個人。