LinkedList原始碼閱讀筆記

三分惡發表於2020-08-20

@



準備

LinkedList是基於雙向連結串列資料結構實現的Java集合(jdk1.8以前基於雙向連結串列),在閱讀原始碼之前,有必要簡單瞭解一下連結串列。

先了解一下連結串列的概念:連結串列是由一系列非連續的節點組成的儲存結構,簡單分下類的話,連結串列又分為單向連結串列和雙向連結串列,而單向/雙向連結串列又可以分為迴圈連結串列和非迴圈連結串列。

  • 單向連結串列:單向連結串列就是通過每個結點的指標指向下一個結點從而連結起來的結構,最後一個節點的next指向null。
  • 單向迴圈連結串列:單向迴圈連結串列和單向列表的不同是,最後一個節點的next不是指向null,而是指向head節點,形成一個“環”。
  • 雙向連結串列:向連結串列是包含兩個指標的,pre指向前一個節點,next指向後一個節點,但是第一個節點head的pre指向null,最後一個節點的tail指向null。
圖一:常見連結串列

在這裡插入圖片描述

  • 雙向迴圈連結串列:向迴圈連結串列和雙向連結串列的不同在於,第一個節點的pre指向最後一個節點,最後一個節點的next指向第一個節點,也形成一個“環”。
圖二:雙向迴圈連結串列

在這裡插入圖片描述


LinkedList繼承體系

在這裡插入圖片描述

通過類圖可以看到,LinkedList不僅實現了List介面,而且實現了現了Queue和Deque介面,所以它既能作為List使用,也能作為雙端佇列使用,也可以作為棧使用。


原始碼分析

節點類

LinkedList有一個靜態內部類,我們看到在雙連結串列中每個節點有前趨、後繼、資料域,節點類實現了這個結構。
在這裡插入圖片描述

    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的主要屬性。first和last對應了雙連結串列的頭結點和尾結點。

在這裡插入圖片描述

//元素個數
transient int size = 0;
//頭結點
transient Node<E> first;
//尾結點
transient Node<E> last;

建構函式

//無參
public LinkedList() {
}

//從其它集合中構造
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

獲取元素

雙向連結串列的靈活處就是連結串列中的一個元素結構就可以向左或者向右開始遍歷查詢需要的元素結構。因此對於一個有序連結串列,查詢的效率比單連結串列高一些。因為,我們可以記錄上次查詢的位置 p,每次查詢時,根據要查詢的值與 p 的大小關係,決定是往前還是往後查詢,所以平均只需要查詢一半的資料。

連結串列查詢示意圖如下:
在這裡插入圖片描述

   //根據索引獲取資料
    public E get(int index) {
        //越界判斷
        checkElementIndex(index);
        //根據index獲取節點
        return node(index).item;
    }

   //根據索引獲取節點
    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;
        }
    }


新增元素

  • 頭插法
    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++;
    }
    

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

  • 尾插法
    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;
        //元素個數加1    
        size++;
        // 修改次數 +1,用於 fail-fast 處理
        modCount++;
    }

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

  public boolean add(E e) {
        linkLast(e);
        return true;
    }

在連結串列頭部和尾部插入時間複雜度都是O(1),頭插法和尾插法的示意圖如下:

在這裡插入圖片描述

  • 中間插入法:中間插入需要找到插入位置節點,改變該節點的前趨和該節點前趨節點的後繼

在這裡插入圖片描述

     //根據索引插入節點
     public void add(int index, E element) {
        //判斷是否越界
        checkPositionIndex(index);
        //未插入
        if (index == size)
            linkLast(element);
        else
            //找到索引位置節點,在該節點前插入新節點
            linkBefore(element, node(index));
    }

   

   // 在節點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的前趨指向
        succ.prev = newNode;
         // 判斷前置節點是否為空
        // 如果為空,說明是第一個新增的元素,頭結點重新賦值
       // 否則修改前置節點的next為新節點
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        //元素個數加1     
        size++;
        // 修改次數 +1,用於 fail-fast 處理
        modCount++;
    }

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


刪除元素

雙連結串列中刪除元素只需要改變前趨和後繼的指向。

  • 刪除頭節點
   //刪除頭節點
    public E removeFirst() {
        final Node<E> f = first;
        //如果連結串列為空,丟擲異常
        if (f == null)
            throw new NoSuchElementException();
         // 刪除首節點   
        return unlinkFirst(f);
    }
    
    // 刪除頭節點
     private E unlinkFirst(Node<E> f) {
        // 頭結點
        final E element = f.item;
        //頭結點後繼節點
        final Node<E> next = f.next;
        //頭結點資料域後繼置空,幫助GC
        f.item = null;
        f.next = null; 
        //頭結點置為後繼節點
        first = next;
       // 如果只有一個元素,刪除了,把last也置為空
       // 否則把next的前趨置為空
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        //返回刪除的節點
        return element;
    }

  • 刪除尾結點
   //刪除尾結點
    public E removeLast() {
        //尾結點
        final Node<E> l = last;
        //連結串列為空,丟擲異常
        if (l == null)
            throw new NoSuchElementException();
            
        return unlinkLast(l);
    }

   //刪除尾結點
   private E unlinkLast(Node<E> l) {
        // 尾結點元素
        final E element = l.item;
        //尾結點前趨節點
        final Node<E> prev = l.prev;
        //尾結點資料、前趨置為null,幫助GC
        l.item = null;
        l.prev = null; 
        //尾結點置為前趨節點
        last = prev;
        // 如果只有一個元素,刪除了把first置為空
       // 否則把前置節點的next置為空
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        //返回刪除的節點
        return element;
    }

注意:
不管是上一節的頭插入和未插入,還是這一節的刪除頭節點和刪除尾結點,都沒有在List中定義。前面提到,LinkedList實現了Deque介面,所以這是作為雙向佇列的LinkedList插入和刪除元素的方式。還有獲取頭結點和尾結點的方法getFirst()和getLast(),同樣都是雙向佇列的實現。

  • 刪除指定位置的節點

在這裡插入圖片描述

   //刪除指定位置的節點
    public E remove(int index) {
       //檢查越界情況
        checkElementIndex(index);
        //根據索引找到節點,刪除
        return unlink(node(index));
    }

     //刪除指定節點
       E unlink(Node<E> x) {
        // 刪除節點的值
        final E element = x.item;
        //被刪除節點的後繼節點
        final Node<E> next = x.next;
        //被刪除節點的前趨節點
        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,fail-fast
        modCount++;
        //返回刪除的元素
        return element;
    }

刪除頭尾節點,時間複雜度為O(1)。

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


前面還提到,LinkedList可以作為棧使用,棧的特點是先進後出,LinkedList同樣有作為棧的方法實現。

push

入棧:插入頭節點

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

pop

出棧:刪除頭結點

    public E pop() {
        return removeFirst();
    }

與ArrayList

LinkedList作為Java中連結串列的實現,ArrayList作為順序表的實現(ArrayList原始碼閱讀筆記),LinkedList常常被拿來和ArrayList來進行比較。

LinkedList、ArrayList基本操作時間效率對比如下(粗略對比):

操作 ArrayList LinkedList
get(int index) O(1) O(n),平均 n / 4步
add(E element) 最壞情況(擴容)O(n) ,平均O(1) O(1)
add(int index, E element) O(n) ,平均n / 2步 O(n),平均 n / 4步
remove(int index) O(n) 平均n /2步 O(n),平均 n / 4步

簡而言之,需要頻繁讀取集合中的元素時,使用ArrayList效率較高,而在插入和刪除操作較多時,使用LinkedList效率較高。



紙上得來終覺淺,絕知此事要躬行。

參考:

【1】:【死磕 Java 集合】— LinkedList原始碼分析
【2】:【JDK1.8】LinkedList原始碼分析
【3】:Java集合乾貨系列-(二)LinkedList原始碼解析
【4】:看動畫輕鬆理解「連結串列」實現「LRU快取淘汰演算法」
【5】:和我一起讀Java8 LinkedList原始碼
【6】:LinkedList Java Data Structure

相關文章