數據結構-Queue系列之ArrayDeque(一)

這段時間暫時不寫關於各種框架的博客了。因爲自己是非科班出生,所以想的還是先從根基抓起,目前在學大學的計算機基礎課,計算機網絡,操作系統那些比較偏理論,而數據結構和算法有很大的關係,所以單單做一些數據結構方面的筆記。
首先就來看看隊列,queue。

最最最基本的概念:

隊列是一種特殊的線性表,是一種先進先出(FIFO)的數據結構。它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。隊列中沒有元素時,稱爲空隊列。

關鍵詞:FIFO,作爲先進先出的一種數據結構,可以較爲方便的按插入順序來獲取元素。

總所周知,Queue在java也只是一個接口,肯定的嘛,就上面那一段抽象的定義,不整成個接口還咋地?Queue的實現類有很多,筆者以java8爲例,如下圖,有十多種Queue的實現類。其中不少與線程併發有關,作爲Queue系列的第一篇,我們就來看看ArrayDeque,因爲不涉及併發安全,所以相對簡單一些。
在這裏插入圖片描述

ArrayDeque的繼承圖

首先看看ArrayDeque實現了哪些接口與父類:
在這裏插入圖片描述

源碼部分研究

源碼部分有幾處非常關鍵,即使不研究這個類,也可以學習其寫法,我第一次看源碼看到虎軀一震的好嗎,其優雅,其精簡,差點讓我在辦公室直接昇天:

源碼:找到大於某個數的最小2的次冪的最佳寫法

這段代碼意義:
規定最小值MIN_INITIAL_CAPACITY = 8,如果入參小於8,數組大小就定義成8;如果大於等於8,就找到到大於入參的2的次冪中最小的一個。
先不看下面代碼,你覺得找到大於入參的最小2的次冪,這個算法該怎麼寫?
然後你停下coding,來看看下面這段代碼,你就會像我一樣驚歎:

private static final int MIN_INITIAL_CAPACITY = 8;

// ******  Array allocation and resizing utilities ******

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

這一通右移是啥操作?假如我們傳入了16,二進制10000,逐步分析下:

1.initialCapacity |= (initialCapacity >>> 1)
右移1位作|操作,10000->01000,‘或’ 操作後11000

2.initialCapacity |= (initialCapacity >>> 2)
接上一步,右移2位作|操作,11000->00110,‘或’ 操作後11110

3.initialCapacity |= (initialCapacity >>> 4)
接上一步,右移4位作|操作,11110->00001,‘或’ 操作後 11111

……

後面就兩步都是11111 | 00000,結果就是 11111

4.initialCapacity++
二進制數11111,+1之後100000,轉換成十進制32

結論:這些’或’ 操作,最終得到了大於入參的2的次冪中最小的一個。

問題:爲什麼是從1右移到16,?
答:我認爲是因爲int數最大32位,除去符號位1位,剩下數字位31位,從1+2+4+。。+16恰好是31

當初看這一段,我還去補了一下原碼,反碼,補碼的知識(見上一篇),真的太佩服作者了。

源碼:確保增減數組索引不越界

再看看插入操作:

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

elements[head = (head - 1) & (elements.length - 1)] 是什麼鬼?
我先說結論:其實這裏就是讓原head前的那一個數組位置,變爲新的head(因爲是插入到隊列最前端嘛)
所以有一個head-1,其實就是head的索引之前的那一個索引位
但是問題來了,如果原來head是0呢?未必把新元素插到索引爲-1的位置上?
吃我數組越界警告!!
所以這裏有個按位與操作,(head - 1) & (elements.length - 1)的結果就是,如果爲head-1爲正,則返回head-1,如果爲負,則到最後一位。相當驚豔有沒有!如果你拿筆用二進制推敲一遍是如何達到這個結果的,那你會更驚豔,恭喜你,找到了程序之美~

addLast,同樣的意思,感興趣可以自己分析~

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
源碼:擴容
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;    //左移,等價乘2,依然保持2的次冪
    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;
}

圖示一目瞭然,我就感覺這個交叉移位帥爆了好不好
在這裏插入圖片描述

部分參考下列博客,如果想繼續深挖,可以看看:
https://segmentfault.com/a/1190000017480142?utm_source=tag-newest

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