文章來源:http://blog.seclibs.com/數據...算法
上一篇文章說了一種「功能受限」的順序表——棧,如今再來講一個 「功能受限」的順序表 ——隊列(queue)。數組
隊列也是一個經常使用的數據結構,在大部分資源有限的狀況下,當沒有空閒資源的時候,基本上都是使用隊列這種數據結構來實現請求排隊的。安全
隊列,顧名思義,就是排的一條隊,好比在買票的時候排的一條隊伍,先來的先買,後來的後買,不容許插隊,也就是先進先出的方式,棧是後進先出的方式。數據結構
棧支持入棧(push)和出棧(pop)兩種操做,隊列也是相似的,支持入隊(enqueue)和出隊(dequeue)兩種操做,入隊就是在尾部追加一個數據,出隊就是在頭部取走一個數據。多線程
隊列做爲一種很是基礎的數據結構,應用是很是普遍的,特別是一些具備某些額外特性的隊列,好比循環隊列、阻塞隊列、併發隊列。它們在不少偏底層系統、框架、中間件的開發中,起着關鍵性的做用。併發
隊列在實現上跟棧也是相似的,可使用數組或鏈表來進行實現,使用數組實現的叫作順序隊列,使用鏈表實現的叫作鏈式隊列。可是棧只須要一個棧頂指針top就能夠了,隊列則須要頭部head指針和尾部tail指針兩個來標識。框架
好比說a、b、c、d四個數據入隊之後,head指針將指向下標爲0的位置,tail指針指向下標爲4的位置性能
當進行兩次出隊的操做後,head指針將指向下標爲2的位置spa
那若是不斷的日後走,一直到下標爲7後,不能再繼續增長了怎麼辦,那就可使用數組實現時的數據移動的方式,將數據向前遷移,那若是每進行一次出棧就遷移一次,那不就至關於一直在刪除下標爲0的元素,而後將整個隊列都拷貝一邊,操做的時間複雜度就爲O(n)了呀,其實徹底能夠用另外一種思路,在每次出棧的時候不進行遷移數據的操做,而是在數據入棧的時候,發現沒有位置了,再將全部的數據向前遷移,這樣時間複雜度就變成了O(1),即隊列未滿時,直接入隊,時間複雜度爲O(1),當隊列已滿時,也就是tail=n時,時間複雜度變爲了O(n),若是將最後一次的n次遷移,平均到前面的n-1次上,那它們的均攤時間複雜度就變爲了O(1)。線程
上面說的是順序隊列,對於鏈式隊列來講,就更加容易了,由於不會涉及到隊滿的狀況了,只須要處理好指針的移動問題就能夠了。
可是它還有一個比較嚴重的問題,由於它的長度是能夠無限長的,因此當請求的任務特別多時,後面的任務將會等待至關長的時間,這對於對時間比較敏感的系統來講,是會出問題的。
順序隊列由於大小有限,若是請求的任務超過了隊列長度,將會直接將後續的任務拒絕掉,這對於對時間比較敏感的系統來講,是比較合理的方式,可是如何設置隊列的大小又是一個問題,隊列太大致使等待的請求太多,隊列過小會致使沒法充分利用系統資源、發揮最大性能。
前面順序隊列的實現時,當tail=n時,會有大量數據遷移的操做,若是使用循環隊列就能夠完美的解決這個問題了。
循環隊列,顧名思義,就是將隊列的首尾相連,組成環狀
就拿上圖來講,這個循環隊列n=8,當前head=4,tail=7,當咱們再插入一個數據的時候,會插入到下標爲7的位置,tail將會向後挪一個,到達了0的位置,再插入數據,依舊像上面說的同樣執行,tail將指向了下標爲1的地方。
經過這樣的方法,就節省了不少數據遷移的操做,可是在代碼實現上就會更加的複雜,其中最關鍵的地方仍是肯定好隊空和隊滿的斷定條件。
隊列爲空的條件仍然是head=tail,那什麼狀況下是隊滿呢?
在通常狀況下,很明顯的能夠看出來,當tail+1=head的時候,即爲隊滿
可是還有一個狀況比較特殊,並不適用這個規則,就是當head=0,tail=7時,此時再插入一個數據的時候,觀察圖能夠發現,此時tail將等於head,可是tail=head的時候,是咱們所說的隊列爲空的時候,這也就是爲何循環隊列會浪費一個數組空間的緣由;這個狀況明顯已經屬於隊滿的狀況了,若是套用上面的公式,tail+1=8,head=0,並不知足咱們前面所提到的常規狀況。
那麼如何才能將tail+1在最大的狀況下等於0呢,這裏能夠引入取餘的方法。當tail=7時,tail+1=8,將它與n=8取餘,8%8=0,就能夠知足這個要求了,通用的公式就變成了(tail+1)%n=head,那通常狀況滿不知足這個公式呢,由於在通常狀況下,tail+1都是小於n的,只有在上面所說的特殊狀況時tail+1=n,因此無論怎麼取餘都是tail+1自己,也就是等於head了,因此隊滿的條件爲(tail+1)%n=head。
上面在解釋的時候也說了循環隊列會浪費一個數組空間的緣由,那若是咱們專門用一個記錄隊列大小的值size,當這個值與數組大小相等時,表示隊列已滿,當tail達到最底時,size不等於數組大小時,tail就指向數組第一個位置。當出隊時,size- -,入隊時size++。
這種的記錄方法跟以前實現棧時候的思路相似,可是這樣依舊會新建立一個內存空間來存放size值,最後消耗的大小是同樣的。
到這裏基礎的隊列就說完了,可是這樣基礎的隊列在實際的業務開發中都不大會直接用到,經常使用的是一些比較特殊的隊列,好比阻塞隊列和併發隊列。
阻塞隊列其實就是在正常的隊列操做中加上了阻塞操做,當隊列爲空時,在對頭取數據的時候,會被阻塞,由於隊列中尚未任何能夠取的數據,直到隊列中有數據時,纔會取出數據並返回;若是隊列已經滿了,那插入數據將會被阻塞,直到有空閒位置後,將數據插入纔會返回。
經過阻塞隊列,很輕鬆就能夠實現「生產者 – 消費者模型」,它能夠有效的協調生產與消費的速度,當生產過快的時候,隊列就沒法再加入了,生產者就會被阻塞,直到出現空餘位置的時候纔會繼續生產。
那咱們設想一個場景,若是消費者方面已經不須要這個東西了,那生產方仍是在不斷的往進放東西,這樣就會形成極大的浪費,這樣的狀況讓我想起了咱們如今全面深化改革的一點——供給側結構性改革,再也不讓供給方無限的供給了,要在可能快出現問題的時候,便讓供給方減小供給,以便讓資源獲得更好的利用。
插了點題外話,咱們繼續說阻塞隊列,爲了讓數據更加高效的處理,咱們還能夠協調供給者和消費者的的數量。
那併發隊列又是什麼,併發隊列就是在多線程的狀況下,因爲有多個線程同時操做,可能會存在一些安全問題,爲了實現線程安全的隊列就是併發隊列,與阻塞隊列相似,併發隊列就是在入隊和出隊上加鎖,同一時刻僅容許一個存或取的操做,可是鎖粒度大併發度就會比較低,這個也是一個須要協調的東西。
實際上,基於數組的循環隊列,利用 CAS 原子操做 (Compare & Set,或是 Compare & Swap),能夠實現很是高效的併發隊列。這也是循環隊列比鏈式隊列應用更加普遍的緣由。
參考文檔
極客時間-數據結構與算法之美
文章首發公衆號和我的博客
公衆號:無意的夢囈(wuxinmengyi)