在平常生活中,隊列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程序設計中,隊列也有着普遍的應用,例如計算機的任務調度系統、爲了削減高峯時期訂單請求的消息隊列等等。與棧相似,隊列也是屬於操做受限的線性表,不過隊列是隻容許在一端進行插入,在另外一端進行刪除。在其餘數據結構如樹的一些基本操做中(好比樹的廣度優先遍歷)也須要藉助隊列來實現,所以這裏咱們來看看隊列。html
隊列(queue)是隻容許在一端進行插入操做,而在另外一端進行刪除操做的線性表。它是一種先進先出(First In First Out)的線性表,簡稱FIFO。容許插入的一端稱爲隊尾,容許刪除的一端稱爲隊頭。node
(1)入隊(Enqueue):將一個數據元素插入隊尾;redis
(2)出隊(Dequeue):讀取隊頭節點數據並刪除該節點;算法
既然隊列也屬於特殊的線性表,那麼其實現也會有兩種形式:順序存儲結構和鏈式存儲結構。首先,對於Queue,咱們但願可以提供如下幾個方法供調用:數據庫
Queue<T>() 數組 |
建立一個空的隊列服務器 |
void Enqueue(T s) 數據結構 |
往隊列中添加一個新的元素架構 |
T Dequeue() 併發 |
移除隊列中最先添加的元素 |
bool IsEmpty() |
隊列是否爲空 |
int Size() |
隊列中元素的個數 |
與Stack不一樣,在隊列中咱們須要定義一個head隊頭「指針」和tail隊尾「指針」,當新元素入隊時tail+1,當老元素出隊時head+1。下面重點來看看Enqueue和Dequeue兩個方法的代碼實現。
(1)入隊:Enqueue
public void EnQueue(T item) { if (Size == items.Length) { // 擴大數組容量 ResizeCapacity(items.Length * 2); } items[tail] = item; tail++; size++; }
新元素入隊後,tail隊尾指針向前移動指向下一個新元素要插入的位置;這裏仍然模仿.NET中的實現,在數組容量不足時及時進行擴容以容納新元素入隊。
(2)出隊:Dequeue
public T DeQueue() { if (Size == 0) { return default(T); } T item = items[head]; items[head] = default(T); head++; if (head > 0 && Size == items.Length / 4) { // 縮小數組容量 ResizeCapacity(items.Length / 2); } size--; return item; }
在對老元素進行出隊操做時,首先取得head指針所指向的老元素,而後將head指針向前移動一位指向下一個將出隊的老元素。這裏將要出隊的元素所在數組中的位置重置爲默認值。最後判斷容量是否太小,若是是則進行數組容量的縮小。
下面是完整的隊列模擬實現代碼,僅供參考,這裏就再也不作基本功能測試了,有興趣的讀者能夠自行測試:
/// <summary> /// 基於數組的隊列實現 /// </summary> /// <typeparam name="T">類型</typeparam> public class MyArrayQueue<T> { private T[] items; private int size; private int head; private int tail; public MyArrayQueue(int capacity) { this.items = new T[capacity]; this.size = 0; this.head = this.tail = 0; } /// <summary> /// 入隊 /// </summary> /// <param name="item">入隊元素</param> public void EnQueue(T item) { if (Size == items.Length) { // 擴大數組容量 ResizeCapacity(items.Length * 2); } items[tail] = item; tail++; size++; } /// <summary>v /// 出隊 /// </summary> /// <returns>出隊元素</returns> public T DeQueue() { if (Size == 0) { return default(T); } T item = items[head]; items[head] = default(T); head++; if (head > 0 && Size == items.Length / 4) { // 縮小數組容量 ResizeCapacity(items.Length / 2); } size--; return item; } /// <summary> /// 重置數組大小 /// </summary> /// <param name="newCapacity">新的容量</param> private void ResizeCapacity(int newCapacity) { T[] newItems = new T[newCapacity]; int index = 0; if (newCapacity > items.Length) { for (int i = 0; i < items.Length; i++) { newItems[index++] = items[i]; } } else { for (int i = 0; i < items.Length; i++) { if (!items[i].Equals(default(T))) { newItems[index++] = items[i]; } } head = tail = 0; } items = newItems; } /// <summary> /// 棧是否爲空 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.size == 0; } /// <summary> /// 棧中節點個數 /// </summary> public int Size { get { return this.size; } } }
跟Stack鏈式存儲結構不一樣,在Queue鏈式存儲結構中須要設置兩個節點:一個head隊頭節點,一個tail隊尾節點。如今咱們來看看在鏈式存儲結構中,如何實現Enqueue與Dequeue兩個方法。
(1)入隊:Enqueue
public void EnQueue(T item) { Node<T> oldLastNode = tail; tail = new Node<T>(); tail.Item = item; if(IsEmpty()) { head = tail; } else { oldLastNode.Next = tail; } size++; }
入隊操做就是在鏈表的末尾插入一個新節點,將原來的尾節點的Next指針指向新節點。
(2)出隊:Dequeue
public T DeQueue() { T result = head.Item; head = head.Next; size--; if(IsEmpty()) { tail = null; } return result; }
出隊操做本質就是返回鏈表中的第一個元素即頭結點,這裏能夠考慮到若是隊列爲空,將tail和head設爲null以加快垃圾回收。
模擬的隊列鏈式存儲結構的完整代碼以下,這裏就再也不作基本功能測試了,有興趣的讀者能夠自行測試:
/// <summary> /// 基於鏈表的隊列節點 /// </summary> /// <typeparam name="T"></typeparam> public class Node<T> { public T Item { get; set; } public Node<T> Next { get; set; } public Node(T item) { this.Item = item; } public Node() { } } /// <summary> /// 基於鏈表的隊列實現 /// </summary> /// <typeparam name="T">類型</typeparam> public class MyLinkQueue<T> { private Node<T> head; private Node<T> tail; private int size; public MyLinkQueue() { this.head = null; this.tail = null; this.size = 0; } /// <summary> /// 入隊操做 /// </summary> /// <param name="node">節點元素</param> public void EnQueue(T item) { Node<T> oldLastNode = tail; tail = new Node<T>(); tail.Item = item; if(IsEmpty()) { head = tail; } else { oldLastNode.Next = tail; } size++; } /// <summary> /// 出隊操做 /// </summary> /// <returns>出隊元素</returns> public T DeQueue() { T result = head.Item; head = head.Next; size--; if(IsEmpty()) { tail = null; } return result; } /// <summary> /// 是否爲空隊列 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.size == 0; } /// <summary> /// 隊列中節點個數 /// </summary> public int Size { get { return this.size; } } }
首先,咱們來看看下面的情景,在數組容量固定的狀況下,隊頭指針以前有空閒的位置,而隊尾指針卻已經指向了末尾,這時再插入一個元素時,隊尾指針會指向哪裏?
圖1
從圖中能夠看出,目前若是接着入隊的話,因數組末尾元素已經佔用,再向後加,就會產生數組越界的錯誤,可實際上,咱們的隊列在下標爲0和1的地方仍是空閒的。咱們把這種現象叫作「假溢出」。現實當中,你上了公交車,發現前排有兩個空座位,然後排全部座位都已經坐滿,你會怎麼作?立馬下車,並對本身說,後面沒座了,我等下一輛?沒有這麼笨的人,前面有座位,固然也是能夠坐的,除非坐滿了,纔會考慮下一輛。
因此解決假溢出的辦法就是後面滿了,就再從頭開始,也就是頭尾相接的循環。咱們把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。在循環隊列中須要注意的幾個問題是:
(1)入隊與出隊的索引位置如何肯定?
這裏咱們能夠藉助%運算對head和tail兩個指針進行位置肯定,實現方式以下所示:
// 移動隊尾指針 tail = (tail + 1) % items.Length; // 移動隊頭指針 head = (head + 1) % items.Length;
(2)在隊列容量固定時如何判斷隊列空仍是隊列滿?
①設置一個標誌變量flag,當head==tail,且flag=0時爲隊列空,當head==tail,且flag=1時爲隊列滿。
②當隊列空時,條件就是head=tail,當隊列滿時,咱們修改其條件,保留一個元素空間。也就是說,隊列滿時,數組中還有一個空閒單元。以下圖所示:
圖2
從上圖能夠看出,因爲tail可能比head大,也可能比head小,因此儘管它們只相差一個位置時就是滿的狀況,但也多是相差整整一圈。因此若隊列的最大尺寸爲QueueSize,那麼隊列滿的條件是 (tail+1)%QueueSize==head(取模「%」的目的就是爲了整合tail與head大小爲一個問題)。好比上面這個例子,QueueSize=5,圖中的左邊front=0,而rear=4,(4+1)%5=0,因此此時隊列滿。再好比圖中的右邊,front=2而rear=1。(1+1)%5=2,因此此時隊列也是滿的。
(3)因爲tail可能比head大,也可能比head小,那麼隊列的長度如何計算?
當tail>head時,此時隊列的長度爲tail-head。但當tail<head時,隊列長度分爲兩段,一段是QueueSize-head,另外一段是0+tail,加在一塊兒,隊列長度爲tail-head+QueueSize。所以通用的計算隊列長度公式爲:(tail-head+QueueSize)%QueueSize。
隊列在實際開發中應用得很是普遍,這裏來看看在互聯網系統中常見的一個應用場景:消息隊列。「消息」是在兩臺計算機間傳送的數據單位。消息能夠很是簡單,例如只包含文本字符串;也能夠更復雜,可能包含嵌入對象。消息被髮送到隊列中,「消息隊列」是在消息的傳輸過程當中保存消息的容器。
在目前普遍的Web應用中,都會出現一種場景:在某一個時刻,網站會迎來一個用戶請求的高峯期(好比:淘寶的雙十一購物狂歡節,12306的春運搶票節等),通常的設計中,用戶的請求都會被直接寫入數據庫或文件中,在高併發的情形下會對數據庫服務器或文件服務器形成巨大的壓力,同時呢,也使響應延遲加重。這也說明了,爲何咱們當時那麼地抱怨和吐槽這些網站的響應速度了。當時2011年的京東圖書促銷,曾一直出如今購物車中點擊「購買」按鈕後一直是「Service is too busy」,其實就是由於當時的併發訪問量過大,超過了系統的最大負載能力。固然,後邊,劉強東臨時購買了很多服務器進行擴展以求加強處理併發請求的能力,還請了信息部的人員「喝茶」,如今京東已是超大型的網上商城了,我也有同窗在京東成都研究院工做了。
從京東當年的「Service is too busy」不難看出,高併發的用戶請求是網站成長過程當中必不可少的過程,也是一個必需要解決的難題。在衆多的實踐當中,除了增長服務器數量配置服務器集羣實現伸縮性架構設計以外,異步操做也被普遍採用。而異步操做中最核心的就是使用消息隊列,經過消息隊列,將短期高併發產生的事務消息存儲在消息隊列中,從而削平高峯期的併發事務,改善網站系統的性能。在京東之類的電子商務網站促銷活動中,合理地使用消息隊列,能夠有效地抵禦促銷活動剛開始就開始大量涌入的訂單對系統形成的衝擊。
雖然隊列有順序存儲和鏈式存儲兩種存儲方式,但在.NET中使用的是順序存儲,它所對應的集合類是System.Collections.Queue與System.Collections.Generic.Queue<T>,二者結構相同,不一樣之處僅在於前者是非泛型版本,後者是泛型版本的隊列。它們都屬於循環隊列,這裏咱們經過Reflector來重點看看泛型版本的實現。
咱們來看看在.NET中的Queue<T>是如何實現入隊和出隊操做的。首先來看看入隊Enqueue方法:
public void Enqueue(T item) { if (this._size == this._array.Length) { int capacity = (this._array.Length * 200) / 100; if (capacity < (this._array.Length + 4)) { capacity = this._array.Length + 4; } this.SetCapacity(capacity); } this._array[this._tail] = item; this._tail = (this._tail + 1) % this._array.Length; this._size++; this._version++; }
能夠看出,與咱們以前所實現的Enqueue方法相似,首先判斷了隊列是否滿了,若是滿了則進行擴容,不一樣之處在咱們是直接*2倍,這裏是在原有容量基礎上+4。因爲是循環隊列,對tail指針使用了%運算來肯定下一個入隊位置。
咱們再來看看Dequeue方法時怎麼實現的:
public T Dequeue() { if (this._size == 0) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue); } T local = this._array[this._head]; this._array[this._head] = default(T); this._head = (this._head + 1) % this._array.Length; this._size--; this._version++; return local; }
一樣,與以前相似,不一樣之處在於判斷隊空時這裏直接拋了異常,其次因爲是循環隊列,head指針也使用了%運算來肯定下一個出隊元素的位置。
(1)程傑,《大話數據結構》
(2)陳廣,《數據結構(C#語言描述)》
(3)段恩澤,《數據結構(C#語言版)》
(4)yangecnu,《淺談算法與數據結構:—棧和隊列》
(5)李智慧,《大型網站技術架構:核心原理與案例分析》
(6)Edison Chou,《Redis初探:消息隊列》