LinkedList常用方法分析

LinkedList概覽

與ArrayList一樣,LinkedList也實現了List接口。ArrayList由於基於數組,在中間刪除元素或插入元素的操作中,效率較低。而LinkedList適合於修改較頻繁的場景、集合元素先入先出和先入後出的場景(隊列)。
ArrayList的底層數據結構,是一個雙向鏈表。如下圖所示:
image.png
LinkedList中,有以下重要概念:
LinkedList中每個節點,被稱爲Node。
prev 即 前驅節點。
next 即 後繼節點。
first 是 頭節點,它的前驅節點恆爲null
last 是 尾節點,它的後繼節點恆爲null
當LinkedList 爲空、或只有一個數據時,first=last

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;
    }
}

類介紹(註釋)

類註釋大致內容如下:

  1. 允許 null 值,實現了List接口的所有方法,並實現了_Deque_接口
  2. 所有的操作結果,都可以參考雙向鏈表Doubly-linked list
  3. 是非線程安全的,多線程情況下,推薦在創建時,使用:_Collections.synchronizedList(new LinkedList(...))_
  4. 增強 for 循環,或者使用迭代器迭代過程中,如果數組大小被改變,會快速失敗,拋出**ConcurrentModificationException**異常。

常用方法源碼

增加元素

add(從尾部添加)

public boolean add(E e) {
    // 直接調用了linkLast方法
    linkLast(e);
    return true;
}

void linkLast(E e) {
    // 備份尾節點
    final Node<E> l = last;
    // 爲插入的元素,創建一個新節點
    final Node<E> newNode = new Node<>(l, e, null);
    // 將 新節點 賦值給 尾節點
    last = newNode;
    
    // 如果操作前,尾節點(備份尾節點)是null,即鏈表爲空,則在鏈表只有一個元素時,first = last = 該元素
    if (l == null)
        first = newNode;
    else
        // 如果操作前,尾節點(備份尾節點)不是null,則將l的next,指向新增的節點。
        l.next = newNode;
    
    // 記錄長度的增加
    size++;
    // 記錄版本號的變更
    modCount++;
}

addFirst(從頭部添加)

public void addFirst(E e) {
    // 直接調用了linkLast方法
    linkFirst(e);
}

private void linkFirst(E e) {
    // 備份 頭節點
    final Node<E> f = first;
    // 爲插入的元素,創建一個新節點
    final Node<E> newNode = new Node<>(null, e, f);
    // 將 新節點 賦值給 頭節點
    first = newNode;
    
    // 如果操作前,頭節點(備份頭節點)是null,即鏈表爲空,則在鏈表只有一個元素時,first = last = 該元素
    if (f == null)
        last = newNode;
    else
        // 如果操作前,頭節點(備份頭節點)不是null,則將f的前驅節點,指向新增的節點。
        f.prev = newNode;
    
    // 記錄長度、版本號的變更
    size++;
    modCount++;
}

addaddFirst非常類似,只是前者是移動頭節點的 prev 指向,後者是移動尾節點的 next 指向。

刪除元素

removeFirst(從頭部刪除)

public E removeFirst() {
    // 頭結點爲null (鏈表爲空時) 拋出錯誤
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    
    // 主要邏輯
    return unlinkFirst(f);
}

// 取消鏈接頭節點,並返回被刪除的元素
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 得到要返回的元素值
    final E element = f.item;
    
    // 備份 第二個節點
    final Node<E> next = f.next;
    
    // 將 頭結點 的元素、後繼節點,都設爲null,以幫助GC (頭結點的前驅節點,本身就爲null)
    f.item = null;
    f.next = null; // help GC
    
    // 將原先的第二個節點,作爲頭節點
    first = next;
    
    // 如果原先的第二個節點爲null (即整個鏈表只有一個節點)
    if (next == null)
        last = null;
    else
        // 如果原先的第二個節點不爲null (即整個鏈表有多於一個的節點)
        // 需要將現在 頭結點的前驅節點,設置爲null
        next.prev = null;
    
    // 記錄長度、版本號的變更
    size--;
    modCount++;
    
    return element;
}

removeLast(從尾部刪除)

public E removeLast() {
    // 尾結點爲null (鏈表爲空時) 拋出錯誤
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    
    // 主要邏輯
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    // 得到要返回的元素值
    final E element = l.item;
    
     // 備份 倒數第二個節點
    final Node<E> prev = l.prev;
    
    // 將 尾結點 的元素、後繼節點,都設爲null,以幫助GC (尾結點的後繼節點,本身就爲null)
    l.item = null;
    l.prev = null; // help GC
    
    // 將原先的倒數第二個節點,作爲尾節點
    last = prev;
    
    // 如果原先的倒數第二個節點爲null (即整個鏈表只有一個節點)
    if (prev == null)
        first = null;
    else
        // 如果原先的倒數第二個節點不爲null (即整個鏈表有多於一個的節點)
        // 需要將現在 尾結點的後繼節點,設置爲null
        prev.next = null;
    
    // 記錄長度、版本號的變更
    size--;
    modCount++;
    
    return element;
}

從源碼中我們可以瞭解到,鏈表結構的節點新增、刪除都非常簡單,僅僅把前驅/後繼節點的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。並不像ArrayList中,刪除、新增時可能還需要移動大量元素。
**

查找元素

鏈表結構中,查找元素是比較耗時的,一般先想到的,是直接遍歷鏈表,這無可厚非。下面是LinkedList實現根據索引來查找元素的方法:

Node<E> node(int index) {
    // assert isElementIndex(index);

    // 如果索引小於 size的一半 (size >> 1 等同 size/2 )
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 查找 從頭開始,到index-1 個Node
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        // 查找 從尾開始,到index+1 個Node
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

從源碼中我們可以發現,LinkedList 並沒有採用從頭循環到尾的做法,而是採取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之從尾開始尋找。通過這種方式,使循環的次數至少降低了一半,提高了查找的性能,這種思想值得我們借鑑。

LinkedList的雙向迭代

Java 中存在一個雙向迭代器的接口:ListIterator,這個接口提供了向前和向後的迭代方法,如下所示:
image.png
LinkedList通過實現ListIterator來實現 雙向迭代的功能。其中有如下重要概念:
private Node lastReturned :上一次執行 next() 或者 previos() 方法時獲得的節點
private Node next :下一個節點
private int nextIndex:下一個節點的索引
expectedModCount:期望版本號
modCount:目前版本號

正向迭代(next)

正向迭代的思路,較爲簡單,就是判斷 hasNext,後使用next方法取下一個元素。

public boolean hasNext() {
    // 如果下一個元素的位置,未超過鏈表大小,則還有 後繼節點
    return nextIndex < size;
}

public E next() {
    // 判斷版本號是否被修改
    checkForComodification();
    
    if (!hasNext())
        throw new NoSuchElementException();

    // 保存本次獲得的元素。(next其實是當前節。點在上一次執行 next() 方法時被賦值的。
    // 第一次執行時,是在創建迭代器的時候,next 被賦值
    lastReturned = next;
    // next.next 是下一個節點,爲下次迭代做準備
    next = next.next;
    
    // 變更當前索引
    nextIndex++;
    return lastReturned.item;
}

// 迭代器的構造方法,next第一次在這裏被賦值
ListItr(int index) {
    // assert isPositionIndex(index);
    next = (index == size) ? null : node(index);
    nextIndex = index;
}

反向迭代(previous)

public boolean hasPrevious() {
    // 如果上次節點索引大於0,則還有前驅節點
    return nextIndex > 0;
}

// 取出前驅節點元素
public E previous() {
    // 判斷版本號是否被修改
    checkForComodification();
    
    if (!hasPrevious())
        throw new NoSuchElementException();
	
    // 這裏比正向迭代複雜。
    // next 爲空場景:1:說明是第一次迭代,取尾節點(last); 2:上一次操作把尾節點刪除掉了
    // next 不爲空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev)
    lastReturned = next = (next == null) ? last : next.prev;
    
    // 變更當前索引
    nextIndex--;
    return lastReturned.item;
}

迭代時刪除(remove)

public void remove() {
    // 判斷版本號是否被修改
    checkForComodification();
	
    // lastReturned 是本次需要刪除的值,分以下空和非空兩種情況:
    // lastReturned 爲空,說明調用者沒有主動執行過 next() 或者 previos(),直接拋出錯誤
    // lastReturned 不爲空,是在上次執行 next() 或者 previos()方法時獲得的值
    
    // ListIterator.remove方法註釋中提到 拋出錯誤,有以下幾種可能
    // 1. 沒有主動執行過 next() 或者 previos()
    // 2. 在最後一次調用next/previous方法後,再調用了remove方法或add方法
    if (lastReturned == null)
        throw new IllegalStateException();

    // 先準備好被刪除節點的後繼節點
    Node<E> lastNext = lastReturned.next;
    // 刪除此節點,其中有 將size--
    unlink(lastReturned);
    
    if (next == lastReturned)
        // next == lastReturned 的場景分析:從尾到頭迭代,是第一次迭代,或者要刪除最後一個元素的情況
    	// 這種情況下,previous() 方法裏面設置了 lastReturned = next = last,所以 next 和 lastReturned會相等
        // 這時候 lastReturned 是尾節點,lastNext 是 null,所以 next 也是 null,這樣在 previous() 執行時,發現 next 是 null,就會把尾節點賦值給 next
        next = lastNext;
    else
        // 其他情況下,需要減小當前索引
        nextIndex--;
    lastReturned = null;
    
    // 修改版本號
    expectedModCount++;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章