深入Collection之LinkedList

​ 這一篇是有關LinkedList的學習,那麼閒話不多扯,直接按照上一篇的博文的模式來分析LinkedList的實現和功能。

成員變量

    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

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

由定義的成員變量和Node類的結構可以看出LinkedList是以雙向鏈表爲實現形式的List結構。

構造方法

  1. 無參構造

    public LinkedList() {
       }

    沒有任何其他的操作,僅僅構造一個空的List。

  2. 參數爲Collection的構造

    public LinkedList(Collection<? extends E> c) {
           this();
           addAll(c);
       }

    很顯然,先構造一個空List,然後執行addAll(c)方法,將Collection c添加入List。關於addAll()方法的介紹請看下文。

成員方法

因爲對LinkedList的操作均是傳遞到對雙向鏈表的操作,所以先來看對鏈表的基礎操作:

操作雙向鏈表方法

  1. 添加第一個節點

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

    ​ 先將f指向原來初始節點對象對應的內存空間,再新建新的初始節點,定義前置爲null,內容爲e,後置爲f(原來的初始節點),同時令first指向新建的初始節點的內存空間。

    當前狀態

    ​ 因此如果之前沒有初始節點,只需要將last指向newNode即可。如果有初始節點,將初始節點f的prev指向newNode即可完成。再將List大小+1,更改次數+1.

  2. 作爲最後一個元素節點添加

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

    ​ 插入過程與第一個很類似。先將 l 指向原來的終節點,再新建一個新的終節點,並將last指向新建的終節點。如果原來沒有終節點,則將first也指向新建節點,此時鏈表中只有一個節點。若原來有終節點,將原來終節點的後置節點指向新建的終節點,完成連接。最後將表大小+1,更改次數+1.

  3. 解綁鏈表的第一個節點

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

    ​ 從鏈表角度來看,解綁初始節點,無非是將first節點指向第二個節點,再將第二個節點next的prev設爲null。而java實現中,需要將初始節點f的item和next設爲null,以便對象f沒有對象引用方便GC。當只有鏈表只有一個節點時,需要將last設爲null。

  4. 解綁鏈表的最後一個節點

    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;
           if (prev == null)
               first = null;
           else
               prev.next = null;
           size--;
           modCount++;
           return element;
       }

    ​ 同樣從雙向鏈表的角度來看,唯一值得關注的操作就是讓l的item引用和prev引用爲null,方便GC。其他操作是將last指向prev節點。如果prev爲null,則將first設爲null,否則另prev的後置節點爲null。

  5. 在一個節點前插入一個元素

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

    ​ 定義succ之前的前置節點爲pred,新建插入節點。將succ的前置節點設爲新建節點。如果succ沒有前置節點,則將first指向新建節點;若有,則將pred的後置節點指向新建節點newNode。

  6. 移除鏈表中一個非空節點

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

    ​ 移除節點x,定義x的前置節點爲prev,後置節點爲next。正常移除時需要將prev的後置引用指向next,next的前置引用指向prev。如果prev或next爲空,則將first指向next或last指向prev。同時清除x殘留的引用。大小-1,更改次數+1.

    操作LinkedList方法

    常用方法
    public E getFirst() {
           final Node<E> f = first;
           if (f == null)
               throw new NoSuchElementException();
           return f.item;
       }
    
    public E getLast() {
           final Node<E> l = last;
           if (l == null)
               throw new NoSuchElementException();
           return l.item;
       }
    
    public E removeFirst() {
           final Node<E> f = first;
           if (f == null)
               throw new NoSuchElementException();
           return unlinkFirst(f);
       }
    
    public E removeLast() {
           final Node<E> l = last;
           if (l == null)
               throw new NoSuchElementException();
           return unlinkLast(l);
       }
    
    public void addFirst(E e) {
           linkFirst(e);
       }
    
    public void addLast(E e) {
           linkLast(e);
       }
    
    public int size() {
           return size;
       }
    
    public boolean add(E e) {
           linkLast(e);
           return true;
       }
    
    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 get(int index) {
           checkElementIndex(index);
           return node(index).item;
       }
    
       public E set(int index, E element) {
           checkElementIndex(index);
           Node<E> x = node(index);
           E oldVal = x.item;
           x.item = element;
           return oldVal;
       }

    以上方法常用,且操作簡單,都是對鏈表的簡單操作。因此在這不加多餘的解釋,相信大家也能理解。

    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;
       }
    
    //將LinkedList轉化成數組,實際實現是遍歷鏈表,將鏈表中的值存入數組內。
    public Object[] toArray() {
           Object[] result = new Object[size];
           int i = 0;
           for (Node<E> x = first; x != null; x = x.next)
               result[i++] = x.item;
           return result;
       }
    
    //找到處於index位置的節點,if-else判斷是爲了找出耗時較少的遍歷途徑,返回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;
           }
       }

    ​ 作爲addAll方法,實現的本質還是在鏈表的index位置插入內容爲c的鏈表。定義succ爲當前插入位置的節點,pred爲插入位置的前置節點。插入過程就是遍歷內容數組,新建一個節點,將pred的後置節點指向新建節點newNode。完成後,後移pred指向新建節點。數組循環完成後,建立最後一個新建節點和插入位置節點succ的關聯。至此,操作完成。

    ​ 而其他的成員方法,都是對鏈表的操作,均可以調用操作鏈表的方法,所以不再一一介紹,如果有興趣可以自己查看源碼。由於LinkedList採用雙向鏈表實現,所以不存在數組擴容的問題。通過以上方法可以看出,LinkedList的增刪操作開銷很小,只需要遍歷到目標位置,進行鏈表節點的增刪,而對於在表起點和終點的增刪,更只是常數時間的操作。但相較於ArrayList的get方法,LinkedList仍需要遍歷鏈表才能找到索引對應值,效率不好。

迭代器的實現

​ 想要在不暴露LinkedList的內部實現的前提下,遍歷訪問List使用迭代器是一種很好的選擇。接下來就是介紹LinkedList中的迭代器實現:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //step 4
    public ListIterator<E> listIterator(int index) {
        checkPositionIndex(index);
        return new ListItr(index);
    } 
}
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
  //step 1  
  public Iterator<E> iterator() {
        return listIterator();
  }
  //step 3
  public abstract ListIterator<E> listIterator(int index); 
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

  public Iterator<E> iterator() {
        return new Itr();
    }

    //step 2 
    public ListIterator<E> listIterator() {
        return listIterator(0);
    }

    public ListIterator<E> listIterator(final int index) {
        rangeCheckForAdd(index);

        return new ListItr(index);
    }
}

按照以上的執行順序可以看到,LinkedList的iterator()的實現最終追溯到內部類ListItr。

private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned = null;//可以理解爲當前訪問的節點
        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;
        }
        //判斷是否有下個元素,將下個元素索引值和List大小比較
        public boolean hasNext() {
            return nextIndex < size;
        }
        //返回索引指向的元素,並將next指向當前的後置節點,索引值+1。
        public E next() {
            checkForComodification();//迭代器和初始List一致性校驗
            if (!hasNext())
                throw new NoSuchElementException();

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

        public boolean hasPrevious() {
            return nextIndex > 0;
        }
        //返回當前next節點指向的前置節點,並將next指向當前的前置節點,索引值-1.
        //主要應用於定義的另一種倒敘遍歷方式
        public E previous() {
            checkForComodification();
            if (!hasPrevious())
                throw new NoSuchElementException();

            lastReturned = next = (next == null) ? last : next.prev;
            nextIndex--;
            return lastReturned.item;
        }

        public int nextIndex() {
            return nextIndex;
        }

        public int previousIndex() {
            return nextIndex - 1;
        }
        //把當前位置節點移除
        public void remove() {
            checkForComodification();
            if (lastReturned == null)
                throw new IllegalStateException();

            Node<E> lastNext = lastReturned.next;
            unlink(lastReturned);
            if (next == lastReturned)
                next = lastNext;
            else
                nextIndex--;
            lastReturned = null;
            expectedModCount++;
        }

        public void set(E e) {
            if (lastReturned == null)
                throw new IllegalStateException();
            checkForComodification();
            lastReturned.item = e;
        }

        public void add(E e) {
            checkForComodification();
            lastReturned = null;
            if (next == null)
                linkLast(e);
            else
                linkBefore(e, next);
            nextIndex++;
            expectedModCount++;
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

既然提到迭代遍歷,也可以順帶討論List遍歷時刪除元素的問題:

public static List<String> initialList(){
        List<String> list = new LinkedList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        return list;
    }

    public static void removeByFor(List<String> list){
        for (int i = 0; i < list.size(); i++) {
            if("b".equals(list.get(i))){
                System.out.println(i);
                list.remove(i);
            }
            if("d".equals(list.get(i))){
                System.out.println(i);
                list.remove(i);
            }
        }
        System.out.println(list);
    }

    public static void removeByForeach(List<String> list){
        for (String string : list) {
            if ("b".equals(string)) {
                list.remove(string);
            }
        }
        System.out.println(list);
    }

    public static void removeByIterator(List<String>list){
        Iterator<String> e = list.iterator();
        while (e.hasNext()) {
            String item = e.next();
            if ("b".equals(item)) {
                e.remove();
            }
        }
        System.out.println(list);
    }

    public static void main(String []args){
        removeByFor(initialList());         //1
        removeByForeach(initialList());     //2
        removeByIterator(initialList());    //3
    }
  • 方法1:能正常刪除,但是刪除d時,索引index爲2,和原List中的索引值不同,可能會對其他操作造成影響。且刪除操作經歷了兩次遍歷(外部一次,remove操作一次),時間複雜度增加。
  • 方法2:會拋出ConcurrentModificationException異常。因爲foreach內部也是使用iterator進行遍歷,方法2操作只更改了modCount,沒有更改Iterator中的expectedModCount。
  • 方法3:最合適的遍歷刪除,只經歷一次遍歷,時間複雜度低。

結語

LinkedList的實現是基於雙向鏈表,因此它的一切性質都是與雙向鏈表相關的:

  • get(int index) O(n) 由於雙向鏈表需要依次遍歷到目標index,所以get效率不如ArrayList。
  • add(E e) O(1) 添加到鏈表尾部,只需要建立關聯即可。
  • add(int index,E e) O(n) 由於需要遍歷到目標index,再添加節點建立關聯。但是效率比ArrayList的移動數組好了不少。
  • remove(int index) O(n) 和add方法類似,耗時主要在遍歷到目標index。整體效率比ArrayList的remove()方法好。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章