您的位置:首页 > 理论基础 > 数据结构算法

ArrayList与linkedlist插入效率分析

2017-05-23 22:20 197 查看
网上有很多比较ArrayList和LinkedList的文章,基本上都认为ArrayList是用数组实现,而LinkedList用链表实现,所以ArrayList查询效率高,LinkedList插入效率高,但是事实真的是这样吗?

比较他们的使用效率之前,先看看ArrayList与LinkeLlist的底层数据结构。

/** 

      * The array buffer into which the elements of the ArrayList are stored. 

      * The capacity of the ArrayList is the length of this array buffer. 

      */  

     private transient Object[] elementData;  

   

     /** 

      * The size of the ArrayList (the number of elements it contains). 

      * 

      * @serial 

      */  

     private int size; 

ArrayList内部用数组来保存元素,size记录当前数组的大小。但是数组的大小是确定的,那么他是怎么实现ArrayList这种大小可变的集合的呢?

public boolean add(E e) {  

    ensureCapacity(size + 1);  // Increments modCount!!  

    elementData[size++] = e;  

    return true;  

}

/** 

      * Increases the capacity of this <tt>ArrayList</tt> instance, if 

      * necessary, to ensure that it can hold at least the number of elements 

      * specified by the minimum capacity argument. 

      * 

      * @param   minCapacity   the desired minimum capacity 

      */  

     public void ensureCapacity(int minCapacity) {  

     modCount++;  

     int oldCapacity = elementData.length;  

     if (minCapacity > oldCapacity) {  

         Object oldData[] = elementData;  

         int newCapacity = (oldCapacity * 3)/2 + 1;  

             if (newCapacity < minCapacity)  

         newCapacity = minCapacity;  

             // minCapacity is usually close to size, so this is a win:  

             elementData = Arrays.copyOf(elementData, newCapacity);  

     }  



每次ArrayList在添加元素的时候都会调用ensureCapacity方法,这个方法会检查数组容量是否够大,如果不够则新创建一个容量为1.5倍的数组,

并把数据复制到这个新数组上。

所以不能简答的说ArrayList底层数据结构是一个数组,其实它是一个动态表。
   接下来看看LinkedList的实现,它是一个双向循环链表。

//链表大小,transient代表不可序列化  

transient int size = 0;  

  

/** 

 * 指向头节点的指针 

 */  

transient Node<E> first;  

  

/** 

 * 指向尾节点的指针 

 */  

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;  

    }  

}

值得注意的是,这里的链表节点是linkedlist的私有内部类,也就是说其他类是不可能拿到节点的引用的。所以在链表中间插入的时候,通过修改几个指针就能插入一个节点这样的操作是不可能的。
    
    了解底层实现之后,我们开始比较ArrayList与linkedlist的插入效率。
    1.尾部插入
    

public boolean add(E e) {
 

    ensureCapacity(size + 1);  // Increments modCount!!  

    elementData[size++] = e;  

    return true;
 

}

ArrayList最常用的add方法就是尾部插入,在不需要扩容的情况下,只需要将size递增即可,时间复杂度O(1)

需要扩容的情况下,时间复杂度也不会受到影响。理由如下:

假设它每次扩容两倍,那么从开始的16个元素扩容到n,共需要复制16+32+。。。+n/4+n/2+n<2n个元素

所以平均到每一次添加操作,扩容只需要复制常数个元素 

/** 

*默认的添加动作,可以看到这个方法是把新元素添加 到表尾 

*/  

public boolean add(E e) {  

    addBefore(e, header); //加到头结点之前 ,即表尾  

        return true;  

}

/**  

*将元素e添加到entry结点之前  

*/  

private Entry<E> addBefore(E e, Entry<E> entry) {  

    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);  

    newEntry.previous.next = newEntry; //将新结点与前后结点相连接   

    newEntry.next.previous = newEntry;  

    size++;  

    modCount++;  

    return newEntry;
 

}

可以看出linkedlist的最常用的add方法也是插入尾部,只需要改变几个指针,时间复杂度O(1)

   综上,在两个集合最常用的尾部插入时间复杂度都是O(1),不存在ArrayList插入比linkedlist慢的说法

   2.中间插入

public void add(int index, E element) {  

     if (index > size || index < 0)  

         throw new IndexOutOfBoundsException(  

         "Index: "+index+", Size: "+size);  

   

     ensureCapacity(size+1);  // Increments modCount!!  

     System.arraycopy(elementData, index, elementData, index + 1,  

              size - index);  

     elementData[index] = element;  

     size++;  

}

ArrayList的中间插入效率比较低,因为需要将index之后的元素往后移动,时间复杂度O(n),但是不代表ArrayList就比linkedlist慢

public void add(int index, E element) {

        checkPositionIndex(index);

        if (index == size)

            linkLast(element);

        else

            linkBefore(element, node(index));

 }

    /**

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

        }

    }

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息