Java源碼分析 - LinkedList

什麼是LinkedList

和ArrayList不同,LinkedList是基於鏈表實現的線性數據結構。節點之間訪問不是通過下標進行,而是通過指針。同時,LinkedList實現了List接口和Deque接口。在Deque接口中提供了許多有用的方法,我們下面會選一些詳細說。

這是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;
        }
    }
  • item: 數據
  • next: 下一個節點
  • prev: 上一個節點

可以看到,LinkedList是雙向鏈表,因爲每個節點都有指向前驅後繼節點的指針。

他的數據結構類似這樣:

| prev | node1 | next | -> | prev | node2 | next | -> | prev | node3 | next | -> | prev | node4 | next |

LinkedList是基於鏈表實現的,因此我們的重點就是掌握節點之間是如何添加/刪除的。掌握了節點是如何修改指針之後,無論是需要第一個元素,還是最後一個元素,還是要獲取其中任意一個元素,我們都能夠自己實現了。

LinkedList的add()

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    void linkLast(E e) {
        final Node<E> l = last;  // 1
        final Node<E> newNode = new Node<>(l, e, null);  // 2
        last = newNode;  // 3
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
  • add() 方法調用 linkLast() 方法
  • linkLast()方法分以下幾步
    • 讓一個l 節點指向 last
    • 創建一個新的節點newNode,並且把前驅結點設置爲 last
    • last 指向新創建的節點newNode
    • 設置 l.next 爲新創建的節點

這就實現了把一個新的節點插入到鏈表末尾的功能。我們可以畫圖來表示:

  • | last.prev | last | null | - 這是last節點的數據結構
  • | null | data | null | - 這是新節點的數據結構
  • | last.prev| last | null | -> | last | data | null | - 把 e 的前驅節點設置爲 last
  • | last.prev| last | data | -> | last | data | null | - 把 l 的後繼節點設置爲 newNode

通過這4步,就成功的把兩個節點連接在一起了,而且新創建的節點在last節點之後,成爲新的尾節點

用文字來總結就是:

  • 持有一個對last節點的引用
  • 創建新的節點
  • 新節點的前驅設置爲last
  • last節點的後繼設置爲新節點

注意:註釋3這一步,last = newNode,其實在後面做也可以,只要保證last指向的是最後一個元素地址就可以了。

說完了添加操作,我們再來看看鏈表的移除。

LinkedList的remove()

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

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

從代碼量來看,remove() 方法比add() 要複雜,實際上也確實是的。

想象一下,一條隊伍裏相鄰的兩個人互相手牽手,如果要在隊尾加上一個人,那麼只是最後一個人和新來的人牽手就可以了。

但是如果是中間某個人要離開隊伍了,那麼和他相鄰的兩個人(前驅後繼)都會受到影響。在鏈表裏面也是一樣的,如果我們要進行中間元素的刪除操作,那麼需要修改的節點是3個。當然插入中間元素也是一樣的意思。

下面進入源碼分析階段:

  • remove(): 遍歷找到要刪除的元素
  • 如果是null,那麼找data == null的節點 (null 不能用equals())
  • 如果不爲null,那麼就找 data.equals(x.item) 的節點,反正是找到第一個值相等的元素

找到之後就進入unlink() 方法:

  • 首先持有x節點的前驅後繼節點的指針
  • 判斷是不是頭節點(沒有prev),是的話直接讓 first 指向 x.next 就可以了
  • 不是頭節點的話,讓前驅節點的後繼節點設爲x的後繼節點,也就是把x從前驅節點中去掉了
  • 再把x的前驅節點設爲null,也就是鬆開自己和前面一個人的手(去掉自己的前驅節點)
  • 同樣的方法處理後繼節點

如果通過畫圖來展示的話就是:

第一種情況:頭節點

| null | x | x.next | -> | x | a | a.next |

直接讓first = x.next,也就是讓 first 指向 a,完成 x 的刪除操作

第二種情況:中間節點

| x | first | first.next | -> | first | x | x.next | -> | x | b | b.next |

首先讓 first.next -> b ( x.next ),此時變成了

| x | first | b | -> | first | x | x.next | -> | x | b | b.next |

然後修改x的前驅爲null,此時變成了

| x | first | b | -> | null | x | x.next | -> | x | b | b.next |

這就解除了和前驅結點之間的聯繫,然後相同的思路處理後繼節點即可。

第三種情況:尾節點

| x.prev | x | null |

這種情況我們讓last指向x.prev就可以了,尾節點就從x變成了它的前一個節點

LinkedList的一些細節

通過我們上面總結的添加和移除元素的思路,再配合Deque接口裏面的許多方法,可以實現很多有意思的操作。

有意思的查找Node算法

    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),但是LinkedList裏面還是用了點小技巧進行優化,通過類似二分法的思想,把整個鏈表平分爲兩部分,判斷傳入的下標是在前半部分還是後半部分,如果是前半部分,那麼從頭開始找。如果是後半部分,那麼就從尾開始找。這樣做的好處是:

  • 把O(n) 的時間複雜度縮短爲O(n/2)
  • 利用了雙向鏈表的優勢,可以從前往後,也可以從後往前
  • 同樣用了位運算來判斷index的位置,優化計算效率

LinkedList如何通過下標插入?

LinkedList相比ArrayList的一個優勢就是插入刪除更加高效,因爲只要修改相鄰節點就可以了,但是ArrayList需要進行數組的複製移動。需要的內存和時間都更多。

而LinkedList不僅實現了在頭/尾插入刪除元素,還能指定下標進行添加/刪除操作。具體是怎麼做到的呢?

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

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

核心方法其實就是上面說的node ,傳入index然後返回找到的node,然後再通過我們在上面說的,修改前驅後繼節點來實現插入操作。

Deque的peekFrist() 和 getFirst()有什麼區別?

Deque裏面這兩個實現方法:

    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }

    public E element() {
        return getFirst();
    }
    
    public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }
  • peek(): 返回 null 或者 元素數據
  • element(): 拋出NoSuchElementException() 或者 元素數據

也就是說,如果我們可以接受null的話,可以使用peekFirst(),類似的方法還有peekLast() 和 getLast()。

LinkedList的clear()

clear():

    public void clear() {
        // Clearing all of the links between nodes is "unnecessary", but:
        // - helps a generational GC if the discarded nodes inhabit
        //   more than one generation
        // - is sure to free memory even if there is a reachable Iterator
        for (Node<E> x = first; x != null; ) {
            Node<E> next = x.next;
            x.item = null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;
        size = 0;
        modCount++;
    }

爲了能夠讓JVM進行GC,把所有的對象設置爲null,包括first & last。

總結

LinkedList的操作都圍繞着如果管理相鄰節點的前驅後繼指針。只要把這個弄清楚,那麼無論是添加還是刪除都是比較容易寫出來的。如果大家覺得不好理解,可以在紙上把數據結構畫出來,在每一步操作之後修改對應的節點指針,會好理解一點。

LinkedList實現的Deque接口,提供了許多有用的方法,如果我們需要進行頻繁插入刪除操作的話,可以優先考慮LinkedList而不是ArrayList,並且看看裏面有沒有我們需要的實現~

在這裏也推薦一些鏈表相關的算法題,可以通過做題的方式檢驗一下自己是不是真的掌握了:

  • LeetCode.19 刪除鏈表倒數第N位節點
  • LeetCode.21 合併兩個有序鏈表
  • LeetCode.206 反轉鏈表
  • LeetCode.237 刪除鏈表中的節點
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章