ArrayList与linkedlist插入效率分析

来源:互联网 发布:java urlencode转码 编辑:程序博客网 时间:2024/06/06 04: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











原创粉丝点击