ArrayDeque(双端队列的线性实现)详解
ArrayDeque 是 java 中對雙端隊(duì)列的線性實(shí)現(xiàn)
一. 特性
無容量大小限制,容量按需增長;
非線程安全隊(duì)列,無同步策略,不支持多線程安全訪問;
當(dāng)用作棧時(shí),性能優(yōu)于Stack,當(dāng)用于隊(duì)列時(shí),性能優(yōu)于LinkedList
兩端都可以操作
具有 fail-fast 特征
不能存儲null
支持雙向迭代器遍歷
注意: ArrayDeque 的迭代器和大多數(shù)容器迭代器一樣,都是快速失敗 (fail-fast),但是程序不能利用這個(gè)特性決定是或否進(jìn)行了并發(fā)操作。
二. 數(shù)據(jù)結(jié)構(gòu)
為了更好的理解使用線性數(shù)組實(shí)現(xiàn)的雙端隊(duì)列,這里我們先來圖解線性數(shù)組實(shí)現(xiàn)基本數(shù)據(jù)結(jié)構(gòu) - 隊(duì)列:
如上圖所示,head 指向隊(duì)頭,入隊(duì)加元素時(shí),tail 隊(duì)尾向后移動,出隊(duì)時(shí)從 head 出取出元素并移除,這樣就利用了線性數(shù)組實(shí)現(xiàn)先進(jìn)先出的隊(duì)列數(shù)據(jù)結(jié)構(gòu),當(dāng) head 等于 tail 時(shí),則表示隊(duì)列為空。但是這樣存在問題:當(dāng)不斷出隊(duì)時(shí),head 向后移動,前面空出來的空間就被浪費(fèi),導(dǎo)致不斷入隊(duì)時(shí),需要數(shù)組擴(kuò)容,出隊(duì)時(shí)造成大量空間無法使用,空間利用率低下!假設(shè),如果能將前面空出來的空間也利用起來進(jìn)行存儲末尾的元素,則空間使用率將提高,這里就需要有個(gè)循環(huán)的思維,把這種線性的彎曲成一個(gè)圓環(huán),這樣就可以反復(fù)使用空出來的空間,入隊(duì)時(shí)使用出隊(duì)空余出來的空間,就解決以上的問題,圖解如下:
同樣,當(dāng) head 等于 tail 時(shí),則表示循環(huán)隊(duì)列為空。head 和 tail 也是循環(huán)的,像鐘表中的時(shí)針,具有周期性。這里 head 和 tail 需要對長度 lenth 取模,這樣 head 和 tail 將一直在長度范圍內(nèi),可以作為數(shù)組的下標(biāo)。
對于如何將數(shù)據(jù)分布到相應(yīng)大小的連續(xù)空間中,常用的方式就是取模運(yùn)算,即 position=index%len,利用整數(shù)倍的周期性,將剩余的部分作為空間索引。
三. 源碼分析
1. ArrayDeque 數(shù)據(jù)域
/***?The?array?in?which?the?elements?of?the?deque?are?stored.*?The?capacity?of?the?deque?is?the?length?of?this?array,?which?is*?always?a?power?of?two.?The?array?is?never?allowed?to?become*?full,?except?transiently?within?an?addX?method?where?it?is*?resized?(see?doubleCapacity)?immediately?upon?becoming?full,*?thus?avoiding?head?and?tail?wrapping?around?to?equal?each*?other.??We?also?guarantee?that?all?array?cells?not?holding*?deque?elements?are?always?null.*/ transient?Object[]?elements;?//?non-private?to?simplify?nested?class?access/***?The?index?of?the?element?at?the?head?of?the?deque?(which?is?the*?element?that?would?be?removed?by?remove()?or?pop());?or?an*?arbitrary?number?equal?to?tail?if?the?deque?is?empty.*/ transient?int?head;/***?The?index?at?which?the?next?element?would?be?added?to?the?tail*?of?the?deque?(via?addLast(E),?add(E),?or?push(E)).*/ transient?int?tail;/***?The?minimum?capacity?that?we'll?use?for?a?newly?created?deque.*?Must?be?a?power?of?2.*/ private?static?final?int?MIN_INITIAL_CAPACITY?=?8;首先看下 ArrayDeque 持有的成員域,其中非常核心的是 elements,head,tail 三個(gè)。下面逐一介紹:
elements: 該數(shù)組用于存儲隊(duì)列元素,且是大小總是 2 的冪次方(后面會介紹為什么?)。這個(gè)數(shù)組不會滿容量,會在add方法中擴(kuò)容,使得頭 head 和 tail 不會纏繞在一起(即 head 增長或不會超過 tail,head 減小時(shí)不會溢出到 tail),這里隊(duì)列長度是 2 的冪次方的原因后續(xù)會闡明;
head: 雙端隊(duì)列的頭位置,出隊(duì)時(shí)或者彈出棧時(shí)的元素位置,加入雙端隊(duì)列頭端元素位置,表示當(dāng)前頭元素位置;
tail: 雙端隊(duì)列的尾,入隊(duì)和進(jìn)棧時(shí)的元素位置,加入雙端隊(duì)列尾端的下個(gè)元素的索引,tail 位總是空的;
MIN_INITIAL_CAPACITY: 最小的初始化容量
2. 構(gòu)造函數(shù)
/***?Constructs?an?empty?array?deque?with?an?initial?capacity*?sufficient?to?hold?16?elements.*/ public?ArrayDeque()?{elements?=?new?Object[16]; }/***?Constructs?an?empty?array?deque?with?an?initial?capacity*?sufficient?to?hold?the?specified?number?of?elements.**?@param?numElements??lower?bound?on?initial?capacity?of?the?deque*/ public?ArrayDeque(int?numElements)?{allocateElements(numElements); }/***?Constructs?a?deque?containing?the?elements?of?the?specified*?collection,?in?the?order?they?are?returned?by?the?collection's*?iterator.??(The?first?element?returned?by?the?collection's*?iterator?becomes?the?first?element,?or?<i>front</i>?of?the*?deque.)**?@param?c?the?collection?whose?elements?are?to?be?placed?into?the?deque*?@throws?NullPointerException?if?the?specified?collection?is?null*/ public?ArrayDeque(Collection<??extends?E>?c)?{allocateElements(c.size());addAll(c); }第一個(gè)默認(rèn)的無參構(gòu)造函數(shù):創(chuàng)建初始化大小為 16 的隊(duì)列
第二個(gè)構(gòu)造函數(shù):根據(jù)參數(shù)numElements創(chuàng)建隊(duì)列,如果numElements小于 8,則隊(duì)列初始化大小為 8;如果numElements大于 8,則初始化大小為大于numElements的最小 2 的冪次方。如:numElements=17,則初始化大小為 32
第三個(gè)構(gòu)造函數(shù):根據(jù)集合元素創(chuàng)建隊(duì)列,初始化大小為大于集合大小的最小 2 的冪次方
這里重點(diǎn)看下第二個(gè)構(gòu)造器的過程。其中調(diào)用allocateElements(numElements)方法,該方法用來實(shí)現(xiàn)容量分配,下面看下內(nèi)部具體實(shí)現(xiàn):
/***?Allocates?empty?array?to?hold?the?given?number?of?elements.**?@param?numElements??the?number?of?elements?to?hold*/ private?void?allocateElements(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?offinitialCapacity?>>>=?1;//?Good?luck?allocating?2?^?30?elements}elements?=?new?Object[initialCapacity]; }首先判斷指定大小 numElements 與MIN_INITIAL_CAPACITY的大小關(guān)系。如果小于MIN_INITIAL_CAPACITY,則直接分配大小為MIN_INITIAL_CAPACITY的數(shù)組;如果大于MIN_INITIAL_CAPACITY,則進(jìn)行無符號右移操作,然后在加 1,這樣就可以尋找到大于 numElements 的最小 2 的冪次方。原理:無符號右移再進(jìn)行按位或操作,就是將其低位全部補(bǔ)成 1,然后再自加加一次,就是再向前進(jìn)一位。這樣就能得到其最小的 2 次冪。之所以需要最多移 16 位,是為了能夠處理大于 2^16 次方數(shù)。
最后再判斷值是否小于 0,因?yàn)槿绻跏贾翟?int 最大值 231-1 和 230 之間,進(jìn)行一系列移位操作后將得到 int 最大值,再加 1,則溢出變成負(fù)數(shù),所以需要檢測臨界值,然后再右移 1 位!!!
接下來再來分析下 ArrayDeque 的幾個(gè)重要雙端操作。對于雙端隊(duì)列有哪些重要的雙端操作,可以移步至我的之前寫的另一篇文章 Java 中 Deque 特性及 API
在詳細(xì)介紹 ArrayDeque 的重要 API 實(shí)現(xiàn)之前,以圖解的方式看下 ArrayDeque 構(gòu)造函數(shù)初始化出的隊(duì)列的數(shù)據(jù)結(jié)構(gòu):
組的0下標(biāo)位置。在了解初始化后的數(shù)據(jù)構(gòu)成后,再首先來看下addFirst方法
<h6 id=")3\." 重要行為=""addFirst 方法/***?Inserts?the?specified?element?at?the?front?of?this?deque.**?@param?e?the?element?to?add*?@throws?NullPointerException?if?the?specified?element?is?null*/ public?void?addFirst(E?e)?{if?(e?==?null)throw?new?NullPointerException();elements[head?=?(head?-?1)?&?(elements.length?-?1)]?=?e;if?(head?==?tail)doubleCapacity(); }先用圖解的方式分析下這個(gè)方法,在第一次調(diào)用這個(gè)方法后,數(shù)據(jù)變化如下:
根據(jù)圖的變化來分析下代碼實(shí)現(xiàn)。首先判斷插入元素是否為空,再計(jì)算即將插入的位置,計(jì)算出后將元素賦值給相應(yīng)的槽位,最后再判斷隊(duì)列容量進(jìn)行擴(kuò)容。
將數(shù)組的高位端作為雙端隊(duì)列的頭部,將低位作為雙端隊(duì)列尾部。沒從頭部加入一個(gè)元素時(shí),head 頭逆時(shí)針向 tail 尾方向移動一個(gè)位置,實(shí)現(xiàn)上即將 head 減 1 后對數(shù)組的最大下標(biāo)按位與運(yùn)算。這里就利用了 2 的冪次方的特性,隊(duì)列容量設(shè)置為 2 的冪次方后,數(shù)組的最大下標(biāo)位置等于 2 的冪次方減 1,在二進(jìn)制表示時(shí),就是所有二進(jìn)制位都是 1。這樣 head 位置減 1 后與其進(jìn)行按位與運(yùn)算就能得到頭部插入的位置。
當(dāng) head 等于 tail 時(shí),就表示隊(duì)列已經(jīng)滿了。這時(shí)需要進(jìn)行擴(kuò)容。
下面再來看下擴(kuò)容策略:
/***?Doubles?the?capacity?of?this?deque.??Call?only?when?full,?i.e.,*?when?head?and?tail?have?wrapped?around?to?become?equal.*/ private?void?doubleCapacity()?{assert?head?==?tail;int?p?=?head;int?n?=?elements.length;int?r?=?n?-?p;?//?number?of?elements?to?the?right?of?pint?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; }按照 2 倍方式擴(kuò)容
擴(kuò)容后,將原隊(duì)列中從頭部插入的元素即 head 右邊元素從擴(kuò)容后新數(shù)組的 0 位置開始排放,然后將左邊的元素緊接著排放進(jìn)新數(shù)組。
將 head 置 0,tail 置成擴(kuò)容前數(shù)組長度。
如果從頭端插入,則 head 繼續(xù)逆時(shí)針旋轉(zhuǎn)方式插入新元素。從以上圖中不難看出 addFirst 是操作雙端隊(duì)列頭端,且是逆時(shí)針方式旋轉(zhuǎn)插入。接下來再看看從尾端插入的過程
addLast 方法
/***?Inserts?the?specified?element?at?the?end?of?this?deque.**?<p>This?method?is?equivalent?to?{@link?#add}.**?@param?e?the?element?to?add*?@throws?NullPointerException?if?the?specified?element?is?null*/ public?void?addLast(E?e)?{if?(e?==?null)throw?new?NullPointerException();elements[tail]?=?e;if?(?(tail?=?(tail?+?1)?&?(elements.length?-?1))?==?head)doubleCapacity(); }上述的 addFirst 是逆時(shí)針的插入方式,addLast 剛好與其相反,即順時(shí)針方向插入,且 tail 表示的是下一個(gè)插入的元素的位置。
判斷元素是否為空,然后直接將元素插入 tail 槽位
然后 tail 向后移動一位,再按位與(控制循環(huán))作為新的 tail 槽位
判斷新的 tail 槽位是否與 head 相等,然后依此進(jìn)行擴(kuò)容(這里擴(kuò)容與上述擴(kuò)容過程一樣,不再贅述)。
pollFirst 方法
public?E?pollFirst()?{int?h?=?head;@SuppressWarnings("unchecked")E?result?=?(E)?elements[h];//?Element?is?null?if?deque?emptyif?(result?==?null)return?null;elements[h]?=?null;?????//?Must?null?out?slothead?=?(h?+?1)?&?(elements.length?-?1);return?result; }取出頭元素,如果頭元素為空,則返回null
否則,將頭元素槽位置為空(因?yàn)?pollFirst 是移除操作)
再將 head 順時(shí)針向后移動一位,即加 1 再和數(shù)組最大下標(biāo)按位與計(jì)算出新的 head
注:讀到這里,相信讀者已經(jīng)已經(jīng)對雙端隊(duì)列的數(shù)據(jù)結(jié)構(gòu)已經(jīng)非常清晰,即雙端操作的數(shù)組,tail 向前(順時(shí)針)移動即從尾端插入元素或者向后移動即從尾端移除元素,head 向后(逆時(shí)針)移動即從頭端插入元素或者向前移動即從頭端移除元素。這幾個(gè)過程正好具有 FIFO 和 LIFO 的特點(diǎn),所以 ArrayDeque 既可以作為隊(duì)列 Queue 又可以作為棧 Stack。
pollLast 方法
public?E?pollLast()?{int?t?=?(tail?-?1)?&?(elements.length?-?1);@SuppressWarnings("unchecked")E?result?=?(E)?elements[t];if?(result?==?null)return?null;elements[t]?=?null;tail?=?t;return?result; }從以上描述的 ArrayDeque 的數(shù)據(jù)結(jié)構(gòu)和 tail 的含義中,可以大致思考下,從尾端移除元素的過程。
先將 tail 向后(逆時(shí)針)移動一位,然后對數(shù)組最大下標(biāo)按位與計(jì)算出將要移除元素的槽位
取出計(jì)算出的槽位中元素,判斷是否為空,為空則返回null
如果不為空,則將該槽位置為空,將槽位下標(biāo)作為新的 tail
以上的過程基就是 ArrayDeque 的工作原理的最基本實(shí)現(xiàn),其他的行為大都是基于這些過程實(shí)現(xiàn):
offer 方法:內(nèi)部調(diào)用 offerLast 插入元素,返回插入結(jié)果 true/false
add 方法:內(nèi)部調(diào)用 addLast 實(shí)現(xiàn)
poll 方法:內(nèi)部調(diào)用 pollFirst 實(shí)現(xiàn)
remove 方法:內(nèi)部調(diào)用 removeFirst 實(shí)現(xiàn)
peek 方法:內(nèi)部調(diào)用 peekFirst 實(shí)現(xiàn)
element 方法:內(nèi)部調(diào)用 getFirst 實(shí)現(xiàn)
pop 方法:內(nèi)部調(diào)用 addFirst 實(shí)現(xiàn)
push 方法:內(nèi)部調(diào)用 removeFirst 實(shí)現(xiàn)
這里不再詳述每個(gè)操作的具體實(shí)現(xiàn),因?yàn)檫@些操作都是基于 addFirst、addLast、pollFirst 和 pollLast 實(shí)現(xiàn)。具體調(diào)用這些基礎(chǔ)行為實(shí)現(xiàn)的細(xì)節(jié),讀者可以閱讀 ArrayDeque 源碼。
參考:
位運(yùn)算總結(jié) (按位與, 或, 異或) https://blog.csdn.net/sinat_35121480/article/details/53510793 Java 中 >> 和 >>> 的區(qū)別 https://www.cnblogs.com/leo0705/p/8473071.html java int short long float double 精度最大值整理 https://blog.csdn.net/truelove12358/article/details/48522437
作者:懷瑾握瑜
來源鏈接:
https://www.cnblogs.com/lxyit/p/9080590.html
總結(jié)
以上是生活随笔為你收集整理的ArrayDeque(双端队列的线性实现)详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 位图和矢量图转换工具推荐
- 下一篇: 数独 九宫格 破解