Java LinkedList源碼分析

簡介

LinkedList是一個經常使用的集合類,用於順序存儲元素。LinkedList常常和ArrayList一塊兒被說起。大部分人應該都知道ArrayList內部採用數組保存元素,適合用於隨機訪問比較多的場景,而隨機插入、刪除等操做由於要移動元素而比較慢。LinkedList內部採用鏈表的形式存儲元素,隨機訪問比較慢,可是插入、刪除元素比較快,通常認爲時間複雜都是O(1)(須要查找元素時就不是了,下面會說明)。本文分析LinkedList的具體實現。java

繼承關係

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

LinkedList繼承了一個抽象類AbstractSequentialList,這個類就是用調用ListIterator實現了元素的增刪查改,好比add方法:node

public void add(int index, E element) {
    try {
        listIterator(index).add(element);
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

不過這些方法在LinkedList中被複寫了。數組

LinkedList實現了ListDequeCloneable以及Serializable接口。其中Deque是雙端隊列接口,因此LinkedList能夠看成是棧、隊列或者雙端隊隊列。數據結構

內部變量

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

總共就三個內部變量,size是元素個數,first是指向第一個元素的指針,last則指向最後一個。元素在內部被封裝成Node對象,這是一個內部類,看一下它的代碼:this

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內部有幾個關鍵的私有方法,它們實現了鏈表的插入、刪除等操做。好比在表頭插入:code

private void linkFirst(E e) {
    final Node<E> f = first;    //先保存當前頭節點
    //建立一個新節點,節點值爲e,前驅節點爲空,後繼節點爲當前頭節點
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;    //讓first指向新節點
    if (f == null)    //若是鏈表原來爲空,把last指向這個惟一的節點
        last = newNode;
    else    ·        //不然原來的頭節點的前驅指向新的頭節點
        f.prev = newNode;
    size++;
    modCount++;
}

其實就是雙向鏈表的插入操做,調整指針的指向,時間複雜度爲O(1),學過數據結構的應該很容易看懂。其它還有幾個相似的方法:對象

//尾部插入
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)    //若是鏈表原來爲空,讓first指向這個惟一的節點
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
//中間插入
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
//刪除頭節點
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next; //先保存下一個節點
    f.item = null;    
    f.next = null; // help GC
    first = next;    //讓first指向下一個節點
    if (next == null)    //若是下一個節點爲空,說明鏈表原來只有一個節點,如今成空鏈表了,要把last指向null
        last = null;
    else        //不然下一個節點的前驅節點要置爲null
        next.prev = null;
    size--;
    modCount++;
    return element;
}
//刪除尾節點
 private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;  //保存前一個節點
    l.item = null;
    l.prev = null; // help GC
    last = prev;    //last指向前一個節點
    if (prev == null)    //與頭節點刪除同樣,判斷是否爲空
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}
//從鏈表中間刪除節點
 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要指向下一個節點
        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;
}

公開方法

公開的方法幾乎都是調用上面幾個方法實現的,例如add方法:繼承

public boolean add(E e) {
    linkLast(e);
    return true;
}
public boolean add(E e) {
    linkLast(e);
    return true;
}
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

這些方法的實現都很簡單。注意最後一個方法add(int index, E element),這個方法是在指定的位置插入元素。首先判斷位置是否越界,而後判斷是否是最後一個位置。若是是就直接插入鏈表末尾,不然調用linkBefore(element, node(index)方法。這裏在傳參數的時候又調用了node(index),這個方法的目的是找到這個位置的節點對象,代碼以下:接口

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

這裏有個小技巧是先判斷位置是在鏈表的前半段仍是後半段,而後決定從鏈表的頭仍是尾去尋找節點。要注意的是遍歷鏈表尋找節點的時間複雜度是O(n),即便作了位置的判斷,最壞狀況下也要遍歷鏈表中一半的元素。因此此時插入操做的時間複雜度就不是O(1),而是O(n/2)+O(1)。用於查找指定位置元素的get(int index)方法即是調用node實現的:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

再看一下remove方法:

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

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

第一個remove(int index)方法一樣要調用node(index)尋找節點。而第二個方法remove(Object o)是刪除指定元素,這個方法要依次遍歷節點進行元素的比較,最壞狀況下要比較到最後一個元素,比調用node方法更慢,時間複雜度爲O(n)。另外從這個方法能夠看出LinkedList的元素能夠是null

總結

  • LinkedList基於雙向鏈表實現,元素能夠爲null
  • LinkedList插入、刪除元素比較快,若是隻要調整指針的指向那麼時間複雜度是O(1),可是若是針對特定位置須要遍歷時,時間複雜度是O(n)
相關文章
相關標籤/搜索