以CURD的角度手撕LinkedList源碼

上文書說到ArrayList的簡要源碼分析,就不得不提到和它相近的類似的LinkedList,同樣都是列表,讓我們一起來看看有何相同和不同之處。


talk is cheap,show me the code  ---undefined


老規矩,先來一段代碼示例

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class LinkedListTest {
    
        public static void main(String[] args) {

            //無參構造函數new一個linklist
            List linklist=new LinkedList();

            //linklist的add操作
            linklist.add("hello world");
            linklist.add("world hello");
            System.out.println("當前linklist的容量大小"+linklist.size());

            //linklist的getter setter操作
            System.out.println("getter"+linklist.get(3));
            System.out.println("setter"+linklist.set(3,"huhu"));
            System.out.println("當前linklist的容量大小"+linklist.size());

            //linklist的addall操作
            linklist.addAll(linklist);
            System.out.println("當前linklist的容量大小"+linklist.size());

            //linklist的index操作
            System.out.println("這是索引"+linklist.indexOf("world hello"));
            System.out.println("這是最後索引"+linklist.lastIndexOf("world"));
            System.out.println("當前linklist的容量大小"+linklist.size());
            System.out.println("是否包含有"+linklist.contains("huhu"));
            System.out.println("是否包含所有"+linklist.containsAll(linklist));
            System.out.println("當前linklist的容量大小"+linklist.size());

            //linklist的remove操作
            System.out.println("刪除元素"+linklist.remove("world hello"));
            System.out.println("刪除元素"+linklist.remove(4));
            System.out.println("當前linklist的容量大小"+linklist.size());

            //linklist的retain操作
            List linklist2=new LinkedList();
            linklist2.add("hu hu");
            linklist2.add("world hello");
            linklist2.add(1);
            linklist2.add(100);
            linklist2.add(150);


            System.out.println("保留指定集合中的內容"+linklist.retainAll(linklist2));
            System.out.println("刪除指定集合中的內容"+linklist.removeAll(linklist2));
            System.out.println("當前linklist的容量大小"+linklist.size());

            LinkedListTest linklistTest=new LinkedListTest();
            linklistTest.linklistop(linklist2);

        }

        public boolean linklistop(List linklist) {
            Iterator iterator = linklist.iterator();
            while (iterator.hasNext()) {
                System.out.println("這是第幾個元素 \t " + iterator.next());
            }
            return true;
        }
}

1. 無參構造函數


    public LinkedList() {
    }

可以看到,無參構造函數是空的,即什麼也沒有做。

注1:和ArrayList無參構造函數不同,由於採用的底層數據結構不同,ArrayList的底層數據結構爲數組,因此無參構造在初始化時,就需要提前申請一個數組的空間;而LinkedList採用的是雙向鏈表+雙向隊列,鏈表的一個特性就是即用即申請,因此在LinkedList中的無參構造就什麼也沒有做啦。

Q1:LinkedList有哪些構造函數?分別對應哪種情況?

Q2:爲什麼LinkedList的底層數據結構是雙向鏈表?爲什麼不是單向鏈表?這兩種鏈表有何處異同以及好壞處?

Q3:爲什麼LinkedList還實現了Deque(隊列接口)?實現了雙向鏈表+雙向隊列的數據存儲結構?

2. add函數


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

add實現了LinkedList的末尾追加,也就是添加元素的功能,其步驟如下

1. add函數調用了linkLast函數

2. linkLast函數以插入節點數據爲形參

2.1 將LinkedList中的last(末尾標誌指針,永遠指向最後一個元素節點)賦值給l節點

2.2 創建新的節點newNode,節點的前驅指向l節點,節點的後驅設置爲空

2.3 將last指向剛剛創建的新節點newNode

2.4 判斷l節點是否是空節點,如果是將first(首部指針,永遠指向第一個元素節點)指向新創建的newNode節點,如果不是,將l的後驅指向newNode

2.5 LinkedList大小加1

2.6 標誌結構性變化的變量加1 

3. 返回真值

注2:很顯然,add函數調用了linkLast函數,而linkLast函數採用的是尾插法,也就是說,作爲末尾追加元素時,LinkedList採用了尾插法作爲元素新建和插入。

Q4:爲什麼LinkedList的add是末尾追加元素呢?追加元素爲什麼會採用尾插法,而不是頭插法呢?

Q5:前面提到了LinkedList是雙向鏈表,也就說頭插法和尾插法都可以?那麼什麼情況適合頭插法?什麼情況適合尾插法呢?

3. getter setter函數

getter函數

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

    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

   private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

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

1. get函數調用了checkElementIndex函數

2. checkElementIndex函數調用了isElementIndex函數

3. isElementIndex則是簡單的對index索引值的判斷,判斷該值是否合法,如果不合法就拋出IndexOutOfBoundsException異常

4. 合法就繼續調用了node函數

5. node函數實現比較有技巧性(重點!請注意一下)

注3:node函數採用了二分法來循環遍歷來查找所需要的index上的元素值

5.1 在if(index<(size>>1))判斷語句中,可以看見了判斷是從size右移了一位除以2,即判斷了index是否小於size的一半,如果是從前往後遍歷,根據後驅慢慢查找,直到處於索引的位置

5.2 如果index大於size的一半,則是從後往前遍歷,根據前驅慢慢查找,直到處於索引的位置

Q6:關於node節點的查找函數爲什麼是二分法進行查找?在二分法中爲什麼又採用了位運算?

Q7:node節點的查找遍歷的時間複雜度如何?

setter函數

      public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

1. 先校驗索引值是否合法

2. 合法就調用了node函數

3. node函數其實就是在上文getter方法中也出現過的node函數,這個函數實際上就是利用了二分法來查找對應index位置上的node節點

4. 將該位置的節點作爲舊值返回

5. 設置傳入的新值

注4:值得注意的是,LinkedList和ArrayList不同,其索引值不能同數組下標一樣,直接返回(實際上ArrayList是隨機訪問,LinkedList則是順序訪問),因此需要設立循環,根據前驅或者後驅,循環遍歷直到該索引值的節點處,因此,時間複雜度就是O(n),比起ArrayList的O(1)要大一些。

4. addAll函數

     public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }


   public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

    
     private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }


       private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

針對addAll函數所實現的功能大概都有這麼幾個情況

1. 原列表爲空,沒有任何節點,傳入的集合節點,直接添加進去

2. 原列表有節點,在節點末尾追加傳入的集合節點,這是addAll傳入參數collection的情況

3. 原列表有節點,在指定的index位置上追加傳入的集合節點,這是allAll傳入index,collection的情況

可以看見addAll(collection)也是調用了addAll(index,collection),換句話說,addAll主要依靠addAll(index,collection)實現

那麼我們來看下這個函數的實現

1. 首先是調用了checkPositionIndex函數針對index所處位置合法性的校驗,如果不合法就要拋出異常

2. 將集合中元素轉爲數組

3. 對數組大小進行判斷,如果數組是空的,就返回false,否則就下一步

4. 設立兩個節點,一個節點pred指向要添加節點的前一個節點,一個節點succ指向要添加節點的位置

5. 如果要添加index的位置等於大小,就說明了是在末尾處添加(情況3),將要succ指向null,pred指向原列表的最後一個節點;如果不是,則將succ指向要添加的節點的位置(實際上是在添加的節點的前一個節點處),pred指向該節點的前驅節點(情況2)

6. 循環遍歷數組,遍歷一個,就創建一個節點,使用尾插法在要添加的節點處進行鏈接,此處需要對pred是否爲空進行判斷,如果爲空(情況1),就指向新創建的節點,如果不是,則將pred後驅指向新節點

7. 如果succ是null,則將last指針指向添加鏈接完成後的pred節點

8. 如果不是,則將上面的pred的後驅指向succ,然後succ的前驅指向pred

9. 改變大小,modCount++,返回真值

注5:針對批量添加的情況,比較複雜,建議讀者們手動畫圖模擬一下插入的過程

Q8:爲什麼會將集合元素要先轉爲數組,再遍歷創建新節點進行鏈接呢?和直接在集合中遍歷創建節點鏈接有什麼異同?

Q9:設立pred,succ兩個臨時節點指針是否多餘?

5. indexOf、lastindexOf、contains、containsAll函數

關於indexOf的函數實現如下

    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

可以看出,步驟都比較簡單

1. 設立index爲0

2. 對object進行判空

3. 如果空值,從first節點開始,按照後驅遍歷查找爲空值的節點,如果找到了,就返回該節點的index

4. 如果非空,也同樣的是從firs節點開始,按照後驅遍歷查找爲空值的節點,如果找到了,就返回該節點的index

5. 都沒找到的,就返回-1

注6:針對空值和非空值的查找,其查找方式都是一樣的,只是在對比值的時候,由於空值的特殊性只能採用==來判斷,而非空值可以直接equals來判斷

關於lastIndexOf函數實現

        public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

同樣可以看出,跟indexOf的實現方式是一樣的,只不過遍歷是從後驅開始慢慢查找的

再看看contains和containsAll函數實現

  public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

//AbstractCollection中的實現
    public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }

很顯然,contains調用了indexOf的方法,即其實現也是用indexOf實現的,將indexOf返回跟-1進行判斷就可以得出元素是否在列表中的結果了,然後返回結果

注7:需要注意的是,containsAll的實現在LinkedList中並沒有,取而代之則是AbstractCollection中的containsAll實現,而AbstractCollection中的containsAll實現也是循環調用了contains方法

Q10:不知道爲什麼在LinkedList中沒有實現containsAll的方法?

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

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


   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重載了兩個函數,一個是參數是index,一個參數是object,一個是根據index移除節點,一個是根據object移除節點

根據index索引來移除節點的函數實現中要先對index進行合法性檢查,再調用了unlink函數

根據object移除節點要對object是否爲空值進行判斷,採取不同的判斷方式,遍歷查找到要移除的節點位置,再調用unlink函數

重點來了,我們需要看看unlink是怎麼移除節點的

1. 設立要返回的元素爲該節點的元素值,設立prev爲該節點的前驅節點,next爲該節點的後驅節點

2. 對前驅節點prev進行判空,說明是否是首節點,如果不是首節點,則將前驅的後驅節點指向next的節點,然後把該節點的前驅置空

3. 對後驅節點next進行判空,說明是否是尾節點,如果不是尾節點,則將後驅的前驅節點指向prev節點,然後把該節點的後驅置空

4. 節點上的元素置空,大小減一,mouCount加一,返回刪除節點的元素值

7. retainAll、removeAll函數

 public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<E> it = iterator();
        while (it.hasNext()) {
            if (!c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

可以看出,removeAll和retainAll其實現邏輯基本都是一樣的

1. 調用requireNonNull對集合是否爲空進行判斷

2. 把修改標誌modified設置爲false

3. 取得迭代器iterator

4. 迭代器循環獲取下一個節點元素

5. removeAll是針對集合中存在元素進行判斷,然後調用remove進行刪除;retainAll則是針對集合中非存在的元素進行判斷,不存在就調用remove刪除

6. 返回modified

Q11:爲什麼retainAll、removeAll使用了迭代器進行刪除或者保留?和ArrayList有什麼異同?

8. 其他常用的函數

 

9. 迭代器

 

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