先進者先出,這就是典型的「佇列」。
佇列跟棧非常相似,支援的操作也很有限,也是一種操作受限的線性表資料結構。
最基本的操作也是兩個:
跟棧一樣,佇列可以用陣列來實現,也可以用鍊錶來實現。用陣列實現的棧叫作順序棧,用鍊錶實現的棧叫作鏈式棧。同樣,用陣列實現的佇列叫作順序佇列,用鍊錶實現的佇列叫作鏈式佇列。
2.1、基於陣列的實現
佇列需要兩個指標:乙個是 head 指標,指向隊頭;乙個是 tail 指標,指向隊尾。
// 用陣列實現的佇列
public
class
arrayqueue
// 入隊
public
boolean
enqueue
(string item)
// 出隊
public string dequeue()
}
但是隨著不停地進行入隊、出隊操作,head 和 tail 都會持續往後移動。當 tail 移動到最右邊,即使陣列中還有空閒空間,也無法繼續往佇列中新增資料了。這個問題該如何解決呢?
用資料搬移!但是,每次進行出隊操作都相當於刪除陣列下標為 0 的資料,要搬移整個佇列中的資料,這樣出隊操作的時間複雜度就會從原來的 o(1) 變為 o(n)。能不能優化一下呢?
實際上,我們在出隊時可以不用搬移資料。如果沒有空閒空間了,我們只需要在入隊時,再集中觸發一次資料的搬移操作。
借助這個思想,出隊函式 dequeue() 保持不變,我們稍加改造一下入隊函式 enqueue() 的實現,就可以輕鬆解決剛才的問題了。
// 入隊操作,將 item 放入隊尾
public
boolean
enqueue
(string item)
// 搬移完之後重新更新 head 和 tail
tail -= head;
head =0;
}
items[tail]
= item;
++tail;
return
true
;}
2.2、基於鍊錶實現
基於鍊錶的實現,我們同樣需要兩個指標:head 指標和 tail 指標。它們分別指向鍊錶的第乙個結點和最後乙個結點。如圖所示,入隊時,tail->next= new_node, tail = tail->next;出隊時,head = head->next。
2.3、迴圈佇列
用陣列來實現佇列的時候,在 tail==n 時,會有資料搬移操作,這樣入隊操作效能就會受到影響。那有沒有辦法能夠避免資料搬移呢?我們來看看迴圈佇列的解決思路。
迴圈佇列,顧名思義,它長得像乙個環。原本陣列是有頭有尾的,是一條直線。現在我們把首尾相連,扳成了乙個環。我畫了一張圖,你可以直觀地感受一下。
迴圈佇列的**實現難度要比前面講的非迴圈佇列難多了。要想寫出沒有 bug 的迴圈佇列的實現**,我個人覺得,最關鍵的是,確定好隊空和隊滿的判定條件。
在用陣列實現的非迴圈佇列中,隊滿的判斷條件是 tail == n,隊空的判斷條件是 head == tail。那針對迴圈佇列,如何判斷隊空和隊滿呢?
隊列為空的判斷條件仍然是 head == tail。
隊滿時,(tail+1)%n=head
當佇列滿時,圖中的 tail 指向的位置實際上是沒有儲存資料的。所以,迴圈佇列會浪費乙個陣列的儲存空間。
public
class
circularqueue
// 入隊
public
boolean
enqueue
(string item)
// 出隊
public string dequeue()
}
3.1、阻塞佇列
阻塞佇列其實就是在佇列基礎上增加了阻塞操作。簡單來說,就是在隊列為空的時候,從隊頭取資料會被阻塞。因為此時還沒有資料可取,直到佇列中有了資料才能返回;如果佇列已經滿了,那麼插入資料的操作就會被阻塞,直到佇列中有空閒位置後再插入資料,然後再返回。
上述的定義就是乙個「生產者 - 消費者模型」!是的,我們可以使用阻塞佇列,輕鬆實現乙個「生產者 - 消費者模型」!
這種基於阻塞佇列實現的「生產者 - 消費者模型」,可以有效地協調生產和消費的速度。當「生產者」生產資料的速度過快,「消費者」來不及消費時,儲存資料的佇列很快就會滿了。這個時候,生產者就阻塞等待,直到「消費者」消費了資料,「生產者」才會被喚醒繼續「生產」。
而且不僅如此,基於阻塞佇列,我們還可以通過協調「生產者」和「消費者」的個數,來提高資料的處理效率。比如前面的例子,我們可以多配置幾個「消費者」,來應對乙個「生產者」。
3.2、併發佇列
執行緒安全的佇列我們叫作併發佇列。最簡單直接的實現方式是直接在 enqueue()、dequeue() 方法上加鎖,但是鎖粒度大併發度會比較低,同一時刻僅允許乙個存或者取操作。實際上,基於陣列的迴圈佇列,利用 cas 原子操作,可以實現非常高效的併發佇列。這也是迴圈佇列比鏈式佇列應用更加廣泛的原因。
3.3、如何選擇佇列的實現形式
我們希望公平地處理每個排隊的請求,先進者先服務,所以佇列這種資料結構很適合來儲存排隊請求。我們前面說過,佇列有基於鍊錶和基於陣列這兩種實現方式。這兩種實現方式對於排隊請求又有什麼區別呢?
基於鍊錶的實現方式,可以實現乙個支援無限排隊的無界佇列(unbounded queue),但是可能會導致過多的請求排隊等待,請求處理的響應時間過長。所以,針對響應時間比較敏感的系統,基於鍊錶實現的無限排隊的執行緒池是不合適的。
而基於陣列實現的有界佇列(bounded queue),佇列的大小有限,所以執行緒池中排隊的請求超過佇列大小時,接下來的請求就會被拒絕,這種方式對響應時間敏感的系統來說,就相對更加合理。不過,設定乙個合理的佇列大小,也是非常有講究的。佇列太大導致等待的請求太多,佇列太小會導致無法充分利用系統資源、發揮最大效能。
資料結構與演算法 基礎篇 佇列
佇列是一種操作受限的線性表資料結構。佇列最大的特點就是先進先出。最基本的操作 入隊 enqueue 放乙個資料到佇列尾部 出隊 dequeue 從佇列頭部取乙個元素。用陣列實現的佇列叫順序佇列,用鍊錶實現的佇列叫鏈式佇列。佇列需要兩個指標 乙個是 head 指標,指向隊頭 乙個是 tail 指標,指...
資料結構與演算法解析 棧篇
後進者先出,先進者後出,這就是典型的 棧 結構。從棧的操作特性上來看,棧是一種 操作受限 的線性表,只允許在一端插入和刪除資料。但這種受限,也控制了出錯的概率。當某個資料集合只涉及在一端插入和刪除資料,並且滿足後進先出 先進後出的特性,我們就應該首選 棧 這種資料結構。從棧的定義看,棧主要包含兩個操...
資料結構與演算法解析 「遞迴」篇
遞迴,在數學與電腦科學中,是指在函式的定義中使用函式自身的方法。也就是說,遞迴演算法是一種直接或者間接呼叫自身函式或者方法的演算法。遞迴是一種應用非常廣泛的演算法 或者程式設計技巧 很多資料結構和演算法的編碼實現都要用到遞迴,比如 dfs 深度優先搜尋 前中後序二叉樹遍歷等等。去的過程叫 遞 回來的...