网上有很多比较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.youkuaiyun.com/dlutbrucezhang/article/details/9931025