路過一家奶茶店,由於生意火爆,門口的排著長長的隊伍,先排隊的人先買到奶茶,然後再輪到下一個,秩序井然。有沒有一種資料結構能體現”先來後到“這種順序呢?

當然有,那就是佇列。先看一下定義:佇列是一種操作受限的線性表,它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作。只能在表的最前端刪除,最後端插入,這和排隊買奶茶中先給最前面的人做奶茶,新來的只能在最後面排隊一樣,相對“公平”。

根據定義我們可以知道,佇列主要支援兩種操作,一種是刪除(出隊dequeue)操作,另一種是插入(入隊enqueue)操作,它的一個主要特點是先進先出,這一定要記住

之前我們講過”“,它和佇列類似,也是一種操作受限的線性表,但它是”先進後出“,與佇列相反。

佇列的實現

和棧一樣,它可以使用陣列實現(順序佇列),也可以使用連結串列實現(鏈式佇列)。

順序佇列

    public class QueueArray<T>
  {
       //儲存內容的泛型陣列
       public T[] items;

       //陣列長度
       private int len;

       //使用頭指標和尾指標輔助入隊、出隊操作
       int head, tail = 0;

       public QueueArray(int capacity)
      {
           items = new T[capacity];
           len = capacity;
      }

       //入隊
       public bool Enqueue(T val)
      {
           //尾指標與陣列長度相同,說明佇列已滿,返回false(注意,該判斷存在問題)
           if (tail == len)
               return false;

           items[tail++] = val;

           return true;
      }

       //出隊
       public T Dequeue()
      {
           if (tail == head)
               throw new Exception("Queue is empty");

           return items[head++];
      }
  }

現在基本功能已經實現了,但仔細分析會發現,程式碼中的tailhead都只會向後移動(數量只會增加),因此tail == len並不代表陣列已滿,因為陣列頭部的資料可能已經出隊了,前面出現了許多空閒空間。如下圖所示

隨著不停的執行入隊、出隊操作,即使陣列中還有空閒空間,也無法繼續往佇列中新增資料了。此時,需要使用資料搬移,即將佇列中的元素整體搬移到陣列頭,如下圖所示。

該操作只需要在入隊並且”佇列已滿“的時候執行,因此我們需要修改Enqueue()的程式碼為

        //入隊
       public bool Enqueue(T val)
      {
           if (tail == len)
          {
               // tail ==n && head==0,表示整個佇列都佔滿了
               if (head == 0) return false;
               // 資料搬移
               for (int i = head; i < tail; ++i)
              {
                   items[i - head] = items[i];
              }
               // 搬移完之後重新更新head和tail
               tail -= head;
          }

           items[tail++] = val;

           return true;
      }

迴圈佇列

迴圈佇列也是基於陣列實現的,並且能夠很好的解決上面當tail==n需要資料搬移的問題(資料搬移會消耗許多效能)。

顧名思義,環形佇列長得像是一個環,怎麼將陣列“變成”環呢?思路是當tail==n時,如果有空閒位置讓tail = (tail + 1) % len,即將尾部指標轉移到陣列頭部來開始新的迴圈,這樣修改最關鍵的就是要正確判斷隊空和隊滿的條件。下圖中藍色代表頭指標,紅色代表尾指標

修改入隊和出隊的程式碼

        //入隊
       public bool Enqueue(T val)
      {
           //使用尾指標與頭指標來判斷佇列是否滿
           if ((tail + 1) % len == head)
               return false;

           items[tail] = val;
           tail = (tail + 1) % len;
           return true;
      }

       //出隊
       public T Dequeue()
      {
           if (tail == head)
               throw new Exception("Queue is empty");

           T ans = items[head];
           head = (head + 1) % len;
           return ans;
      }

因為判斷隊滿使用的是(tail+1)%n=head,所以當佇列滿時,tail指向的位置實際上是沒有儲存資料的,浪費了陣列的一個儲存空間。

‍♂ 程式碼雖然不多,但最好能夠自己手動實現

鏈式佇列

基於連結串列的實現,我們同樣需要兩個指標:head 指標和 tail 指標。它們分別指向連結串列的第一個結點和最後一個結點。如圖所示,入隊時,tail->next= new_node, tail = tail->next;出隊時,head = head->next,實現起來比較簡單,這裡就省略了

總結