ArrayList與linkedlist插入效率分析

congye6發表於2017-05-23

網上有很多比較ArrayList和LinkedList的文章,基本上都認為ArrayList是用陣列實現,而LinkedList用連結串列實現,所以ArrayList查詢效率高,LinkedList插入效率高,但是事實真的是這樣嗎?

比較他們的使用效率之前,先看看ArrayList與LinkeLlist的底層資料結構。

  1. /** 
  2.       * The array buffer into which the elements of the ArrayList are stored. 
  3.       * The capacity of the ArrayList is the length of this array buffer. 
  4.       */  
  5.      private transient Object[] elementData;  
  6.    
  7.      /** 
  8.       * The size of the ArrayList (the number of elements it contains). 
  9.       * 
  10.       * @serial 
  11.       */  
  12.      private int size; 

ArrayList內部用陣列來儲存元素,size記錄當前陣列的大小。但是陣列的大小是確定的,那麼他是怎麼實現ArrayList這種大小可變的集合的呢?

  1. public boolean add(E e) {  
  2.     ensureCapacity(size + 1);  // Increments modCount!!  
  3.     elementData[size++] = e;  
  4.     return true;  
  5. }
  1. /** 
  2.       * Increases the capacity of this <tt>ArrayList</tt> instance, if 
  3.       * necessary, to ensure that it can hold at least the number of elements 
  4.       * specified by the minimum capacity argument. 
  5.       * 
  6.       * @param   minCapacity   the desired minimum capacity 
  7.       */  
  8.      public void ensureCapacity(int minCapacity) {  
  9.      modCount++;  
  10.      int oldCapacity = elementData.length;  
  11.      if (minCapacity > oldCapacity) {  
  12.          Object oldData[] = elementData;  
  13.          int newCapacity = (oldCapacity * 3)/2 + 1;  
  14.              if (newCapacity < minCapacity)  
  15.          newCapacity = minCapacity;  
  16.              // minCapacity is usually close to size, so this is a win:  
  17.              elementData = Arrays.copyOf(elementData, newCapacity);  
  18.      }  
  19. 每次ArrayList在新增元素的時候都會呼叫ensureCapacity方法,這個方法會檢查陣列容量是否夠大,如果不夠則新建立一個容量為1.5倍的陣列,
  20. 並把資料複製到這個新陣列上。
  21. 所以不能簡答的說ArrayList底層資料結構是一個陣列,其實它是一個動態表。
   接下來看看LinkedList的實現,它是一個雙向迴圈連結串列。
  1. //連結串列大小,transient代表不可序列化  
  2. transient int size = 0;  
  3.   
  4. /** 
  5.  * 指向頭節點的指標 
  6.  */  
  7. transient Node<E> first;  
  8.   
  9. /** 
  10.  * 指向尾節點的指標 
  11.  */  
  12. transient Node<E> last;  

  1. private static class Node<E> {  
  2.     E item;  
  3.     Node<E> next; // 指向後一個結點  
  4.     Node<E> prev; // 指向前一個結點  
  5.   
  6.     Node(Node<E> prev, E element, Node<E> next) {  
  7.         this.item = element;  
  8.         this.next = next;  
  9.         this.prev = prev;  
  10.     }  
  11. }
  12. 值得注意的是,這裡的連結串列節點是linkedlist的私有內部類,也就是說其他類是不可能拿到節點的引用的。所以在連結串列中間插入的時候,通過修改幾個指標就能插入一個節點這樣的操作是不可能的。
    
    瞭解底層實現之後,我們開始比較ArrayList與linkedlist的插入效率。
    1.尾部插入
    
  1. public boolean add(E e) {  
  2.     ensureCapacity(size + 1);  // Increments modCount!!  
  3.     elementData[size++] = e;  
  4.     return true;  
  5. }
    ArrayList最常用的add方法就是尾部插入,在不需要擴容的情況下,只需要將size遞增即可,時間複雜度O(1)
  6. 需要擴容的情況下,時間複雜度也不會受到影響。理由如下:
  7. 假設它每次擴容兩倍,那麼從開始的16個元素擴容到n,共需要複製16+32+。。。+n/4+n/2+n<2n個元素
  8. 所以平均到每一次新增操作,擴容只需要複製常數個元素 

  1. /** 
  2. *預設的新增動作,可以看到這個方法是把新元素新增 到表尾 
  3. */  
  4. public boolean add(E e) {  
  5.     addBefore(e, header); //加到頭結點之前 ,即表尾  
  6.         return true;  
  7. }
  1. /**  
  2. *將元素e新增到entry結點之前  
  3. */  
  4. private Entry<E> addBefore(E e, Entry<E> entry) {  
  5.     Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);  
  6.     newEntry.previous.next = newEntry; //將新結點與前後結點相連線   
  7.     newEntry.next.previous = newEntry;  
  8.     size++;  
  9.     modCount++;  
  10.     return newEntry;  
  11. }
  12. 可以看出linkedlist的最常用的add方法也是插入尾部,只需要改變幾個指標,時間複雜度O(1)
   綜上,在兩個集合最常用的尾部插入時間複雜度都是O(1),不存在ArrayList插入比linkedlist慢的說法

   2.中間插入
  1. public void add(int index, E element) {  
  2.      if (index > size || index < 0)  
  3.          throw new IndexOutOfBoundsException(  
  4.          "Index: "+index+", Size: "+size);  
  5.    
  6.      ensureCapacity(size+1);  // Increments modCount!!  
  7.      System.arraycopy(elementData, index, elementData, index + 1,  
  8.               size - index);  
  9.      elementData[index] = element;  
  10.      size++;  
  11. }
  12. ArrayList的中間插入效率比較低,因為需要將index之後的元素往後移動,時間複雜度O(n),但是不代表ArrayList就比linkedlist慢
  1. public void add(int index, E element) {
            checkPositionIndex(index);


            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
     }
  2.     /**
         * Returns the (non-null) Node at the specified element index.
         */
        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;
            }
        }
  3. linkedlist雖然插入的時間複雜度為O(1),但是在每次插入前需要找到插入的位置。前面已經說過linkedlist的node節點是私有內部類,外部不可能拿到node的引用,所以linkedlist只能進行順序定址找到插入的位置,總時間複雜度也是O(n)
     綜上,ArrayList與linkedlist在中間插入的時間複雜度也是一樣
   
   3.迭代器插入
   linkedlist和ArrayList內部都內建了一個迭代器用來遍歷他們裡面的元素,在遍歷的時候也可以插入元素。ArrayList的迭代器插入用的是中間插入的方法,所以時間複雜度還是一樣。但是linkedlist在迭代過程中不需要定位插入的位置,插入的時間複雜度變成O(1)。所以在需要迭代器插入O(n)個元素的時候,ArrayList時間複雜度為O(n^2),而linkedlist的時間複雜度為O(n),這時候才體現了連結串列插入的優勢。但是如果只需要插入常數個元素的時候,迭代器遍歷的開銷大於插入的開銷,此時兩個集合的時間複雜度還是一樣。

   所以,不能簡單的說ArrayList插入速度比linkedlist慢,附上別人做的效能測試,僅供參考
http://blog.csdn.net/dlutbrucezhang/article/details/9931025











相關文章