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