1. 程式人生 > >資料結構之——佇列

資料結構之——佇列

1. 佇列的特點?

  • 佇列是一種先進先出的資料結構。類似於我們日常排隊購物,先到的人排在前面,自然優先被服務。棧有出棧和入棧兩個基本操作,佇列也只有兩個基本操作,從隊尾入隊 enqueue,從隊頭出隊 dequene。
    佇列和棧

  • 佇列可以用陣列或者連結串列實現,用陣列實現的佇列叫作順序佇列,用連結串列實現的佇列叫作鏈式佇列

2. 順序佇列?

  • C++ 實現簡易順序佇列。首先,定義一個固定長度的陣列,然後用兩個陣列的索引值分別表徵佇列的隊頭和隊尾,入隊則隊尾索引值增 1,出隊則隊頭索引值增 1。

順序佇列

  • 有一種特殊情況,隊尾元素一直進隊到達了陣列上限,但前面有元素出隊,這時候,佇列其實還有儲存空間沒有利用,我們就將佇列的資料都往前平移,騰出一些空間來。

資料搬移

class ArrayQueue
{
    private:
        int *data;      // 順序佇列
        int head;       // 佇列頭索引值
        int tail;       // 佇列尾索引值
        int len;        // 佇列的大小

    public:
        // 初始化佇列,申請一個大小為 n 的陣列
        ArrayQueue(int n)
        {
            data = new int[n];
            head = 0;
            tail =
0; len = n; } bool enqueue(int item) { // 佇列頭索引值為零且佇列尾索引值等於陣列長度,佇列空間已滿,返回 false,入隊失敗 if (tail == len && head == 0) { cout << "queue is full" << endl; return false; } else
{// 若只有佇列尾索引值等於陣列長度,向前平移資料 if (tail == len && head != 0) { int num = tail - head; for (int i = 0; i < num; i++) { data[i] = data[len - num + i]; } head = 0; tail = num; } // 佇列空間未滿,入隊,佇列尾索引值加 1 data[tail] = item; tail++; return true; } } int dequeue() { // 佇列頭等於佇列尾,佇列為空,返回 -1,出隊失敗 if (head == tail) { cout << "queue is empty" << endl; return -1; } // 佇列非空,隊頭元素出隊,佇列頭索引值加 1 else { int item = data[head]; head++; return item; } } };

2. 鏈式佇列?

  • C++ 實現簡易鏈式佇列。定義頭指標指向哨兵結點,尾指標指向最後一個結點,入隊則在鏈尾插入一個新結點,出隊則刪除哨兵結點後的第一個結點。

 鏈式佇列

// 單向連結串列
struct linked_list
{
    int data;
    linked_list *next;
};

class ListQueue
{
    private:
        linked_list *head;  // 連結串列佇列的頭指標
        linked_list *tail;  // 連結串列佇列的尾指標
        int num;            // 佇列中元素個數

    public:
        // 初始化佇列,增加一個哨兵結點,方便連結串列操作
        ListQueue()
        {
            head = new linked_list;
            head->data = -1;
            head->next = NULL;
            tail = head; // 頭指標和尾指標都指向哨兵
            num = 0;
        }

        // 入隊,在鏈尾插入新結點
        bool enqueue(int item)
        {
            linked_list *node = new linked_list;
            if (node)
            {
                node->data = item;
                node->next = NULL;
                tail->next = node;  // 新增新結點
                tail = node; // 更新尾指標
                num++;
                return true;
            }
            else // 記憶體不足,無法插入新結點,返回 false
            {
                return false;
            }
        }

        int dequeue()
        {
            // 佇列為空,返回 -1,出隊失敗
            //if (num == 0)
            if (head == tail)
            {
                cout << "queue is empty" << endl;
                return -1;
            }
            // 出隊,彈出哨兵結點後第一個結點的值,並刪除結點
            else
            {
                if (head->next == tail) // 當佇列只有一個結點時,需要更改尾節點指向哨兵
                {
                    tail = head;
                }

                int item = head->next->data;
                num--;
                linked_list * node = head->next;
                head->next = node->next;
                delete node;
                return item;
            }
        }
};

4. 迴圈佇列?

  • 用陣列來實現佇列,當隊尾到達陣列上限時,我們需要進行資料搬移來利用實際上處於空閒的記憶體空間,但這樣入隊操作的效能就會受到影響。
  • 為了避免進行資料搬移,我們引入迴圈佇列來解決這個問題。

迴圈佇列

  • 迴圈佇列實質上就是將陣列首尾相連組成一個環,當隊尾到達陣列上限但陣列頭還有空間時,我們就可以繼續沿著陣列頭繼續進隊。

  • 實現迴圈佇列的關鍵就是確定佇列空和佇列滿的條件。類似順序佇列,當 head = tail 時,佇列為空;當 (head+1) % len = head 時,佇列為滿。而且,當佇列滿時, tail 指向的位置是沒有資料的

佇列滿

class CircularQueue
{
    private:
        int *data;      // 順序佇列
        int head;       // 佇列頭索引值
        int tail;       // 佇列尾索引值
        int len;        // 佇列的大小

    public:
        // 初始化佇列,申請一個大小為 n 的陣列
        CircularQueue(int n)
        {
            data = new int[n];
            head = 0;
            tail = 0;
            len = n;
        }

        bool enqueue(int item)
        {
            // 佇列空間已滿,返回 false,入隊失敗
            if ((tail + 1) % len == head)
            {
                cout << "queue is full" << endl;
                return false;
            }
            else
            {
                // 佇列空間未滿,入隊,佇列尾索引值加 1
                data[tail] = item;
                tail++;
                // 到達陣列上限,索引值迴圈到 0
                if (tail == len)
                {
                    tail = 0;
                }
                return true;
            }
        }

        int dequeue()
        {
            // 佇列頭等於佇列尾,佇列為空,返回 -1,出隊失敗
            if (head == tail)
            {
                cout << "queue is empty" << endl;
                return -1;
            }
            // 佇列非空,隊頭元素出隊,佇列頭索引值加 1
            else
            {
                int item = data[head];
                head++;

                // 到達陣列上限,索引值迴圈到 0
                if (head == len)
                {
                    head = 0;
                }
                return item;
            }
        }

        void print()
        {
            cout << head << " " << tail << endl;
        }
};

3. 阻塞佇列和併發佇列?

  • 阻塞佇列就是在佇列基礎上增加了阻塞操作。佇列為空時,出隊操作會被阻塞,直到佇列中有了資料才能返回;佇列滿時,入隊操作會被阻塞,直到佇列中有空閒位置時才能插入新資料。

阻塞佇列

  • 使用阻塞佇列,我們可以輕鬆實現一個“生產者 - 消費者“模型。此外,我們還可以通過協調”生產者“和”消費者“的數量,來提高資料的處理效率。

生產者消費者模型

  • 在多執行緒情況下,會有多個執行緒同時操作佇列,要想實現一個執行緒安全的佇列,我麼就需要一個併發佇列。一種是在 enqueue、dequeue方法中加鎖,同一時刻只允許一個存或取操作;另一種則是基於迴圈佇列,利用 CAS 原子操作,可以實現非常高效的併發佇列。

3. 佇列線上程池等有限資源池中的應用?

執行緒池中沒有空閒執行緒時,新的任務請求執行緒資源時,執行緒池該如何處理?各種處理策略又是如何實現的呢?

  • 一般有兩種策略。非阻塞的處理方式直接拒絕任務請求;阻塞的處理將請求進行排隊,等到有空閒執行緒時,取出請求進行處理。

  • 基於連結串列,可以實現支援無限排隊的無界佇列(unbounded queue),但是可能會導致過多的請求排隊,會導致請求的響應時間過長。而基於陣列的有界佇列(bounded queue),佇列大小有限,超出的請求就會被拒絕。

參考資料-極客時間專欄《資料結構與演算法之美》

獲取更多精彩,請關注「seniusen」!
seniusen