1. 程式人生 > >資料結構基礎溫故-3.佇列

資料結構基礎溫故-3.佇列

在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如樹的一些基本操作中(比如樹的廣度優先遍歷)也需要藉助佇列來實現,因此這裡我們來看看佇列。

一、佇列的概念及操作

1.1 佇列的基本特徵

佇列(queue)是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。它是一種先進先出(First In First Out)的線性表,簡稱FIFO

。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。

1.2 佇列的基本操作

  (1)入隊(Enqueue):將一個數據元素插入隊尾;

  (2)出隊(Dequeue):讀取隊頭節點資料並刪除該節點;

二、佇列的基本實現

  既然佇列也屬於特殊的線性表,那麼其實現也會有兩種形式:順序儲存結構和鏈式儲存結構。首先,對於Queue,我們希望能夠提供以下幾個方法供呼叫:

Queue<T>()

建立一個空的佇列

void Enqueue(T s)

往佇列中新增一個新的元素

T Dequeue()

移除佇列中最早新增的元素

bool IsEmpty()

佇列是否為空

int Size()

佇列中元素的個數

2.1 佇列的順序儲存實現

  與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;
            }
        }
    }
View Code

2.2 佇列的鏈式儲存實現

  跟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;
            }
        }
    }
View Code

2.3 迴圈佇列

  首先,我們來看看下面的情景,在陣列容量固定的情況下,隊頭指標之前有空閒的位置,而隊尾指標卻已經指向了末尾,這時再插入一個元素時,隊尾指標會指向哪裡?

圖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!

  從京東當年的“Service is too busy”不難看出,高併發的使用者請求是網站成長過程中必不可少的過程,也是一個必須要解決的難題。在眾多的實踐當中,除了增加伺服器數量配置伺服器叢集實現伸縮性架構設計之外,非同步操作也被廣泛採用。而非同步操作中最核心的就是使用訊息佇列,通過訊息佇列,將短時間高併發產生的事務訊息儲存在訊息佇列中,從而削平高峰期的併發事務,改善網站系統的效能。在京東之類的電子商務網站促銷活動中,合理地使用訊息佇列,可以有效地抵禦促銷活動剛開始就開始大量湧入的訂單對系統造成的衝擊

message queue

四、.NET中的Queue<T>

  雖然佇列有順序儲存和鏈式儲存兩種儲存方式,但在.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#語言版)》

(5)李智慧,《大型網站技術架構:核心原理與案例分析》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

資料結構基礎-3.佇列

在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如

資料結構基礎-5.圖(中):圖的遍歷演算法

上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用

資料結構基礎-1.線性表(中)

在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。 一、單鏈表基礎 1.1 單鏈表的

資料結構基礎-6.查詢(上):基本查詢與樹表查詢

只要你開啟電腦,就會涉及到查詢技術。如炒股軟體中查股票資訊、硬碟檔案中找照片、在光碟中搜DVD,甚至玩遊戲時在記憶體中查詢攻擊力、魅力值等資料修改用來作弊等,都要涉及到查詢。當然,在網際網路上查詢資訊就更加是家常便飯。查詢是計算機應用中最常用的操作之一,也是許多程式中最耗時的一部分,查詢方法的優劣對於系統的執

資料結構基礎-4.樹與二叉樹(下)

上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過

資料結構基礎-6.查詢(下):雜湊表

雜湊(雜湊)技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖示表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。雜湊技術最適合的求解問題是查詢與給定值相等的記錄。

資料結構基礎-5.圖(中):最小生成樹演算法

圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成樹 1.1 生成樹   對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過

資料結構基礎-5.圖(上):圖的基本概念

前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的關係。圖(Graph)是一種複雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關係是任意的。現實生活中的很多

資料結構基礎-4.樹與二叉樹(中)

在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系

資料結構基礎-5.圖(下):最短路徑

圖的最重要的應用之一就是在交通運輸和通訊網路中尋找最短路徑。例如在交通網路中經常會遇到這樣的問題:兩地之間是否有公路可通;在有多條公路可通的情況下,哪一條路徑是最短的等等。這就是帶權圖中求最短路徑的問題,此時路徑的長度不再是路徑上邊的數目總和,而是路徑上的邊所帶權值的和。帶權圖分為無向帶權圖和有向帶權圖,但如

資料結構基礎-4.樹與二叉樹(上)

前面所討論的線性表元素之間都是一對一的關係,今天我們所看到的結構各元素之間卻是一對多的關係。樹在計算機中有著廣泛的應用,甚至在計算機的日常使用中,也可以看到樹形結構的身影,如下圖所示的Windows資源管理器和應用程式的選單都屬於樹形結構。樹形結構是一種典型的非線性結構,除了用於表示相鄰關係外,還可以表示層次

資料結構基礎-2.棧

現實生活中的事情往往都能總結歸納成一定的資料結構,例如餐館中餐盤的堆疊和使用,羽毛球筒裡裝的羽毛球等都是典型的棧結構。而在.NET中,值型別線上程棧上進行分配,引用型別在託管堆上進行分配,本文所說的“棧”正是這種資料結構。棧和佇列都是常用的資料結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制

資料結構基礎-1.線性表(下)

在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。 一、迴圈連結串列基礎 1.1 迴圈連結串列節點結構   迴圈連結串列和單鏈表的

資料結構基礎-7.排序

排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為按關鍵字“有序”的記錄序列。如何進行排序,特別是高效率地進行排序時計算機工作者學習和研究的重要課題之一。排序有內部排序和外部排序之分,若整個排序過程不需要訪問外存便能完成,則稱此類排序為內部排序,反之則為外部排序。本篇主

資料結構基礎02-棧和佇列

本文系列 資料結構基礎01-基本概念和術語/線性表 資料結構基礎02-棧和佇列 棧 棧(Stack):所有的插入和刪除只在表的一端進行的線性表,即是一種操作受限的線性表。在表中,允許插入和刪除的一端叫棧頂(top),不允許插 入和刪除的另一端叫棧底(bottom)。 特點

野生前端的資料結構基礎練習(3)——連結串列

連結串列的基本知識 特點: 連結串列由節點組成,每個節點增加一個物件的引用指向它的後繼節點。連結串列也就是將一個線性錶轉換為一個儲存空間上不連續,而在抽象層面可連續訪問的表。 用途: 更快的插入和刪除,因為只需要操作插入刪除位置相鄰元素即可,如果線上性表中,操作中間位置的元素後,後續的元素位置都需要調整

棧與佇列-順序棧與鏈棧類模板的實現(資料結構基礎3周)

這是用C++編寫的棧的類模板的實現,包括順序棧和鏈棧,並進行了簡單的測試。 程式碼中srrStack類和lnkStack類均繼承於Stack類, Stack類可以看成是棧的邏輯結構(ADT抽象資料型別,Abstract Data Type)。注意這裡實現是棧與

資料結構基礎佇列

轉自:http://www.cnblogs.com/edisonchou/p/4620379.html   佇列 在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、

軟考:資料結構基礎——迴圈佇列C語言實現

  迴圈佇列得實現: 1.     在入隊和出隊時,我們通過      q->rear = (q->rear +1)%MAX_LENTH 來實現迴圈入隊     q

資料結構與演算法(3)—— 佇列(java)

1 陣列實現的佇列 public class ArrayQueue { private String[] items; private int n=0; //陣列的大小 private int head