資料結構之「佇列」

清塵閒聊發表於2019-03-21

什麼是佇列?

佇列(queue)是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。是一種先進先出(First In First Out)的線性表,簡稱 FIFO。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。 佇列有 2 種方式來儲存:陣列 和 連結串列。 陣列我們都知道它是預先分配好長度的,因此會出現溢位現象,而且刪除元素需要向隊頭移動一個位置,時間複雜度就變成 O(n)。因此,需要一種新的方式來解決這個問題,那就是迴圈佇列。 佇列的這種頭尾相接的順序儲存結構稱為迴圈佇列。為了避免佇列刪除元素需要移動整個佇列,使得隊頭和隊尾可以在陣列中迴圈變化。解決了移動元素的時間損耗,使得本來刪除是 O(n) 的時間複雜度變成了 O(1)。

1. 陣列式佇列

申請一片連續的儲存空間,並設定兩個指標進行管理。一個是隊頭指標front,它指向隊頭元素;另一個是隊尾指標rear,它指向下一個入隊元素的儲存位置。一般是實現迴圈佇列,隊頭和隊尾會隨著入隊和出隊的變化而變化的。例如 JDK 中 ArrayBlockingQueue 就是基於迴圈佇列來實現的。不過它會有溢位現象,一般解決方案是如果佇列滿了,設定入隊等待時間或者返回入隊不成功。一般在確定元素個數情況下使用,如果不確定元素個數,建議使用連結串列式佇列。

陣列式佇列

2. 連結串列式佇列

它就是基於連結串列儲存結構的佇列,可以動態的建立和刪除元素,不用關心佇列的長度,因此不用擔心溢位現象。新元素插入到隊尾,讀取的時候從隊頭開始,每次讀取一個元素,釋放一個元素,這就是所謂的動態建立和動態刪除。

連結串列式佇列

佇列有什麼用?

1. 保證輸入順序

比如吃飯排隊,先找服務員拿個號碼,上面會寫著前面還有 n 桌,這就相當於服務員把你加入了她們店的佇列中,當有空位置時,就直接叫你入座吃飯,當沒有空位子時,要麼排隊等候,要麼換一家吃飯。這就是佇列的用處。

2. 解耦

在系統設計中,好的設計是低耦合,高內聚。意思就是一個系統只做一件事情,把一件事情做好。既方便程式碼維護,又方便擴充套件。比如又一個下單的場景,使用者下單之後需要加積分,需要給各種優惠券等等。我們就可以利用佇列來解偶,但真實使用一般是用開源的訊息佇列,如Rocket MQ,Kafka 等等。

3. 提升系統吞吐量

就拿上面訂單來說,本來以前是使用者下單,給使用者新增積分,給使用者發優惠券是一起處理成功後返回的。現在只是處理下單,然後告訴某某系統,給某某使用者加積分,給某某使用者發優惠券了,它自己並不真正做這個動作,所以會提升系統響應,當然需要保證最終一致性。一般也是用開源的訊息佇列來完成的。

佇列怎麼實現?

這裡是基於陣列的迴圈佇列實現,也就是 JDK 的 ArrayBlockingQueue。有興趣的朋友也可以看看基於連結串列的 LinkedBlockingQueue 的實現。 儲存結構

public class ArrayBlockingQueue<E> {
    //用陣列來儲存元素
    Object[] items;
    //陣列裡出隊的下標
    int takeIndex;
    //入隊的下標
    int putIndex;
    //元素個數
    int count;
}
複製程式碼

入隊

public boolean offer(E e) {
    //加入陣列滿了,則返回入隊失敗
    if (count == items.length) {
        return false;
    } else {
        //獲得當前陣列
        Object[] items = this.items;
        //把元素 e 加入到隊尾
        items[putIndex] = e;
        //判斷是不是隊尾是不是倒數第二個元素
        //是的話把下標為 0 置為隊尾,說明一圈了
        if (++putIndex == items.length) {
            putIndex = 0;
        }
        //總數加一
        count++;
        return true;
    }
}
複製程式碼

出隊

public E poll() {
    //空佇列則返回 null
    if (count == 0) {
        return null;
    } else {
        //獲取當前陣列
        Object[] items = this.items;
        //獲取隊頭元素
        E item = (E) items[takeIndex];
        //置空
        items[takeIndex] = null;
        //重置隊頭為下標 0;
        if (++takeIndex == items.length) {
            takeIndex = 0;
        }
        //總數減一
        count--;
        return item;
    }
}
複製程式碼

總結

佇列的作用還是很大的,比如保證輸入順序,解偶,提升系統吞吐量等等,都基於佇列的原理來實現的。佇列有陣列式佇列和連結串列式佇列。陣列式佇列就是基於陣列儲存結構來實現的,陣列的優缺點它全有。連結串列式佇列就是基於連結串列儲存結構來實現的,它也包含來連結串列的優缺點。所以在使用時可以根據業務需求來選擇最優的方案。

相關文章