LinkedList源碼理解

LinkedList是也是很是常見的集合類,LinkedList是基於鏈表實現的集合。它擁有List集合的特色:java

  • 存取有序
  • 帶索引
  • 容許重複元素

還擁有Deque集合的特色:node

  • 先入先出
  • 雙端操做

它自己的特色是:數組

  • 對元素進行插入或者刪除,只須要更改一些數據,不須要元素進行移動。

依然是經過源碼來看看LinkedList如何實現本身的特性的。數據結構


Doubly-linked list implementation of the {@code List} and {@code Deque} interfaces. Implements all optional list operations,and permits all elements (including {@code null}).性能

對於List接口和Deque接口的雙鏈表實現。實現了全部List接口的操做而且能存儲全部的元素。測試

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

能夠看到LinkedList實現了一個Deque接口,實際上是說,LinkedList除了有List的特性,還有Deque的特性,那麼Deque是什麼呢?優化

public interface Deque<E> extends Queue<E>

        public interface Queue<E> extends Collection<E>

原來是繼承了Collection集合的另外一個接口。this

Queue就是咱們常說的隊列,它的特性是FIFO( First In First Out )先進先出,它的操做只有兩個:spa

  • 把元素存進隊列尾部
  • 從頭部取出元素

 就像排隊辦事同樣的。3d

而它的子接口Deque除了這兩操做之外,還能比Queue隊列有更多的功能

  • 既能夠添加元素到隊尾,也能夠添加元素到隊頭
  • 既能夠從隊尾取元素,也能夠從隊頭取元素

如此看來就像兩邊均可以當隊頭和隊尾同樣,因此Deque又叫雙端隊列 。

理所應當的,LinkedLisk也實現了這些特性,而且有Doubly-linked(雙鏈表的特性)

那麼什麼又是鏈表呢?

其實鏈表是一種線性的存儲結構,意思是將要存儲的數據存在一個存儲單元裏面,這個存儲單元裏面除了存放有待存儲的數據之外,還存儲有其下一個存儲單元的地址。

雙鏈表顧名思義,存儲單元除了存儲其下一個存儲單元的地址,還存儲了上一個存儲單元的地址。每次查找數據的時候,就經過存儲單元裏存儲的地址信息進行查找。


 成員變量:

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

只有三個,size表明LinkedList存儲的元素個數。那這個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內部的數據結構Node,做爲LinkedList的基本存儲單元,也最能體現LinkedList雙鏈表的特性。

像這樣的。

其中prev存儲上一個節點的引用(地址),next存儲下一個單元的引用,item就是具體要存的數據。

First和Last用來標明隊頭跟隊尾。


 添加數據:

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

    
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的基本存儲單元是Node,因此添加進來的數據會被封裝進Node的item屬性裏,並且這個新Node的prev會指向前一個Node,前一個Node的next會指向這個新Node。

相似這樣,可是注意畫線只是一種形象的表達方法,就如上面所說,在Node裏的prev屬性和next屬性是用來存儲引用的,經過這個引用就能找到前一個Node或者後一個Node。

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

private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

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

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

其實LinkedList不少不一樣名的方法,可是實現方式都是相似的,這是由於咱們有可能用LinkedList表達不一樣的數據結構,雖然都是添加元素到隊首/隊尾,可是清晰的描述對代碼的可讀性是有好處的。像若是要用LinkedList表示Stack(棧)數據結構時候用push()/pop()/peek()等方法來描述,用LinkedList表示Queue(隊列)數據結構時候用add()/offer()等方法來描述。(固然,更好的實現方式是多態。)


 刪除數據:

//刪除頭Node
public E removeFirst() {
        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;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }
//刪除尾Node
public E removeLast() {
        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;
        //拿到最後一個元素的prev前元素的引用
        final Node<E> prev = l.prev;
        //將它們賦值爲null
        l.item = null;
        l.prev = null; // help GC
        //如今前元素是list(最後一個Node)
        last = prev;
        //若是前元素已是null說明沒有Node了
        if (prev == null)
            first = null;
        else
            //說明前面還有元素,那麼前元素的next就存放null
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

先看看簡單的刪除, 這裏是指定刪除最前跟最後的元素,因此只要判斷刪除後Node的prev或者next是否還有值,有就說明還有Node,沒有就說明LinkedList已經爲空了。

怎樣纔算刪除了頭/尾Node,只要它的next/prev爲空,說明沒有引用指向它了,那麼咱們就認爲它從LinkedList裏刪除了,由於咱們沒法經過存儲單元的引用找到這個Node,因此GC很快也會來回收掉這個Node。

 這只是刪除頭尾Node,那要是刪除中間的Node呢?這要跟下面的查找和插入一塊兒看。

 


查找元素:

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


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底層可沒有維護這麼一個數組,那怎麼知道第幾個元素是什麼呢?

笨方法,我有size個元素,我不知道你指定的index在哪,那我一個一個找過去不就完事了?畢竟個人存儲單元Node記得它旁邊的單元的引用(地址)。

若是你的index比我size的一半還大,那我就從後面找,由於我是雙端隊列,有Last的引用(地址),因此能夠調換兩頭。

因此,在LinkedList裏面找元素可不容易,最多可能要找size/2次才能找到。

只要找到了想要的位置,那麼插入和刪除指定的這個Node就很簡單了。

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

E unlink(Node<E> x) {
        // assert x != null;
    //拿到所要刪除的Node的item
        final E element = x.item;
    //後一個Node
        final Node<E> next = x.next;
    //前一個Node
        final Node<E> prev = x.prev;

    //若是前一個Node爲null(說明是第一個Node)
        if (prev == null) {
            //那麼後一個Node做爲first
            first = next;
        } else {//不然說明前面有Node
            //那前一個Node的下一個Node引用變爲後一個Node
            prev.next = next;
            //當前的前引用變成null
            x.prev = null;
        }

    //若是後一個Node爲null(說明是最後一個Node)
        if (next == null) {
            //那麼前一個Node做爲last
            last = prev;
        } else {//不然說明後面還有Node
            //那後一個Node的下一個Node引用變爲前一個Node
            next.prev = prev;
            //當前的後引用變爲null
            x.next = null;
        }

    //保存的元素也設爲null
        x.item = null;
    //元素-1
        size--;
    //修改次數+1
        modCount++;
        return element;
    }

    public void add(int index, E element) {
        checkPositionIndex(index);

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

    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        //要插入位置的前一個Node
        final Node<E> pred = succ.prev;
        //新Node,前引用是前一個Node,後引用是當前位置的Node
        final Node<E> newNode = new Node<>(pred, e, succ);
        //後一個Node的前引用變爲這個新Node
        succ.prev = newNode;
        //若是沒有前一個Node
        if (pred == null)
            //說明添加的就是第一個Node了
            first = newNode;
        else//說明前面還有Node
            //將前一個Node的後引用變爲這個新的Node
            pred.next = newNode;
        //元素+1
        size++;
        modCount++;
    }

只是改變了存儲單元Node裏的prev和next,咱們就能夠認爲這個Node被插入/刪除了。

代碼的註釋配合着下圖看,就會方便理解不少,其中注意區分源代碼中的命名,最好拿筆記一下容易區分一些。

若是是插入元素,倒着看就能夠了。


 關於遍歷:

咱們能夠了解到,LinkedList最大的性能消耗就在node(index)這步,這會須要去查找大量的元素。可是隻要找到了這個元素所在的Node,插入跟刪除就很是的方便了。

因此對於get(index)這個方法,咱們須要很是當心的去使用,若是隻想看一看這個位置的元素,能夠用這個方法,可是若是是遍歷LinkedList,千萬不能夠這樣寫:

for (int i = 0; i < linkedList.size(); i++) {
    linkedList.get(i).equals(Obj);
}

這樣對於每次循環,get總會從前或者從後走i次,不考慮get方法中>>1的優化的話,這是一種O(n^2)時間複雜度的作法,效率十分低下。

因此LinkedList提供了內部的Iterator迭代器供咱們使用:

private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;
        private Node<E> next;
        private int nextIndex;
        private int expectedModCount = modCount;

        ListItr(int index) {
            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }

        public boolean hasNext() {
            return nextIndex < size;
        }

        public E next() {
            checkForComodification();
            if (!hasNext())
                throw new NoSuchElementException();

            lastReturned = next;
            next = next.next;
            nextIndex++;
            return lastReturned.item;
        }

其實就是經過不斷調用next()方法取得Node,而後再對Node作操做,這樣時間複雜度就是O(n)了,不會有大量重複無用的遍歷。


 總結:其實LinkedList的特色插入、刪除快,只是針對此次的操做而言的。

LinkedList作插入、刪除的時候,慢在要找到具體的位置,快在只須要改變先後Node的引用地址

ArrayList作插入、刪除的時候,慢在數組元素的批量賦值(前文裏的System.arraycopy),快在搜索

因此,若是待插入、刪除的元素是在數據結構的前半段尤爲是很是靠前的位置的時候,LinkedList的效率將大大快過ArrayList,由於ArrayList將批量copy大量的元素;越日後,對於LinkedList來講,由於它是雙向鏈表,因此在第2個元素後面插入一個數據和在倒數第2個元素後面插入一個元素在效率上基本沒有差異,可是ArrayList因爲要批量copy的元素愈來愈少,操做速度必然追上乃至超過LinkedList。

不論怎麼說,須要根據具體狀況來選擇對應的集合,最好作一下性能測試,這樣纔能有更高的效率。

相關文章
相關標籤/搜索