數組隊列如何手撕?解密ArrayBlockingQueue的實現內幕!

隊列

聊起隊列,你一定會聯想到一個與隊列相似的數據結構:

爲了更好的理解什麼是隊列,我們將它和棧來比較一下:

隊列的特點是:先進先出,如下圖,1先進,1就先出。

圖1:隊列的圖解

棧的特點是:先進後出,如下圖,1先進,1卻最後出。

圖2:棧的圖解

爲了讓你更好的區分與理解隊列與棧,你只記住這個口訣:喫多了拉就是隊列,喫多了吐就是棧

哈哈哈,千萬不要告訴別人這是我告訴你的....

數組實現隊列,難在哪?

數組實現隊列,乍一看,很貼合的一種方式去實現隊列。仔細推敲一下你會發現並不簡單。

爲了更看清數組存在什麼問題,我先new一個size爲4的數組:

圖3:大小爲4的數組

下一步,向數組中入列4個元素,數組按照0、1、2、3的下標被填充:

圖4:隊列寫滿

繼續,消費兩個元素,我們按照先進先出的原則,消費掉下標爲0、1的元素,如圖:

圖5:從隊列消費2個元素

如果繼續入列,你會發現:最大的下標3已經被佔用,而數組依然存在空間。無法入列,造成空間浪費!

數組隊列如何解決困難?

思維活躍的程序員,不可能因爲遇到困難而畏懼!更不可能存在空間,而無法使用。就像飢腸轆轆,食物擺在眼前,卻不知道如何喫掉它!

方案一:擴

瞭解過ArrayListHashMap的朋友肯定知道空間不夠,那就當發現空間不夠用,我們是否可以將空間擴成原來的2倍?,像下圖這樣:

圖6:數組擴容

是辦法,但是不是最好的辦法。因爲下標爲0和1的空間,無法利用和回收,造成浪費!,不妥!也不可取!有人說無所謂,錢不是問題,金主直接return~

方案二:移

既然存在空間無法利用,那發現數組空間滿了之後,是否可以將元素進行挪動呢?如圖:

圖7:數組元素移動

是辦法,是比方案一更好的辦法,但依然不是最好的辦法。那這又爲什麼呢?想想,兩個數據的挪動,貌似沒有什麼成本,但是如果是1萬甚至更多元素挪動,會帶來什麼呢?對!更高的時間複雜度,即o(n)。

除了,真的沒有其他的辦法?有的,今天給大家介紹一個新玩法:循環隊列

循環數組隊列

說起循環,大家肯定腦海中呈現出頭尾相連的一個環,就是循環。是的!循環數組隊列就是這樣的結構,將你認知的連續的長長的數組,抽象成一個環,就是下圖:

圖8:循環數組隊列

在圖5數組隊列的基礎上,繼續入列一個元素,如下圖:

圖9:循環數組隊列追加元素

入列後的數組,你試着把它想象成一個環狀,如果再繼續入列元素,會放到下標爲1的位置。

因此循環數組隊列很好地解決了之前兩種方案的缺點:
1.空間浪費無法充分利用
2.數據遷移導致的o(n)的時間複雜度

如何實現循環數組隊列

屬性定義&說明

實現循環數組隊列,需要定義幾個關鍵的屬性,以幫助我們更好地代碼實現:

  • capicity
    隊列的容量大小
  • items
    定義的數組,容納元素
  • head
    出列的頭部數組下標,指向下次出列的位置
  • tail
    入列的尾部數組下標,指向下次入列的位置
  • size
    隊列入列的元素個數

代碼實現

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`public class LoopQueue<T> {
// 容量
private int capicity;

// 隊列元素
private Object[] items;

// 頭部位置
private int head = 0;

// 尾部位置
private int tail = 0;

// 大小
private int size = 0;

public LoopQueue(int capicity) {
    this.capicity = capicity;
    this.items = new Object[capicity];
}

public boolean enqueue(T t) {
    // 如果超出容量,直接返回入列失敗
    if (size >= capicity) {
        return false;
    }
    // 到尾部了,但是還沒有滿,tail重置
    if (tail == capicity) {
        tail = 0;
    }
    // 數組tail位置放置元素,並將tail自增
    this.items[tail++] = t;
    // 大小累加
    size++;
    return true;
}

public T dequeue() {
    // 隊列無元素,直接返回空
    if (size <= 0) {
        return null;
    }

    // 到尾部了,但是還沒有滿,head重置
    if (head == capicity) {
        head = 0;
    }
    // size自減少
    size--;
    // 獲取元素並自增head
    return (T) this.items[head++];
}

public static void main(String[] args) {
    LoopQueue<Integer> queue = new LoopQueue<Integer>(6);
    for (int i = 0; i < 7; i++) {
        System.out.println(queue.enqueue(i));
    }

    for (int i = 0; i < 7; i++) {
        System.out.println(queue.dequeue());
    }
}

}` </pre>

上述代碼,已經增加了註釋,你可以直接拷貝試驗下。

main方法中可直接運行測試:定義容量爲6的數組隊列,入列7次後,出列7次,運行結果如圖:

圖10:循環數組運行結果

  1. 位置1因爲空間已滿,無法入列,返回false
  2. 位置2因爲隊列已空,出列的元素爲空

ArrayBlockingQueue解密

爲了研究下數組隊列在JDK中如何實現的,我特地看了下ArrayBlockingQueue的源碼:

  1. put方法入口,當空間滿了,阻塞

圖11:put方法

  1. 如果空間沒有滿,進入第2步,enqueue方法,實現邏輯也採用的循環數組隊列

圖12:enqueue方法

寫在最後

數組隊列用了較長的篇幅在討論,採用循環的思路很優雅地解決了空間利用問題。

不知道你有沒有發現一個問題,JDK中的ArrayBlockingQueue的構造函數全部都需要初始化capacity:

圖13:ArrayBlockingQueue構造函數

因爲使用數組需要初始化空間,所以需要初始化capacity。

留一道思考題:有沒有實現無限空間的隊列的方法呢?
不妨打開JDK尋找下答案~

我是公衆號【面試怪圈】的keaizhuzhu,關注我~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章