首頁(yè)技術(shù)文章正文

linkedlist底層是雙向鏈表嗎?

更新時(shí)間:2020-10-13 來源:黑馬程序員 瀏覽量:

LinkedList 集合底層是一個(gè)雙向鏈表結(jié)構(gòu),具有增刪快,查詢慢的忒點(diǎn),內(nèi)部包含大量操作首尾元素的方法。適用于集合元素先入先出和先入后出的場(chǎng)景,在隊(duì)列源碼中被頻繁使用。

一、LinkedList整體架構(gòu)

LinkedList 底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)雙向鏈表,整體結(jié)構(gòu)如下圖所示:
LinkedList結(jié)構(gòu)圖
上圖代表了一個(gè)雙向鏈表結(jié)構(gòu),可以通過前面的節(jié)點(diǎn)找到后面的節(jié)點(diǎn),也可以通過后面的節(jié)點(diǎn)找到前面的節(jié)點(diǎn)

相關(guān)概念:

  • Node: 代表鏈中的每個(gè)節(jié),Node 的 prev 屬性,代表前一個(gè)節(jié)點(diǎn)的地址,Node 的next 屬性,代表后一個(gè)節(jié)點(diǎn)的地址;
  • first :代表雙向鏈表的頭節(jié)點(diǎn),它的前一個(gè)節(jié)點(diǎn)是 null。
  • last: 代表雙向鏈表的尾節(jié)點(diǎn),它的后一個(gè)節(jié)點(diǎn)是 null;
  • 如果鏈表中沒有任何數(shù)據(jù)時(shí),頭節(jié)點(diǎn)first 和 尾節(jié)點(diǎn)last 是同一個(gè)節(jié)點(diǎn),前后指向都是 null;
  • 因?yàn)長(zhǎng)inkedList集合是個(gè)雙向鏈表,所以機(jī)器只要有足夠強(qiáng)大的內(nèi)存,對(duì)于LinkedList集合而言是沒有大小限制的。

鏈表中的元素被稱為Node, Node被定義成私有靜態(tài)內(nèi)部類,內(nèi)容如下 :

  1. private static class Node<E> {
  2. E item;// 節(jié)點(diǎn)中存儲(chǔ)的數(shù)據(jù)
  3. Node<E> next; // 下一個(gè)節(jié)點(diǎn)的地址
  4. Node<E> prev; // 前一個(gè)節(jié)點(diǎn)的地址
  5. // 構(gòu)造方法初始化參數(shù)順序分別是:前一個(gè)節(jié)點(diǎn)的地址值、當(dāng)前節(jié)點(diǎn)中存儲(chǔ)的數(shù)據(jù)、后一個(gè)節(jié)點(diǎn)的地址值
  6. Node(Node<E> prev, E element, Node<E> next) {
  7. this.item = element;
  8. this.next = next;
  9. this.prev = prev;
  10. }
  11. }

二、LinkedList 源碼解析

2.1 添加(新增)節(jié)點(diǎn)

如果想在LinkedList集合中添加節(jié)點(diǎn),我們把新加入的節(jié)點(diǎn)添加到鏈表頭部,也可以把新加入的節(jié)點(diǎn)添加添加到鏈表尾部,add 方法默認(rèn)是從尾部開始添加,addFirst 方法是從頭部開始添加,下面分別來看下兩種不同的添加方式:

從尾部添加(add)

  1. // 從尾部開始添加節(jié)點(diǎn)
  2. void linkLast(E e) {
  3. // 把尾節(jié)點(diǎn)數(shù)據(jù)暫存
  4. final Node<E> l = last;
  5. // 新建新的節(jié)點(diǎn),初始化入?yún)⒑x:
  6. // l 是新節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn),當(dāng)前值是尾節(jié)點(diǎn)值
  7. // e 表示當(dāng)前新增節(jié)點(diǎn),當(dāng)前新增節(jié)點(diǎn)后一個(gè)節(jié)點(diǎn)是 null
  8. final Node<E> newNode = new Node<>(l, e, null);
  9. // 新建節(jié)點(diǎn)添加到尾部
  10. last = newNode;
  11. //如果鏈表為空(l 是尾節(jié)點(diǎn),尾節(jié)點(diǎn)為空,鏈表即空),頭部和尾部是同一個(gè)節(jié)點(diǎn),都是新建的節(jié)點(diǎn)
  12. if (l == null)
  13. first = newNode;
  14. //否則把前尾節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),指向當(dāng)前尾節(jié)點(diǎn)。
  15. else
  16. l.next = newNode;
  17. size++;//集合元素?cái)?shù)量增加1
  18. modCount++;//實(shí)際修改次數(shù)增加1
  19. }

從源碼上來看,尾部添加節(jié)點(diǎn)比較簡(jiǎn)單.

從頭部添加(addFirst)

  1. // 從頭部添加
  2. private void linkFirst(E e) {
  3. // 頭節(jié)點(diǎn)賦值給臨時(shí)變量
  4. final Node<E> f = first;
  5. // 新建節(jié)點(diǎn),前一個(gè)節(jié)點(diǎn)指向null,e 是新建節(jié)點(diǎn),f 是新建節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),目前值是頭節(jié)點(diǎn)的值
  6. final Node<E> newNode = new Node<>(null, e, f);
  7. // 新建節(jié)點(diǎn)成為頭節(jié)點(diǎn)
  8. first = newNode;
  9. // 頭節(jié)點(diǎn)為空,就是鏈表為空,頭尾節(jié)點(diǎn)是一個(gè)節(jié)點(diǎn)
  10. if (f == null)
  11. last = newNode;
  12. //上一個(gè)頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn)
  13. else
  14. f.prev = newNode;
  15. size++;
  16. modCount++;
  17. }

頭部添加節(jié)點(diǎn)和尾部添加節(jié)點(diǎn)非常類似,只是前者是移動(dòng)頭節(jié)點(diǎn)的 prev 指向,后者是移動(dòng)尾節(jié)點(diǎn)的 next 指向。

2.2 刪除節(jié)點(diǎn)

節(jié)點(diǎn)刪除的方式和添加類似,我們可以選擇從頭部刪除,也可以選擇從尾部刪除,刪除操作會(huì)把節(jié)點(diǎn)的值,前后指向節(jié)點(diǎn)都置為 null,幫助 GC 進(jìn)行回收。

從頭部刪除

  1. //從頭刪除節(jié)點(diǎn) f 是鏈表頭節(jié)點(diǎn)
  2. private E unlinkFirst(Node<E> f) {
  3. // 拿出頭節(jié)點(diǎn)的值,作為方法的返回值
  4. final E element = f.item;
  5. // 拿出頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
  6. final Node<E> next = f.next;
  7. //幫助 GC 回收頭節(jié)點(diǎn)
  8. f.item = null;
  9. f.next = null;
  10. // 頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)成為頭節(jié)點(diǎn)
  11. first = next;
  12. //如果 next 為空,表明鏈表為空
  13. if (next == null)
  14. last = null;
  15. //鏈表不為空,頭節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向 null
  16. else
  17. next.prev = null;
  18. //修改鏈表大小和版本
  19. size--;
  20. modCount++;
  21. return element;
  22. }

從尾部刪除節(jié)點(diǎn)的代碼也是類似的,這里就不再詳細(xì)解釋了。

從源碼中我們可以了解到,鏈表結(jié)構(gòu)的節(jié)點(diǎn)新增、刪除都非常簡(jiǎn)單,僅僅把前后節(jié)點(diǎn)的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。

2.3 查詢節(jié)點(diǎn)

在鏈表查詢某一個(gè)節(jié)點(diǎn)是比較慢的,因?yàn)樾枰€(gè)循環(huán)查找才行,我們看看 LinkedList 的源碼是如何尋找節(jié)點(diǎn)的:

  1. // 根據(jù)鏈表索引位置查詢節(jié)點(diǎn)
  2. Node<E> node(int index) {
  3. // 如果 index 處于隊(duì)列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
  4. if (index < (size >> 1)) {
  5. Node<E> x = first;
  6. // 直到 for 循環(huán)到 index 的前一個(gè) node 停止
  7. for (int i = 0; i < index; i++)
  8. x = x.next;
  9. return x;
  10. } else {// 如果 index 處于隊(duì)列的后半部分,從尾開始找
  11. Node<E> x = last;
  12. // 直到 for 循環(huán)到 index 的后一個(gè) node 停止
  13. for (int i = size - 1; i > index; i--)
  14. x = x.prev;
  15. return x;
  16. }
  17. }

從源碼中我們可以發(fā)現(xiàn),LinkedList 并沒有采用從頭循環(huán)到尾的做法,而是采取了簡(jiǎn)單二分法,首先看看 index 是在鏈表的前半部分,還是后半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環(huán)的次數(shù)至少降低了一半,提高了查找的性能,這種思想值得我們借鑒。

2.4 迭代器

因?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 接口,如下圖所示:

  1. // 雙向迭代器
  2. private class ListItr implements ListIterator<E> {
  3. private Node<E> lastReturned;//上一次執(zhí)行 next() 或者 previos() 方法時(shí)的節(jié)點(diǎn)位置
  4. private Node<E> next;//下一個(gè)節(jié)點(diǎn)
  5. private int nextIndex;//下一個(gè)節(jié)點(diǎn)的位置
  6. //expectedModCount:期望版本號(hào);modCount:目前最新版本號(hào)
  7. private int expectedModCount = modCount;
  8. …………
  9. }

我們先來看下從頭到尾方向的迭代:

  1. // 判斷還有沒有下一個(gè)元素
  2. public boolean hasNext() {
  3. return nextIndex < size;// 下一個(gè)節(jié)點(diǎn)的索引小于鏈表的大小,就有
  4. }
  5. // 取下一個(gè)元素
  6. public E next() {
  7. //檢查期望版本號(hào)有無發(fā)生變化
  8. checkForComodification();
  9. if (!hasNext())//再次檢查
  10. throw new NoSuchElementException();
  11. // next 是當(dāng)前節(jié)點(diǎn),在上一次執(zhí)行 next() 方法時(shí)被賦值的。
  12. // 第一次執(zhí)行時(shí),是在初始化迭代器的時(shí)候,next 被賦值的
  13. lastReturned = next;
  14. // next 是下一個(gè)節(jié)點(diǎn)了,為下次迭代做準(zhǔn)備
  15. next = next.next;
  16. nextIndex++;
  17. return lastReturned.item;
  18. }

上述源碼的思路就是直接取當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),而從尾到頭迭代稍微復(fù)雜一點(diǎn),如下:

  1. // 如果上次節(jié)點(diǎn)索引位置大于 0,就還有節(jié)點(diǎn)可以迭代
  2. public boolean hasPrevious() {
  3. return nextIndex > 0;
  4. }
  5. // 取前一個(gè)節(jié)點(diǎn)
  6. public E previous() {
  7. checkForComodification();
  8. if (!hasPrevious())
  9. throw new NoSuchElementException();
  10. // next 為空?qǐng)鼍埃?:說明是第一次迭代,取尾節(jié)點(diǎn)(last);2:上一次操作把尾節(jié)點(diǎn)刪除掉了
  11. // next 不為空?qǐng)鼍埃赫f明已經(jīng)發(fā)生過迭代了,直接取前一個(gè)節(jié)點(diǎn)即可(next.prev)
  12. lastReturned = next = (next == null) ? last : next.prev;
  13. // 索引位置變化
  14. nextIndex--;
  15. return lastReturned.item;
  16. }

這里復(fù)雜點(diǎn)體現(xiàn)在需要判斷 next 不為空和為空的場(chǎng)景,代碼注釋中有詳細(xì)的描述。

迭代器刪除

LinkedList 在刪除元素時(shí),也推薦通過迭代器進(jìn)行刪除,刪除過程如下:

  1. public void remove() {
  2. checkForComodification();
  3. // lastReturned 是本次迭代需要?jiǎng)h除的值,分以下空和非空兩種情況:
  4. // lastReturned 為空,說明調(diào)用者沒有主動(dòng)執(zhí)行過 next() 或者 previos(),直接報(bào)錯(cuò)
  5. // lastReturned 不為空,是在上次執(zhí)行 next() 或者 previos()方法時(shí)賦的值
  6. if (lastReturned == null)
  7. throw new IllegalStateException();
  8. Node<E> lastNext = lastReturned.next;
  9. //刪除當(dāng)前節(jié)點(diǎn)
  10. unlink(lastReturned);
  11. // next == lastReturned 的場(chǎng)景分析:從尾到頭遞歸順序,并且是第一次迭代,并且要?jiǎng)h除最后一個(gè)元素的情況下
  12. // 這種情況下,previous() 方法里面設(shè)置了 lastReturned = next = last,所以 next 和 lastReturned會(huì)相等
  13. if (next == lastReturned)
  14. // 這時(shí)候 lastReturned 是尾節(jié)點(diǎn),lastNext 是 null,所以 next 也是 null,這樣在 previous() 執(zhí)行時(shí),發(fā)現(xiàn) next 是 null,就會(huì)把尾節(jié)點(diǎn)賦值給 next
  15. next = lastNext;
  16. else
  17. nextIndex--;
  18. lastReturned = null;
  19. expectedModCount++;
  20. }

總結(jié)

LinkedList 適用于要求有順序、并且會(huì)按照順序進(jìn)行迭代的場(chǎng)景,主要是依賴于底層的鏈表結(jié)構(gòu),在面試中的頻率還是蠻高的,相信理清楚上面的源碼后,應(yīng)對(duì)面試應(yīng)該沒有問題。

猜你喜歡

LinkedList和ArrayList對(duì)比各有什么優(yōu)勢(shì)?
傳智播客java培訓(xùn)課程

分享到:
在線咨詢 我要報(bào)名
和我們?cè)诰€交談!