1. 程式人生 > >數據結構與算法之美——隊列

數據結構與算法之美——隊列

enqueue 實現 條件 線程安全 pop 取數 線程池 過多 ont

如何理解隊列?

就像排隊買票,先來的先買,後來的人只能排在隊尾,不允許插隊。先進者先出,就是典型的隊列。

我們知道,棧的基本操作有兩個:入棧push()出棧pop(),隊列的基本操作也只有兩個:入隊enqueue(),在隊列尾部放一個數據;出隊dequeue(),從隊列頭部去一個數據。隊列和棧一樣,也是一種操作受限的線性表

順序隊列和鏈式隊列

和棧一樣,隊列也可以用數組來實現(順序隊列),也可以用鏈表來實現(鏈式隊列)。對於棧來說,我們只需要一個棧頂指針就可以了,對於隊列,需要兩個指針,head指向隊頭(指向的是隊頭元素的位置),tail指向隊尾(tail指向的是隊尾元素的下一個空白位置)。在入隊操作時,tail向後移動一個位置,出隊操作時,head向後移動一個位置,我們會發現,隨著不停的入隊和出隊操作,head和tail都會持續的向後移動,當tail移動到最末端,即便數組中有空閑空間,也無法再向隊列中添加數據了,這是就需要數據搬移

。但是,每次出隊操作相當於刪除數組中下標為0的數據,要搬移整個隊列中的元素,這樣出隊的時間復雜度就會從原來的O(1)變成O(n)。實際上,我們在出隊時可以不用搬移數據,如果沒有空閑時間了,我們只需要在入隊時集中觸發一次數據搬移,出隊函數dequeue()保持不變,對入隊函數enqueue()稍加改造:當tail指針移到最末端的時候,如果有新的數據入隊,將所有的數據搬移到0的位置就可以了。

基於鏈表實現的鏈式隊列,同樣需要兩個指針,在入隊時,tail->next = new_node, tail = tail->next;出隊時,head = head->next。

循環隊列

循環隊列,顧名思義,長得像是一個環,貪吃蛇遊戲結束時的蛇,在tail已經指向最後一個位置添加元素時直接添加到0的位置,通過這樣的方法可以避免數據搬移,但是循環隊列的代碼實現難度要大很多,避免bug的最關鍵在於,確定好隊空和隊滿的判定條件。

假設申請的數組容量為n,隊滿的條件:(tail+1)%n=head。

阻塞隊列和並發隊列

阻塞隊列其實就是在隊列的基礎上增加了阻塞操作。在隊列為空/隊列為滿的時候,從隊頭取數據/在隊尾插數據會被阻塞,直到隊列有了數據/隊列有了位置才能進行操作,然後再返回。(生產者-消費者模型)

在多線程情況下,會有多個線程同時操作隊列,這個時候就會存在線程安全問題,線程安全的隊列就叫做並發隊列,最簡單的實現方式就是在enqueue()和dequeue()方法上加鎖,但是鎖粒度大並發度會比較低,同一時刻只允許一個存或取操作。

隊列可以應用在任何有限資源池中,用於排隊請求,比如線程池、數據庫連接池等。基於鏈表的實現方式,可以實現一個無界隊列,但是可能導致過多的請求排隊等待,請求處理的響應時間過長。而基於數組實現的有界隊列,隊列的大小有限,所以線程池中的排隊請求超過隊列大小時,接下來的請求就會被拒絕,這種方式對響應時間敏感的系統來說更加合理。不過設置一個合理的隊列大小,是十分講究的,隊列太大導致等待的請求太對,隊列太小會導致無法充分利用系統資源、無法發揮最大性能。

數據結構與算法之美——隊列