更新時(shí)間:2020-10-13 來源:黑馬程序員 瀏覽量:
LinkedList 集合底層是一個(gè)雙向鏈表結(jié)構(gòu),具有增刪快,查詢慢的忒點(diǎn),內(nèi)部包含大量操作首尾元素的方法。適用于集合元素先入先出和先入后出的場景,在隊(duì)列源碼中被頻繁使用。
LinkedList 底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)雙向鏈表,整體結(jié)構(gòu)如下圖所示:
上圖代表了一個(gè)雙向鏈表結(jié)構(gòu),可以通過前面的節(jié)點(diǎn)找到后面的節(jié)點(diǎn),也可以通過后面的節(jié)點(diǎn)找到前面的節(jié)點(diǎn)
相關(guān)概念:
鏈表中的元素被稱為Node, Node被定義成私有靜態(tài)內(nèi)部類,內(nèi)容如下 :
private static class Node<E> {E item;// 節(jié)點(diǎn)中存儲的數(shù)據(jù)Node<E> next; // 下一個(gè)節(jié)點(diǎn)的地址Node<E> prev; // 前一個(gè)節(jié)點(diǎn)的地址// 構(gòu)造方法初始化參數(shù)順序分別是:前一個(gè)節(jié)點(diǎn)的地址值、當(dāng)前節(jié)點(diǎn)中存儲的數(shù)據(jù)、后一個(gè)節(jié)點(diǎn)的地址值Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}
如果想在LinkedList集合中添加節(jié)點(diǎn),我們把新加入的節(jié)點(diǎn)添加到鏈表頭部,也可以把新加入的節(jié)點(diǎn)添加添加到鏈表尾部,add 方法默認(rèn)是從尾部開始添加,addFirst 方法是從頭部開始添加,下面分別來看下兩種不同的添加方式:
從尾部添加(add)
// 從尾部開始添加節(jié)點(diǎn)void linkLast(E e) {// 把尾節(jié)點(diǎn)數(shù)據(jù)暫存final Node<E> l = last;// 新建新的節(jié)點(diǎn),初始化入?yún)⒑x:// l 是新節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn),當(dāng)前值是尾節(jié)點(diǎn)值// e 表示當(dāng)前新增節(jié)點(diǎn),當(dāng)前新增節(jié)點(diǎn)后一個(gè)節(jié)點(diǎn)是 nullfinal Node<E> newNode = new Node<>(l, e, null);// 新建節(jié)點(diǎn)添加到尾部last = newNode;//如果鏈表為空(l 是尾節(jié)點(diǎn),尾節(jié)點(diǎn)為空,鏈表即空),頭部和尾部是同一個(gè)節(jié)點(diǎn),都是新建的節(jié)點(diǎn)if (l == null)first = newNode;//否則把前尾節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),指向當(dāng)前尾節(jié)點(diǎn)。elsel.next = newNode;size++;//集合元素?cái)?shù)量增加1modCount++;//實(shí)際修改次數(shù)增加1}
從源碼上來看,尾部添加節(jié)點(diǎn)比較簡單.
從頭部添加(addFirst)
// 從頭部添加private void linkFirst(E e) {// 頭節(jié)點(diǎn)賦值給臨時(shí)變量final Node<E> f = first;// 新建節(jié)點(diǎn),前一個(gè)節(jié)點(diǎn)指向null,e 是新建節(jié)點(diǎn),f 是新建節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),目前值是頭節(jié)點(diǎn)的值final Node<E> newNode = new Node<>(null, e, f);// 新建節(jié)點(diǎn)成為頭節(jié)點(diǎn)first = newNode;// 頭節(jié)點(diǎn)為空,就是鏈表為空,頭尾節(jié)點(diǎn)是一個(gè)節(jié)點(diǎn)if (f == null)last = newNode;//上一個(gè)頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn)elsef.prev = newNode;size++;modCount++;}
頭部添加節(jié)點(diǎn)和尾部添加節(jié)點(diǎn)非常類似,只是前者是移動(dòng)頭節(jié)點(diǎn)的 prev 指向,后者是移動(dòng)尾節(jié)點(diǎn)的 next 指向。
節(jié)點(diǎn)刪除的方式和添加類似,我們可以選擇從頭部刪除,也可以選擇從尾部刪除,刪除操作會(huì)把節(jié)點(diǎn)的值,前后指向節(jié)點(diǎn)都置為 null,幫助 GC 進(jìn)行回收。
從頭部刪除
//從頭刪除節(jié)點(diǎn) f 是鏈表頭節(jié)點(diǎn)private E unlinkFirst(Node<E> f) {// 拿出頭節(jié)點(diǎn)的值,作為方法的返回值final E element = f.item;// 拿出頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)final Node<E> next = f.next;//幫助 GC 回收頭節(jié)點(diǎn)f.item = null;f.next = null;// 頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)成為頭節(jié)點(diǎn)first = next;//如果 next 為空,表明鏈表為空if (next == null)last = null;//鏈表不為空,頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向 nullelsenext.prev = null;//修改鏈表大小和版本size--;modCount++;return element;}
從尾部刪除節(jié)點(diǎn)的代碼也是類似的,這里就不再詳細(xì)解釋了。
從源碼中我們可以了解到,鏈表結(jié)構(gòu)的節(jié)點(diǎn)新增、刪除都非常簡單,僅僅把前后節(jié)點(diǎn)的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。
在鏈表查詢某一個(gè)節(jié)點(diǎn)是比較慢的,因?yàn)樾枰€(gè)循環(huán)查找才行,我們看看 LinkedList 的源碼是如何尋找節(jié)點(diǎn)的:
// 根據(jù)鏈表索引位置查詢節(jié)點(diǎn)Node<E> node(int index) {// 如果 index 處于隊(duì)列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。if (index < (size >> 1)) {Node<E> x = first;// 直到 for 循環(huán)到 index 的前一個(gè) node 停止for (int i = 0; i < index; i++)x = x.next;return x;} else {// 如果 index 處于隊(duì)列的后半部分,從尾開始找Node<E> x = last;// 直到 for 循環(huán)到 index 的后一個(gè) node 停止for (int i = size - 1; i > index; i--)x = x.prev;return x;}}
從源碼中我們可以發(fā)現(xiàn),LinkedList 并沒有采用從頭循環(huán)到尾的做法,而是采取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是后半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環(huán)的次數(shù)至少降低了一半,提高了查找的性能,這種思想值得我們借鑒。
因?yàn)?LinkedList 要實(shí)現(xiàn)雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行了,因?yàn)?Iterator 只支持從頭到尾的訪問。Java 新增了一個(gè)迭代接口,叫做:ListIterator,這個(gè)接口提供了向前和向后的迭代方法,如下所示:
| 迭代順序 | 方法 |
|---|---|
| 從尾到頭迭代方法 | hasPrevious、previous、previousIndex |
| 從頭到尾迭代方法 | hasNext、next、nextIndex |
LinkedList 實(shí)現(xiàn)了 ListIterator 接口,如下圖所示:
// 雙向迭代器private class ListItr implements ListIterator<E> {private Node<E> lastReturned;//上一次執(zhí)行 next() 或者 previos() 方法時(shí)的節(jié)點(diǎn)位置private Node<E> next;//下一個(gè)節(jié)點(diǎn)private int nextIndex;//下一個(gè)節(jié)點(diǎn)的位置//expectedModCount:期望版本號;modCount:目前最新版本號private int expectedModCount = modCount;…………}
我們先來看下從頭到尾方向的迭代:
// 判斷還有沒有下一個(gè)元素public boolean hasNext() {return nextIndex < size;// 下一個(gè)節(jié)點(diǎn)的索引小于鏈表的大小,就有}// 取下一個(gè)元素public E next() {//檢查期望版本號有無發(fā)生變化checkForComodification();if (!hasNext())//再次檢查throw new NoSuchElementException();// next 是當(dāng)前節(jié)點(diǎn),在上一次執(zhí)行 next() 方法時(shí)被賦值的。// 第一次執(zhí)行時(shí),是在初始化迭代器的時(shí)候,next 被賦值的lastReturned = next;// next 是下一個(gè)節(jié)點(diǎn)了,為下次迭代做準(zhǔn)備next = next.next;nextIndex++;return lastReturned.item;}
上述源碼的思路就是直接取當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),而從尾到頭迭代稍微復(fù)雜一點(diǎn),如下:
// 如果上次節(jié)點(diǎn)索引位置大于 0,就還有節(jié)點(diǎn)可以迭代public boolean hasPrevious() {return nextIndex > 0;}// 取前一個(gè)節(jié)點(diǎn)public E previous() {checkForComodification();if (!hasPrevious())throw new NoSuchElementException();// next 為空場景:1:說明是第一次迭代,取尾節(jié)點(diǎn)(last);2:上一次操作把尾節(jié)點(diǎn)刪除掉了// next 不為空場景:說明已經(jīng)發(fā)生過迭代了,直接取前一個(gè)節(jié)點(diǎn)即可(next.prev)lastReturned = next = (next == null) ? last : next.prev;// 索引位置變化nextIndex--;return lastReturned.item;}
這里復(fù)雜點(diǎn)體現(xiàn)在需要判斷 next 不為空和為空的場景,代碼注釋中有詳細(xì)的描述。
迭代器刪除
LinkedList 在刪除元素時(shí),也推薦通過迭代器進(jìn)行刪除,刪除過程如下:
public void remove() {checkForComodification();// lastReturned 是本次迭代需要?jiǎng)h除的值,分以下空和非空兩種情況:// lastReturned 為空,說明調(diào)用者沒有主動(dòng)執(zhí)行過 next() 或者 previos(),直接報(bào)錯(cuò)// lastReturned 不為空,是在上次執(zhí)行 next() 或者 previos()方法時(shí)賦的值if (lastReturned == null)throw new IllegalStateException();Node<E> lastNext = lastReturned.next;//刪除當(dāng)前節(jié)點(diǎn)unlink(lastReturned);// next == lastReturned 的場景分析:從尾到頭遞歸順序,并且是第一次迭代,并且要?jiǎng)h除最后一個(gè)元素的情況下// 這種情況下,previous() 方法里面設(shè)置了 lastReturned = next = last,所以 next 和 lastReturned會(huì)相等if (next == lastReturned)// 這時(shí)候 lastReturned 是尾節(jié)點(diǎn),lastNext 是 null,所以 next 也是 null,這樣在 previous() 執(zhí)行時(shí),發(fā)現(xiàn) next 是 null,就會(huì)把尾節(jié)點(diǎn)賦值給 nextnext = lastNext;elsenextIndex--;lastReturned = null;expectedModCount++;}
LinkedList 適用于要求有順序、并且會(huì)按照順序進(jìn)行迭代的場景,主要是依賴于底層的鏈表結(jié)構(gòu),在面試中的頻率還是蠻高的,相信理清楚上面的源碼后,應(yīng)對面試應(yīng)該沒有問題。