深入剖析LinkedList原始碼

初念初戀發表於2022-02-18

簡介

LinkedListArrayList資料結構是完全不一樣的,ArrayList 底層是陣列的結構,而 LinkedList 的底層則是連結串列的結構, 它可以進行高效的插入和移除的操作,它基於的是一個雙向連結串列的結構。

LinkedList的整體結構圖

image-20220210193621308

從圖中也能看出,LinkedList 有好多的Node,並且還有firstlast這兩個變數儲存頭部和尾部節點的資訊;還有就是它不是一個迴圈的雙向連結串列,因為它前後都是null,這個也是我們需要注意的地方。

繼承體系

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{...}

通過繼承體系,我們可以看到 LinkedList 不僅實現了List介面,還實現了QueueDeque介面,所以它既能作為 List 使用,也能作為雙端佇列使用,當然也可以作為棧使用。

原始碼分析

主要屬性

// 元素個數
transient int size = 0;
// 連結串列首節點
transient Node<E> first;
// 連結串列尾節點
transient Node<E> last;

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

構造方法

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    //將集合C中的所有的元素都插入到連結串列中
    addAll(c);
}

新增元素

作為一個雙端佇列,新增元素主要有兩種,一種是在佇列尾部新增元素,一種是在佇列首部新增元素,這兩種形式在LinkedList中主要是通過下面兩個方法來實現的。

// 從佇列首新增元素
private void linkFirst(E e) {
    // 首節點
    final Node<E> f = first;
    // 建立新節點,新節點的next是首節點
    final Node<E> newNode = new Node<>(null, e, f);
    // 讓新節點作為新的首節點
    first = newNode;
    // 判斷是不是第一個新增的元素
    // 如果是就把last也置為新節點
    // 否則把原首節點的prev指標置為新節點
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // 元素個數加1
    size++;
    // 修改次數加1,說明這是一個支援fail-fast的集合
    modCount++;
}

// 從佇列尾新增元素
void linkLast(E e) {
    // 佇列尾節點
    final Node<E> l = last;
    // 建立新節點,新節點的prev是尾節點
    final Node<E> newNode = new Node<>(l, e, null);
    // 讓新節點成為新的尾節點
    last = newNode;
    // 判斷是不是第一個新增的元素
    // 如果是就把first也置為新節點
    // 否則把原尾節點的next指標置為新節點
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 元素個數加1
    size++;
    // 修改次數加1
    modCount++;
}

public void addFirst(E e) {
    linkFirst(e);
}

public void addLast(E e) {
    linkLast(e);
}

// 作為無界佇列,新增元素總是會成功的
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

上面是作為雙端佇列來看,它的新增元素分為首尾新增元素,作為List,是要支援在中間新增元素的,主要是通過下面這個方法實現的。

// 在節點succ之前新增元素
void linkBefore(E e, Node<E> succ) {
    // succ是待新增節點的後繼節點
    // 找到待新增節點的前置節點
    final Node<E> pred = succ.prev;
    // 在其前置節點和後繼節點之間建立一個新節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改後繼節點的前置指標指向新節點
    succ.prev = newNode;
    // 判斷前置節點是否為空
    // 如果為空,說明是第一個新增的元素,修改first指標
    // 否則修改前置節點的next為新節點
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 修改元素個數
    size++;
    // 修改次數加1
    modCount++;
}

// 尋找index位置的節點
Node<E> node(int index) {
    // 因為是雙連結串列
    // 所以根據index是在前半段還是後半段決定從前遍歷還是從後遍歷
    // 這樣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;
    }
}

// 在指定index位置處新增元素
public void add(int index, E element) {
    // 判斷是否越界
    checkPositionIndex(index);
    // 如果index是在佇列尾節點之後的一個位置
    // 把新節點直接新增到尾節點之後
    // 否則呼叫linkBefore()方法在中間新增節點
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

在中間新增元素的方法也很簡單,典型的雙連結串列在中間新增元素的方法。

新增元素的三種方式大致如下圖所示:

qrcode

在佇列首尾新增元素很高效,時間複雜度為O(1)。

在中間新增元素比較低效,首先要先找到插入位置的節點,再修改前後節點的指標,時間複雜度為O(n)。

刪除元素

作為雙端佇列,刪除元素也有兩種方式,一種是佇列首刪除元素,一種是佇列尾刪除元素。

作為List,又要支援中間刪除元素,所以刪除元素一個有三個方法,分別如下。

// 刪除首節點
private E unlinkFirst(Node<E> f) {
    // 首節點的元素值
    final E element = f.item;
    // 首節點的next指標
    final Node<E> next = f.next;
    // 新增首節點的內容,協助GC
    f.item = null;
    f.next = null; // help GC
    // 把首節點的next作為新的首節點
    first = next;
    // 如果只有一個元素,刪除了,把last也置為空
    // 否則把next的前置指標置為空
    if (next == null)
        last = null;
    else
        next.prev = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// 刪除尾節點
private E unlinkLast(Node<E> l) {
    // 尾節點的元素值
    final E element = l.item;
    // 尾節點的前置指標
    final Node<E> prev = l.prev;
    // 清空尾節點的內容,協助GC
    l.item = null;
    l.prev = null; // help GC
    // 讓前置節點成為新的尾節點
    last = prev;
    // 如果只有一個元素,刪除了把first置為空
    // 否則把前置節點的next置為空
    if (prev == null)
        first = null;
    else
        prev.next = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// 刪除指定節點x
E unlink(Node<E> x) {
    // x的元素值
    final E element = x.item;
    // x的前置節點
    final Node<E> next = x.next;
    // x的後置節點
    final Node<E> prev = x.prev;

    // 如果前置節點為空
    // 說明是首節點,讓first指向x的後置節點
    // 否則修改前置節點的next為x的後置節點
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    // 如果後置節點為空
    // 說明是尾節點,讓last指向x的前置節點
    // 否則修改後置節點的prev為x的前置節點
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    // 清空x的元素值,協助GC
    x.item = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// remove的時候如果沒有元素丟擲異常
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
// remove的時候如果沒有元素丟擲異常
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// poll的時候如果沒有元素返回null
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// poll的時候如果沒有元素返回null
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
// 刪除中間節點
public E remove(int index) {
    // 檢查是否越界
    checkElementIndex(index);
    // 刪除指定index位置的節點
    return unlink(node(index));
}

刪除元素的三種方法都是典型的雙連結串列刪除元素的方法,大致流程如下圖所示。

[qrcode

在佇列首尾刪除元素很高效,時間複雜度為O(1)。

在中間刪除元素比較低效,首先要找到刪除位置的節點,再修改前後指標,時間複雜度為O(n)。

前面我們說了,LinkedList是雙端佇列,還記得雙端佇列可以作為棧使用嗎?
package org.example.test;

import java.util.LinkedList;

/**
 * 利用LinkedList來模擬棧
 * 棧的特點:先進後出
 */
public class Test12 {

    private LinkedList<String> linkList = new LinkedList<String>();

    // 壓棧
    public void push(String str){
        linkList.addFirst(str);
    }

    // 出棧
    public String pop(){
        return linkList.removeFirst();
    }

    // 檢視
    public String peek(){
        return linkList.peek();
    }

    // 判斷是否為空
    public boolean isEmpty(){
        return linkList.isEmpty();
    }
}

class Test13 {
    public static void main(String[] args) {
        // 測試棧
        Test12 test12 = new Test12();
        test12.push("我是第1個進去的");
        test12.push("我是第2個進去的");
        test12.push("我是第3個進去的");
        test12.push("我是第4個進去的");
        test12.push("我是第5個進去的");
        // 取出
        while (!test12.isEmpty()){
            String pop = test12.pop();
            System.out.println(pop);
        }
        // 列印結果
        /*我是第5個進去的
        我是第4個進去的
        我是第3個進去的
        我是第2個進去的
        我是第1個進去的*/
    }
}

棧的特性是LIFO(Last In First Out),所以作為棧使用也很簡單,新增刪除元素都只操作佇列首節點即可。

總結

(1)LinkedList是一個以雙連結串列實現的List,因此不存在容量不足的問題,所以沒有擴容的方法。

(2)LinkedList還是一個雙端佇列,具有佇列、雙端佇列、棧的特性。

(3)LinkedList在佇列首尾新增、刪除元素非常高效,時間複雜度為O(1)。

(4)LinkedList在中間新增、刪除元素比較低效,時間複雜度為O(n)。

(5)LinkedList不支援隨機訪問,所以訪問非佇列首尾的元素比較低效。

(6)LinkedList在功能上等於ArrayList + ArrayDeque。

(7)LinkedList是非執行緒安全的。

(8)LinkedList能儲存null值。

經典面試題

 談談ArrayList和LinkedList的區別。

本質的區別來源於兩者的底層實現:ArrayList的底層是陣列,LinkedList的底層是雙向連結串列。

陣列擁有O(1)的查詢效率,可以通過下標直接定位元素;連結串列在查詢元素的時候只能通過遍歷的方式查詢,效率比陣列低。

陣列增刪元素的效率比較低,通常要伴隨拷貝陣列的操作;連結串列增刪元素的效率很高,只需要調整對應位置的指標即可。

以上是陣列和連結串列的通俗對比,在日常的使用中,兩者都能很好地在自己的適用場景發揮作用。

我們常常用ArrayList代替陣列,因為封裝了許多易用的api,而且它內部實現了自動擴容機制,由於它內部維護了一個當前容量的指標size,直接往ArrayList中新增元素的時間複雜度是O(1)的,使用非常方便。

而LinkedList常常被用作Queue佇列的實現類,由於底層是雙向連結串列,能夠輕鬆地提供先入先出的操作。

可以分兩部分答:一個是陣列與連結串列底層實現的不同,另一個是答ArrayList和LinkedList的實現細節。

相關文章