1. 程式人生 > >資料結構——線性結構(4)——順序佇列與迴圈佇列的原理

資料結構——線性結構(4)——順序佇列與迴圈佇列的原理

佇列的介面

從上一個專欄可以看出,棧和佇列是非常相似的結構。它們之間的唯一區別是處理元素的順序。棧使用後進先出(LIFO)的規律,其中對於來說push的最後一個元素始終是第一個pop的元素。而佇列採用更接近於排隊的的先進先出(FIFO)模式。棧和佇列的介面也非常相似。兩個介面的public部分的唯一變化是定義類的行為的兩種方法的名稱。 來自Stack類的push方法現在稱為入隊(enqueue),pop方法現在稱為出隊(dequeue)。這些方法的行為也不同。
鑑於這些結構及其介面的概念相似性,使用基於陣列或基於連結串列的策略都可以實現棧和佇列。然而,對於這些模型中的每一個,佇列的實現具有在棧的情況下不會出現的細微之處。這些差異起因於棧上的所有操作都發生在內部資料結構的同一端。在佇列中,排隊操作發生在一端,出隊操作發生在另一端。我們可以翻到以前的部落格中看看兩者的區別圖解:

C++抽象程式設計——STL(3)——queue 類

基於陣列實現的佇列

鑑於佇列中的動作不再侷限於佇列的一端,所以我們需要兩個索引來跟蹤佇列中的頭和尾位置。 因此私有例項變數看起來像這樣:

ValueType *array;
int capacity;
int head;
int tail;

在該表示中,head成員儲存下一個將要出佇列的元素的索引,tail成員下一個將要入隊的元素的索引。 在一個空的佇列中,很明顯,tail成員應該為0,以指示陣列中的初始位置,但head成員呢? 為了方便起見,通常的策略是將head設定為0。當以這種方式定義佇列時,使head和tail成員相等表示佇列為空。

即隊空等價於:

head == tail;

由此,我們很容易得出,我們初始化一個空佇列(也就是我們的建構函式)應該是這樣的:

template <typename ValueType>
Queue<ValueType>::Queue(){
    head = tail = 0;
}

雖然令人興奮的是認為入隊和出隊方法看起來幾乎完全像Stack類中的push和pop對應,但如果你只是嘗試複製現有程式碼,那麼你將遇到好幾個問題。 通常在程式設計中,我們通過繪製圖表可以確保在轉向實現之前,你必須準確瞭解佇列的執行方式。
要了解佇列的這種表示方法的工作原理,可以想象佇列代表一個等待線,類似於我們之前模擬離散事件中的一個等待線。一個新的客戶不定期到達並新增到佇列中。等待排隊的客戶定期在佇列的前端提供服務,之後他們完全離開等候線。佇列資料結構如何響應這些操作?

C++抽象程式設計——STL(3)——離散事件模擬與排隊問題
假設一開始的佇列為空佇列,那麼內部的結構應該是這樣的:
這裡寫圖片描述
假設現在有五個客戶到達,以字母A到E表示。這些客戶按順序排隊,從而產生以下配置:
這裡寫圖片描述
head域中的值0指示佇列中的第一個客戶儲存在陣列的位置0; tail的值5表示下一個客戶將被放置在位置5,很好。此刻假設你在佇列開始時交替地為客戶服務,然後新增一個新的客戶到最後。例如,客戶A出站,客戶F到達,導致以下情況:
這裡寫圖片描述
假設你在下一個客戶到達之前繼續為一位客戶提供服務,並且直到客戶J到達。佇列的內部結構如下所示:
這裡寫圖片描述
在這一點上,你有一個問題。此時,佇列中只有五個客戶,但你已經佔用了所有可用空間。tail指向超出陣列的末尾。另一方面,你現在在陣列的開頭有未使用的空間。因此,如果你代替增加的尾部,使其表示不存在的位置10,您可以從數列的末端“環繞”回到位置0,如下所示:
這裡寫圖片描述
此時,你就有空間將客戶K排入位置0,這將導致以下配置:
這裡寫圖片描述
如果允許佇列中的元素從陣列結尾開始迴圈,則活動元素始終從head索引向上延伸到tail索引之前的位置,如圖所示:
這裡寫圖片描述
因為陣列的首末端就像是連線在一起,程式設計師把這個表示稱為一個環形緩衝區(ring buffer)。

在你編寫enqueue和dequeue程式碼之前,你需要考慮的唯一剩餘問題是如何檢查佇列是否已滿。測試完整佇列比你想象的更復雜。要了解可能出現問題的原因,假設有三名客戶隨後到達。假設你排隊客戶L,M和N,顯然都在K的後面,資料結構如下所示:
這裡寫圖片描述
在這一點上,似乎有一個額外的空間。 不過,如果客戶O現在到達會發生什麼? 如果你遵循較早的入隊操作的邏輯,則最終導致以下配置中:
這裡寫圖片描述
佇列中的陣列現在已經完全滿了。不幸的是,此時head跟tail具有相同的值,就像在這個圖中一樣,佇列被認為是空的。沒有辦法從佇列結構本身的內容中得出兩個條件中的哪一個是空的或全滿的,因為每種情況下的資料值看起來都一樣。雖然你可以通過對空佇列採用不同的定義並編寫一些特殊情況程式碼來解決此問題,但最簡單的方法是將佇列中的元素數量限制為小於容量的數量,並在每次擴充套件陣列限制它到達尾部。

佇列類模板的環形緩衝區實現程式碼我們下篇部落格再寫上。值得注意的是,程式碼沒有顯式測試陣列索引,以檢視它們是否從陣列結尾開始迴圈。相反,程式碼使用%運算子自動計算正確的索引。使用餘數將計算結果減少到小的週期性整數範圍的技術是稱為模數運算,來段英文解釋(The technique of using remainders to reduce the result of a computation to a small, cyclical range of integers is an important mathematical technique called modular arithmetic.