深刻理解循環隊列----循環數組實現ArrayDeque

     咱們知道隊列這種數據結構的物理實現方式主要仍是兩種,一種是鏈隊列(自定義節點類),另外一種則是使用數組實現,二者各有優點。此處咱們將要介紹的循環隊列實際上是隊列的一種具體實現,因爲通常的數組實現的隊列結構在頻繁出隊的狀況下,會產生假溢出現象,致使數組使用效率下降,因此引入循環隊列這種結構。本文將從如下兩個大角度介紹循環隊列這種數據結構:數組

  • 循環數組實現循環隊列
  • Java中具體實現容器類ArrayDeque

1、循環隊列
     爲了深入體會到循環隊列這個結構優於非循環隊列的地方,咱們將首先介紹數組實現的非循環隊列結構。隊列這種數據結構,不管你是用鏈表實現,仍是用數組實現,它都是要有兩個指針分別指向隊頭和隊尾。在咱們數組的實現方式中,用兩個int型變量用於記錄隊頭和隊尾的索引。數據結構

這裏寫圖片描述

一個隊列的初始狀態,head和tail都指向初始位置(索引爲0處)。head永遠指向該隊列的隊頭元素,tail則指向該隊列最後一個元素的下一位置,當有入隊操做時:函數

這裏寫圖片描述
這裏寫圖片描述

當有出隊操做時:設計

這裏寫圖片描述

當遇到出隊操做時,head會移向下一元素位置。固然,對於這種方式入隊和出隊,隊空的判斷條件顯然是head=tail,隊滿的判斷條件是tail=array.length(數組最後一個位置的下一位置)。顯然,這種結構最致命的缺陷就是,tail只知道向後移動,一旦到達數組邊界就認爲隊滿,可是隊列可能時刻在出隊,也就是前面元素都出隊了,tail也不知道。例如:指針

這裏寫圖片描述

此時tail判斷隊滿,咱們暫時認爲資源利用是能夠接受的,可是若是接下來不斷髮生出隊操做:code

這裏寫圖片描述

此時tail依然經過判斷,認爲隊滿,不能入隊,這時數組的利用率咱們是不能接受的,這樣浪費很大。因此,咱們引入循環隊列,tail能夠經過mode數組的長度實現迴歸初始位置,下面咱們具體來看一下。索引

按照咱們的想法,一旦tail到達數組邊界,那麼能夠經過與數組長度取模返回初始位置,這種狀況下判斷隊滿的條件爲tail=head隊列

這裏寫圖片描述

此時tail的值爲8,取模數組長度8獲得0,發現head=tail,此時認爲隊列滿員。這是合理的,可是咱們忽略了一個重要的點,判斷隊空的條件也是head=tail,那麼該怎麼區分是隊空仍是隊滿呢?解決辦法是,空出隊列中一個位置,若是(tail+1)%array.length=head,咱們就認爲隊滿,下面說明其合理性。圖片

上面遇到的問題是,tail指向了隊尾的後一個位置,也就是新元素將要被插入的位置,若是該位置和head相等了,那麼必然說明當前狀態已經不能容納一個元素入隊(間接的說明隊滿)。由於這種狀況是和隊空的判斷條件是同樣的,因此咱們選擇捨棄一個節點位置,tail指向下一個元素的位置,咱們使用tail+1判斷下一個元素插入以後,是否還能再加入一個元素,若是不能了說明隊列滿,不能容納當前元素入隊(其實還剩下一個空位置),看圖:ci

這裏寫圖片描述

tail經過取模,迴歸到初始位置,咱們判斷tail+1是否等於head,若是等於說明隊滿,不容許入隊操做,固然這是犧牲了一個節點位置來實現和判斷隊空的條件進行區分。上述文字基本完成了隊循環隊列的理論介紹,下面咱們看在Java中對該數據結構的具體實現是怎樣的。

2、雙端隊列實現類ArrayDeque
     ArrayDeque中主要有如下幾個屬性域:

transient Object[] elements;
transient int head;
transient int tail;
private static final int MIN_INITIAL_CAPACITY = 8;

elements就是咱們上述介紹用於存儲隊列中每一個節點,不過在ArrayDeque中該數組長度是沒有限制的,採用一種動態擴容機制實現動態擴充數組容量。head和tail分別表明着頭指針和尾指針。MIN_INITIAL_CAPACITY 表明着建立一個隊列的最小容量,具體使用狀況在下文詳細介紹。如今咱們看下它的幾個構造函數:

public ArrayDeque() {
    elements = new Object[16];
}
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

若是沒有指定顯式傳入elements的長度,則默認16。若是顯式傳入一個表明elements的長度的變量,那麼會調用allocateElements作一些簡單的處理,並不會簡單的將你傳入的參數用來構建elements,它會獲取最接近numElements的2的指數值,好比:numElements等於20,那麼elements的長度會爲32,numElements爲11,那麼對應elements的長度爲16。可是若是你傳入一個小於8的參數,那麼會默認使用咱們上述介紹的靜態屬性值做爲elements的長度。至於爲何這麼作,由於這麼作會大大提升咱們在入隊時候的效率,咱們等會兒會看到。

入隊操做
因爲ArrayDeque實現了Deque,因此它是一個雙向隊列,支持從頭部或者尾部添加節點,因爲內部操做相似,咱們只簡單介紹從尾部添加入隊操做。涉及如下一些函數:

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
    doubleCapacity();
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

public boolean add(E e) {
    addLast(e);
    return true;
}

顯然,主要的方法仍是addLast,其實有人可能會疑問,爲何要這麼多重複的方法呢?其實,雖然咱們這個ArrayDeque它實現了雙端隊列,而且咱們本篇主要把他當作隊列來研究,其實該類徹底能夠做爲棧或者一些其餘結構來使用,因此提供了一些其餘的方法,但本質上仍是某幾個方法。此處咱們主要研究下addLast這個方法,該方法首先將你要添加的元素入隊,而後經過這條語句判斷隊是否已滿:

if ( (tail = (tail + 1) & (elements.length - 1)) == head)

這條語句的判斷條件仍是比較難理解的,咱們以前在構造elements元素的時候,說過它的長度必定是2的指數級,因此對於任意一個2的指數級的值減去1以後必然全部位全爲1,例如:8-1以後爲111,16-1以後1111。而對於tail來講,當tail+1小於等於elements.length - 1,二者與完以後的結果仍是tail+1,可是若是tail+1大於elements.length - 1,二者與完以後就爲0,回到初始位置。這種判斷隊列是否滿的方式要遠遠比咱們使用符號%直接取模高效,jdk優雅的設計今後可見一瞥。接着,若是隊列滿,那麼會調用方法doubleCapacity擴充容量,

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; 
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

該方法仍是比較容易理解的,首先會獲取到原數組長度,擴大兩倍構建一個空數組,接下來就是將原數組中的內容移動到新數組中,下面經過截圖演示兩次移動過程:

這裏寫圖片描述

這是一個滿隊狀態,假如咱們如今還須要入隊,那麼久須要擴容,擴容結果以下:

這裏寫圖片描述

其實兩次移動數組,第一次將head索引以後的全部元素移動到新數組中,第二次將tail到head之間的全部元素移動到新數組中。實際上,就是在移動的時候對原來的順序進行了調整。對於addFirst只不過是將head向前移動一個位置,而後添加新元素。

出隊操做
出隊操做和入隊同樣,具備着多個不一樣的方法,可是內部調用的仍是一個pollFirst方法,咱們主要看下該方法的具體實現便可:

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    if (result == null)
        return null;
    elements[h] = null;
    head = (h + 1) & (elements.length - 1);
    return result;
}

該方法很簡單,直接獲取數組頭部元素便可,而後head日後移動一個位置。這是出隊操做,其實刪除操做也是一種出隊,內部仍是調用了pollFirst方法:

public E removeFirst() {
    E x = pollFirst();
    if (x == null)
        throw new NoSuchElementException();
    return x;
 }

其餘的一些操做
咱們能夠經過getFirst()或者peekFirst()獲取隊頭元素(不刪除該元素,只是查看)。toArray方法返回內部元素的數組形式。

public Object[] toArray() {
    return copyElements(new Object[size()]);
}

還有一些利用索引或者值來檢索具體節點的方法,因爲這些操做並非ArrayDeque的優點,此處再也不贅述了。

至此,有關ArrayDeque的簡單原理已經介該紹完了,ArrayDeque的主要優點在於尾部添加元素,頭部出隊元素的效率是比較高的,內部使用位操做來判斷隊滿條件,效率相對有所提升,而且該結構使用動態擴容,因此對隊列長度也是沒有限制的。在具體狀況下,適時選擇。

相關文章
相關標籤/搜索