戀上鍊表之深入LinkedList源碼分析

背景摘要:在List集合中,我們最熟悉的就是ArrayList與LinkedList。一談到它們我們第一個反應就是ArrayList查詢快,LinkedList增刪快,所以在增刪操作頻繁的場景下適合使用LinkedList,而在其他場景ArrayList就夠用了。那麼前篇我們提到了ArrayList源碼分析,今天再來細聊LinkedList。

目錄

一、戀上鍊表

單向鏈表

雙向鏈表

二、LinkedList

2.3、LinkedList集合新增快的原因

2.4、LinkedList刪除背後的歷程

2.5、LinkedList爲什麼查詢慢?

折半查找


一、戀上鍊表

鏈表,它是一種數據結構,分爲單項鍊表與雙向鏈表。而今天要講的LinkedList集合恰好就是雙向鏈表結構的集合。我們要注意,LinkedList集合 不等於鏈表,它只是底層基於鏈表這種數據結構而實現。

首先我們一張圖來簡單瞭解鏈表的單雙向區別。

單向:當前鏈表節點(後面簡稱爲節點或元素)只鏈接下一個節點。1鏈接2,2鏈接3。

雙向:當前節點與上、下一個節點都有鏈接。1鏈接2, 2鏈接1、3, 3鏈接2。

1.1、單向鏈表

那麼我們再仔細琢磨單向鏈表是如何鏈接下一個元素,爲什麼稱作單向。

首先畫出單向鏈表的數據結構,每個節點分爲Item(當前元素值)與Next(下一個元素)兩部分。那麼我們得知在每一個節點中都會包含下一個節點,如1包含2。如果沒有下一個節點,如3沒有,那麼值爲NULL,即代表3爲最後一個元素。

或許聰明的你會回憶發現LinkedList使用的是雙向鏈表,正在疑惑哪裏使用到單向鏈表呢?答案是HashMap。

我們談到HashMap的組成,想到的就是數組+鏈表。那來一起看看JDK1.7中HashMap的一段源碼,該源碼中是一個鏈表結構的靜態內部類,可以看到當前的結構與上圖類似。

那麼再看源碼圖中Entry<K,V>相當於一個節點,Next爲當前節點的下一個節點。固我們得知,HashMap是由鏈表+數組實現,且鏈表的結構爲單向鏈表

隨後躲在牆角的HashMap:

 

1.2、雙向鏈表

瞭解完單向鏈表之後,我們再來看看雙向鏈表。

首先雙向鏈表比單向鏈表多了個prev(當前節點的上一個節點),如果當前元素是頭部節點(First),那麼prev值爲NULL值,如圖中節點1的指向。節點2前後都有數據,那麼prev爲1,next爲3。節點3爲尾部節點(Last)固next值爲NULL。

這也就說明雙向鏈表特徵爲前後相呼應。

那麼雙向列表的例子就是今天要細說的LinkedList主角了。

LinkedList:

二、LinkedList

集合源碼分析案例項目碼雲地址:https://gitee.com/yiang-hz/gather

當前LinkedList類地址:https://gitee.com/yiang-hz/gather/tree/master/list/src/main/java/com/yiang/list

類名:YiangLinkedList,由於貼代碼太長,這裏就只做片段分析。可直接複製源碼在IDEA進行調試。

首先依舊還是上次ArrayList源碼分析時的List接口。

public interface YiangList<E> {
    int size();
    E get(int index);
    boolean add(E e);
    E remove(int index);
}

2.1、成員變量

這裏分析重要的三個成員變量。集合大小size,集合最後一個元素last,集合第一個元素first。也就是雙向鏈表結構圖中的節點1(first)與節點3(last)。

/**
 * 集合大小
 */
transient int size = 0;
/**
 * 集合的最後一個元素
 */
transient YiangLinkedList.Node<E> last;
/**
 * 集合的第一個元素
 */
transient YiangLinkedList.Node<E> first;

2.2、靜態內部類Node<E>

在雙向鏈表中談到的結構分別是prev、item、next。那麼在LinkedList源碼實現靜態內部類中正好是該三項。

private static class Node<E> {
    /** 當前元素 (HZ)*/
    E item;
    /** 當前元素的上一個元素 (HZ)*/
    YiangLinkedList.Node<E> next;
    /** 當前元素的下一個元素 (HZ)*/
    YiangLinkedList.Node<E> prev;
    Node(YiangLinkedList.Node<E> prev, E element, YiangLinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
    Node(E element) {
        this.item = element;
    }
}

所以從該源碼我們得知,LinkedList底層確實是由雙向列表構成。

 

2.3、LinkedList集合新增快的原因

LinkedList我們都知道新增快,那麼爲什麼快?快在哪裏?源碼中怎麼體現的?我們來一一分析。

/**
 * 默認會鏈接元素作爲最後一個元素
 * @param e 元素對象
 * @return 是否添加成功
 */
@Override
public boolean add(E e) {
    addLast(e);
    return true;
}

源碼中add方法默認是調用addLast(e)方法的,也就是默認是從最後一個元素節點追加。

/**
 * 追加元素,位於最後一個之後
 * @param e 元素
 */
void addLast(E e){
    //獲取最後一個元素
    final YiangLinkedList.Node<E> l = last;
    //創建當前元素
    final YiangLinkedList.Node<E> node = new YiangLinkedList.Node<>(l, e, null);
    //最後一個元素的值等於該元素
    last = node;
    //如果之前的最後一個元素爲空,那麼代表當前集合爲空,node即成爲最後一個元素
    if (null == l){
        first = node;
    } else {
        //否則將當前元素設置爲之前的最後一個元素的下一個值
        l.next = node;
    }
    //大小加1
    size++;
}

添加的實現步驟中

首先獲取到之前的最後一個元素L,其次將當前存放的值創建成節點node,那麼node的上一個節點爲L,並將node設置爲last節點。當然這裏需要判斷一種情況,如果L爲空,那麼證明當前集合是空集合,固first也要設置爲node節點。否則代表L不爲空,那麼需要將L的下一個節點設置爲Node。最後進行size+1。

圖示:e爲添加的元素

1.當前集合爲空情況

2.當前集合包含元素情況

那麼我們得知,LinkedList新增快的主要原因是,在添加時只需要獲取最後一個節點Last,並更改Last節點和當前創建節點的引用鏈關係即可。所以相對於ArrayList的數組重新複製要快很多。

2.4、LinkedList刪除背後的歷程

說完了新增快的原因,我們再來看看刪除快的原因以及它的經歷過程。

@Override
public E remove(int index) {
    //檢測下標是否越界
    checkElementIndex(index);
    //刪除
    return unlink(node(index));
}

刪除需要檢測下標是否越界(當前傳入的index是否>0,並且是否小於size最大值)。當然還要找到需要刪除的元素,再開始刪除方法。

/**
 * 刪除元素 通過鏈表的結構鏈接邏輯實現
 */
E unlink(Node<E> x) {
    //當前元素的值, x爲當前元素
    final E element = x.item;
    //當前元素的上一個元素
    YiangLinkedList.Node<E> prev = x.prev;
    //當前元素的下一個元素
    YiangLinkedList.Node<E> next = x.next;
    //刪除時,如果當前的元素的上一個元素爲NULL,那麼代表當前元素爲第一個元素。
    if (null == prev){
        //所以刪除後設置下一個元素爲first
        first = next;
    } else{
        //否則,代表該元素的上一個元素不爲空,那麼將上一個元素的下一個元素設置爲 當前的next
        prev.next = next;
        // 如果上一個元素不爲空,那麼刪除後需將當前元素的上一個元素置空
        x.prev = null;
    }
    //如果當前的元素的下一個元素爲NULL,那麼代表當前元素爲最後一個元素。
    if (null == next){
        //所以刪除後設置上一個元素爲last
        last = prev;
    } else{
        //當前元素的下一個元素不爲空,那麼需要將下一個元素的上一個元素,設置爲 當前的prev
        next.prev = prev;
        // 如果下一個元素不爲空,那麼刪除後需將當前元素的下一個元素置空
        x.next = null;
    }
    //這裏將值賦值爲空,讓GC進行回收處理
    x.item = null;
    //將大小size - 1
    size--;
    return element;
}

刪除主要分爲三種情況:

  1. 刪除的節點爲頭部節點
  2. 刪除的節點爲尾部節點
  3. 刪除的節點前後有相鄰節點。

光看代碼和文字是很枯燥乏味的,我們直接看實現原理圖,再去看代碼是怎麼實現,思路就很清晰了。

第一種情況:

操作步驟:首先是刪除節點1,其次把節點1的下一個節點,也就是節點2的   prev值  設置爲NULL。並將節點2設置爲頭部節點。

第二種情況:

操作步驟:實現與第一種情況類似,首先是刪除節點3,其次把節點3的上一個節點,也就是節點2的 next值 設置爲NULL。並將節點2設置爲尾部節點。

第三種情況:

這種情況最形象的例子就好比三個人(ABC)手牽着手,中間的那個人(B)離開了。那麼A要牽C手,C要牽A手。

操作步驟:

刪除節點2 --> 獲取節點2上一個節點 節點1 。獲取節點2的下一個節點 節點3。 --> 將節點1的 next值 設置爲節點3,將節點3的prev值 設置爲節點1。


那麼當刪除原理圖看完之後,我們再去看代碼中的設計,真是妙啊!

 

2.5、LinkedList爲什麼查詢慢?

我們知道,在ArrayList中,查詢快的原因是它底層通過數組實現,直接根據索引找到對應的元素即可。那麼在LinkedList集合中是元素一個一個鏈接起來,沒有索引概念的,那麼當我們去進行查找時,就只能根據第一個元素,然後一個一個數位置,數到對應位置數量時才能找到需要查詢的元素。

那麼如果我們要從以下數據中查找 值爲3,也就是第三個節點時。通常情況下指針會從1-3一個一個的去查找,非常耗時,且這裏相當於Mysql的全表檢索,是不是神似根據ID去查詢某條記錄呢?

當然,LinkedList查找並不完全是這樣的,它做了一個優化,稱爲折半查找。

折半查找

我們看完類似於上圖Mysql的全表檢索之後,再來細說LinkedList在此做了什麼優化。

/**
 * 根據索引查找到元素
 * @param index 索引
 * @return 元素
 */
YiangLinkedList.Node<E> node(int index) {
    //LinkedList底層通過next與prev去連接元素,那麼查詢時,需要一個一個鏈接
    //相當於一次全表查詢,但LinkedList做了一個小優化,那就是  折半查詢
    //對比:鏈表 -> 全表查詢   數組 -> 索引查詢
    //如果索引下標 小於 集合大小除2   index < size / 2   那麼從 0-5折半查詢
    // 否之則從 10 - 6反向查詢 實現折半查詢
    // 1.例如索引爲0 size爲10: 0 < 10 / 2,那麼node等於第一個值,並且不會進行循環,直接返回第一個元素
    // 2.例如索引爲1 size爲10: 1 < 10 / 2,那麼node等於 第二個值,循環一次。並且返回
    if (index < (size >> 1)){
        YiangLinkedList.Node<E> node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    } else{
        //3.例如索引爲6 size爲10: 6 > 10 / 2,那麼node等於倒數第4個值,循環4次。並且返回
        YiangLinkedList.Node<E> node = last;
        //size - 1 拿到最後一個元素, 因爲下標是從0開始,固需要-1
        for (int i = size - 1; i > index ; i--) {
            node = node.prev;
        }
        return node;
    }
}

代碼中設計很巧妙,那麼我們簡單畫兩個圖來說明。

從圖中可知,通過算法在查找之前判斷從那邊開始查找更快,從而選擇從頭部節點或者尾部節點開始查找的方式即爲折半查找。而LinkedList查找源碼正是使用該種方法去進行查詢。


總結

首先LinkedList是又鏈表中的雙向鏈表實現,增刪快的原理是只需要操作相鄰兩個數據即可。刪除也需要先查找到當前節點。查詢是通過類似Mysql的全表檢索,雖進行了折半查找,但效率較ArrayList來說還是很低的。最終由於查詢是一個一個遍歷,導致查詢速度過慢,所以日常使用的集合通常是ArrayList,而不是LinkedList。固LinkedList適合在特殊操作頻繁場景可用來提高程序運行效率。

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