LinkedList為什么增刪快、查詢慢


List家族中共兩個常用的對象ArrayList和LinkedList,具有以下基本特征。
  • ArrayList:長於隨機訪問元素,中間插入和移除元素比較慢,在插入時,必須創建空間並將它的所有引用向前移動,這會隨着ArrayList的尺寸增加而產生高昂的代價,底層由數組支持。
  • LinkedList:通過代價較低的在List中間進行插入和刪除操作,只需要鏈接新的元素,而不必修改列表中剩余的元素,無論列表尺寸如何變化,其代價大致相同,提供了優化的順序訪問,隨機訪問相對較慢,特性較ArrayList更大,而且還添加了可以使其作為棧、隊列或雙端隊列的方法,底層由雙向鏈表實現。
 
上面的特性接觸了Java開發的人都知道,初級Java工程師面試更是必問的點,至於更加深入的知識點:為什么會有這樣的特性? 底層是怎么實現的?往往到這里,大部分程序員都答不出來,筆者當初面試那會也是被這里吊打,被面試官無情的嘲諷,當時的情形是這樣的
當時那個臊的啊,拿回簡歷出了公司坐在深圳某天橋思考了許久的人生。。。。。。。。
 
抽空看了下LinkedList的實現,整理成博文的形式,能給自己增加理解,也希望能幫助廣大碼農。
LinkedList本質是一個雙向鏈表,由一個個的Node對象組成,如下圖
LinkedList由一個個Node組成,每一個Node持有前后節點的引用,也可以稱之為指針,看下Node的結構,Node有當前元素對象、前一個節點信息、后一個節點信息三個屬性,構造函數也是由這三個屬性去組成,在LinkedList的添加方法中,會創建一個Node對象,持有前后節點的信息,添加到最后節點之后。
//關於Node構造函數
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
 
先看下LinkedList中共的幾個屬性,下面三個屬性分別描述LinkedList的尺寸、第一個元素、最后一個元素,這三個元素始終貫穿着在LinkedList的使用當中

//transient 保證以下幾個屬性不被序列化
/**
 * The number of times this list has been <i>structurally modified</i>.
 * 該字段表示list結構上被修改的次數。結構上的修改指的是那些改變了list的長度
 * 大小或者使得遍歷過程中產生不正確的結果的其它方式。
 * 
 */
protected transient int modCount = 0;

transient int size = 0;
/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;
/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;
 
再看LinkedList的添加實現,可以添加為null的對象

public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
 * Links e as last element.
 */
void linkLast(E e) {
     final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
看上面的源碼可以知道,當LinkedList添加一個元素時,會默認的往LinkedList最后一個節點后添加,具體步驟為
  • 獲得最后一個節點last作為當前節點l
  • 用當前節點l、添加參數e、null創建一個新的Node對象
  • 將新創建的Node對象鏈接到最后節點,也就是last
  • 如果當前的LinkedList為空,那么添加的node就是first,也是last
  • 當前LinkedList的size+1,表示結構改變次數的對象modCount+1
整個添加過程中,系統只做了兩件事情,添加一個新的節點,然后保存該節點和前節點的引用關系。

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

LinkedList的刪除中,會根據傳遞進來的參數進行null判斷,因為在LinkedList的元素移除中,null和非null的處理不一樣,對於nul使用==去判斷是否匹配,對於非null使用.equals(Object o)去判斷,至於==和equals的區別,讀者自行百度。

null判斷之后,傳入當前節點調用unlink(E e)方法,我們可以從方法名中看出點東西,可能設計者也是為了方便讀者閱讀源碼,可以理解為“放開鏈接”,也可以反映出LinkedList保存數據的方式,一個個元素相互鏈接而成。
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev; 
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}
通過閱讀上面的源碼,LinkedList的刪除中,也是改變節點之間的引用關系去實現的,具體邏輯整理如下:
  • 如果前一個節點prev為null,即第一個節點元素,則鏈表first = x下一個節點;
  • 如果前一個節點prev不為null,即不是第一個節點元素,則將當前節點的next賦值給prev.next,x.prev置為null,也就是當前節點x的prev和next和當前鏈表中的其他元素不存在任何聯系;
  • 如果下一個節點next為null,即為最后一個元素,則鏈表last = x前一個節點;
  • 如果下一個節點next不為null,即不為最后一個元素,則將當前節點的prev賦值給next.prev,同樣當前節點x的prev和next和當前鏈表中的其他元素不存在任何聯系;
 
LinkedList的查詢實現源碼如下,

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}
Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
通過閱讀源碼,我們也可以解讀出LinkedList的查詢邏輯
  • 根據傳入的index去判斷是否為LinkedList中的元素,判斷邏輯為index是否在0和size之間,如果在則調用node(index)方法,否則拋出IndexOutOfBoundsException;
  • 調用node(index)方法,將size右移1位,即size/2,判斷傳入的size在LinkedList的前半部分還是后半部分
    • 如果在前半部分,即index < size/2,則從fisrt節點開始遍歷匹配
    • 如果在后半部分,即index > size/2,則從last節點開始遍歷匹配
可以看出,如果LinkedList鏈表size越大,則遍歷的時間越長,查詢所需的時間也越長。
 
應該可以將LinkedList的添加、刪除、查詢給說清楚,如果讀者還有不清楚或者覺得有什么不對的地方,歡迎各位指正或者入群交流。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM