作为Java中重要的一种数据结构,有必要好好研究下
List接口三个主要实现类ArrayList, Vector和LinkedList,下面从底层维护,增加,查找和删除效率分析ArrayList和LinkedList
ArrayList
通过查看源代码可知,ArrayList维护着Object[] elementData数组和int size变量(内部数组中的元素个数,初始值为0)
1. 下面看看add(E e)方法实现:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
从源代码可知,在每次调用add(E e)方法将元素增加到队列尾端,在增加之前都需要进行容量检查,即确保内部数组有足够的空间,进入到ensureCapacityInternal(int minCapacity)方法中
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在该方法中,先做if判断,如果此时 我们需要的最小容量(minCapacity)大于当前内部数组的长度(elementData.length),那么内部数组容量不够,需要进行扩容,随后调用grow(int minCapacity)方法;否则返回add(E e)方法,直接将该元素加入到内部数组中,size++(表示此时内部数组中的元素个数)。
下面看下gorw(int minCapacity)方法源代码
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
int newCapacity = oldCapacity + (oldCapacity >> 1);
这条语句表示扩容为原来的1.5倍。然后进行数组复制
elementData = Arrays.copyOf(elementData, newCapacity);
最终调用System.arraycopy()方法。
由以上可知,在调用ArrayList的add(E e)方法,即将元素插入到数组尾部,故时间复杂度为O(1)。
下面看看LinkedList的add(E e)方法实现
LinkedList底层维护着一个双向链表数据结构,一个表项结点包含3个部分:元素内容,前驱指针和后驱后驱。在JDK的实现中,无论LinkedList是否为空,链表内都有一个header结点--即头结点。
通过源代码可以看到其结点的定义
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;
}
}
下面看看其add(E e)方法实现
public boolean add(E e) {
linkLast(e);
return true;
}
跟进去
/**
* Links e as last element.
*/
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++;
}
add(E e)方法也是将元素插入到链表的尾部,但是与ArrayList的add()方法不同的是,该方法没有做容量检查。是直接生成一个Node<E>结点并将其插入到链表尾部,其时间复杂度也为O(1)。
下面通过实验比较着两种方法的速度
Object obj = new Object();
long begin = System.currentTimeMillis();
for(int i = 0; i < 500000; i++)
{
list.add(obj);
}
System.out.println(System.currentTimeMillis() - begin);
通过输出,ArrayList耗时15ms, LinkedList耗时31ms.因为LinkedList在添加元素时需要不断的生成新的对象;而对于ArrayList,只有在空间不足时才产生数组扩容和数组复制,所以ArrayList的add()方法效率还是非常高的。
2. 增加元素到列表任意位置
void add(int index, E element)
由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该元素后的所有元素需要重新排列,所以其效率相对会非常低,下面看看该方法源代码:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
该方法也是要做容量检查,然后做数据复制。大量的数组重组操作会导致系统性能低下。并且,插入的元素在List的位置越靠前,数组重组的开销也越大。尽可能的将元素插入到List的尾端,有助于提高该方法的性能。
该方法平均移动元素的次数为(n - 1)/2,故时间复杂度为O(n).
LinkedList的void add(int index, E element)方法
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
对于LinkedList来说,在List的任何位置插入数据都是一样的,不会因为插入的位置而影响到该方法的性能。
该方法时间复杂度为O(1).
下面每次都将元素插入到list的最前端,比较着两个方法的差别
Object obj = new Object();
for(int i = 0; i < 50000; i++)
{
list.add(0, obj);
}
结果显示ArrayList耗时827ms, 而LinkedList耗时31ms。可见两者在性能上的差异。
3.E get(int index)方法比较
ArrayList该方法源码
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
直接返回该对象。故时间复杂度为O(1).
LinkedList中该方法源码
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
根进去
/**
* 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的get(int index)方法的实现是,若该元素在链表的前半段,则从前半段找,否则从后半段找。但是如果我们要查找中间的那个元素,那几乎要遍历完半个List,当List拥有大量元素时,其效率是很低的。