LinkedList集合深度解析

上文講解了ArrayList的底層實現原理,感興趣的小夥伴可以去看下,本文重點討論LinkedList集合。

首先說下ArrayList和LinkedList的區別:(相同點都是有序的~)

① ArrayList底層數據結構是動態數組,LinkedList底層數據結構是雙向鏈表。

②  查詢或者修改的時候,ArrayList比LinkedList的效率更高,因爲LinkedList是線性的基於鏈表的數據存儲方式,所以需要移動指針從前往後依次查找,即使源碼中(下面會詳細說明)用了二分法,但是效率還是不如ArrayList底層基於數組,直接通過索引定位,效率極快。

③ 增加或者刪除的時候,LinkedList比ArrayList的效率更高,因爲ArrayList是數組,所以在其中進行增刪操作時,會對操作點之後所有數據的下標索引造成影響,需要進行數據的移動,而LinkedList只需要修改prev和next的指針引用即可。

綜上所述,Arraylist適用於查詢或者修改比較多的場景,LinkeList適用於查詢/修改較少,增加和刪除較多的場景。

純手寫LinkedList源碼(白話文分析):

package com.example;

public class MyLinkedList<E> implements MyList<E> {
    //E LinkedList 存放的數據類型
    /**
     * 集合的大小
     */
    transient int size = 0;

    /**
     * 第一個節點
     */
    transient Node<E> first;

    /**
     * 最後一個節點
     */
    transient Node<E> last;

    @Override
    public int size() {
        return size;
    }

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

    @Override
    public E get(int index) {
        // 檢查我們index是否越界
        checkElementIndex(index);
        // 通過二分法查找具體Node對象/節點的item
        return node(index).item;
    }

    /** 【刪除原理:改變相互引用的指針】
     * 步驟1:當前要刪除Node節點的上一個的next指向當前要刪除節點的下一節點
     * 步驟2:當前要刪除Node節點的下一個節點的prev指向當前要刪除節點的上一個節點
     * 步驟3:當前要刪除Node節點/對象,所有屬性置爲null,等待gc回收
     * @param index
     * @return
     */
    @Override
    public E remove(int index) {
        // 檢查我們index是否越界
        checkElementIndex(index);
        // node(index)首先獲取到當前刪除節點,然後再刪除unlink(),即調整指針位置
        return unlink(node(index));
    }

    /**
     * 添加我們的節點,作爲最後一個元素
     * @param e
     */
    void linkLast(E e) {
        // 獲取當前的最後一個節點
        final Node<E> l = last;
        // 封裝我們當前自定義元素
        final Node newNode = new Node<E>(l, e, null);
        // 當前新增節點肯定是鏈表中最後一個節點(當前新增節點賦給last)
        last = newNode;
        if (l == null)  //注意,走到這一步last變了,但l沒變
            // 如果我們鏈表中沒有最後一個節點說明當前新增的元素是第一個
            first = newNode;
        else
            // 原來的最後一個節點的下一個節點就是當前新增的節點
            l.next = newNode;
        size++;
    }

    /**
     * 鏈表:其實可以理解爲全表掃描(折半查找)
     * 數組:直接通過索引定位,效率極快
     * @param index index小於折半值,從頭開始查;index大於折半值,從尾開始查
     * @return
     */
    Node<E> node(int index) {
        /* 舉個例子
            現在鏈表中有1-100節點,如果想查第88個節點,正常情況下從1查到88,即索引從0到87
            寫JDK的人比較聰明,運用了折半查找(也成爲二分法),查詢步驟如下:
                size / 2 = 50,如果88大於50,那麼查50~100就行了(索引49-99)
         */

        // size >> 1 → size/2  → if裏面的判斷解析爲 index < size/2

        // 假設鏈表中有1-10節點,查詢下標爲0,又因爲0<10/2,所以在0-4之間找
        if (index < (size >> 1)) {
            // 獲取到第一個節點
            Node<E> x = first;
            for (int i = 0; i < index; i++) {
                // 如果index小於折半值,從頭(0)查詢到index【基於索引】
                x = x.next;
            }
            return x;
        } else {
            // 獲取到最後一個節點
            Node<E> x = last;
            for (int i = size - 1; i > index; i--) {
                // 如果index大於折半值,從尾(size-1)查詢到index【基於索引】
                x = x.prev;
            }
            return x;
        }
        // 1-10 | 3 1-3 | 7 10-7
    }

    /**
     * 刪除節點,重新連接鏈表
     * @param x 當前刪除的Node節點
     * @return
     */
    E unlink(MyLinkedList.Node<E> x) {
        // 獲取到當前刪除的節點的元素值
        E element = x.item;
        // 獲取當前刪除元素的下一個節點
        Node<E> next = x.next;
        // 獲取當前刪除元素的上一個節點
        Node<E> prev = x.prev;

        if (prev == null) { /** 判斷prev */
            // 如果prev爲空,說明當前刪除節點是第一個節點,需要把next置爲第一個節點(first爲全局變量)
            first = next;
        } else {
            // 如果prev不爲空, 上一個Node節點的next指向下一個Node節點
            prev.next = next;
            // 當前刪除節點的prev變爲空,告訴給gc實現回收
            x.prev = null;
        }

        if (next == null) { /** 判斷next */
            // 如果next爲空,說明當前刪除節點是最後一個節點,需要把prev置爲最後一個節點(last爲全局變量)
            last = prev;
        } else {
            // 如果next不爲空,下一個Node節點的prev指向上一個Node節點
            next.prev = prev;
            // 當前刪除節點的next變爲空 告訴給gc實現回收
            x.next = null;
        }
        // 當前刪除的節點的元素值置爲空
        x.item = null;
        size--;
        return element;

    }

    /**
     * 鏈表中的節點
     * @param <E>
     */
    private static class Node<E> {
        // 節點元素值 zhangsan,lisi..
        E item;
        // 當前節點的下一個node(節點/對象)
        MyLinkedList.Node<E> next;
        // 當前節點的上一個node
        MyLinkedList.Node<E> prev;

        // 使用構造函數傳遞參數
        Node(MyLinkedList.Node<E> prev, E element, MyLinkedList.Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }

        Node(E element) {
            this.item = element;
        }

        public void setPrev(Node<E> prev) {
            this.prev = prev;
        }
        public void setNext(Node<E> next) {
            this.next = next;
        }
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index)) {
            throw new IndexOutOfBoundsException("index已經越界啦~~~");
        }
    }
    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }


    public static void main(String[] args) {
        Node node1 = new Node("第一關遊戲");
        Node node2 = new Node("第二關遊戲");
        node1.next = node2;
        node2.prev = node1;
        System.out.println("node:" + node1);
    }

}

在上述main方法最後一行打個斷點,Debug啓動,會發現node1和node2是相互引用的,驗證了LinkedList底層是基於雙向鏈表。

此時,測試一下我們手寫的LinkesList:

package com.example.test;

import com.example.MyLinkedList;

public class Test001 {
    public static void main(String[] args) {
        MyLinkedList<String> linkedList = new MyLinkedList<>();
        linkedList.add("001");
        linkedList.add("002");
        linkedList.add("003");
        linkedList.remove(1);
        System.out.println(linkedList.get(1));
    }
}

會發現,當我們刪除索引爲1的元素,那麼003則會替代002的位置,該測試把我們的增,刪,查都用上了,改的話無非就是先調用node(即查詢指定索引對應的Node節點),然後替換Node的item屬性爲新元素即可。

【總結】

鏈表數據底層原理實現:雙向鏈表頭尾相接

① 在底層中使用靜態內部類Node節點存放節點元素

三個屬性 prev(關聯的上一個節點),item(當前的值) ,next(下一個節點)

② add原理是如何實現? 答案: 一直在鏈表之後新增

③ get原理:採用折半查找 範圍查詢定位node節點

④ remove原理:改變相互引用的指針

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章