java.util.LinkedList是双向链表,这个你们都知道,好比Java的基础面试题喜欢问ArrayList和LinkedList的区别,在什么场景下用。你们都会说LinkedList随机增删多的场景比较合适,而ArrayList的随机访问多的场景比较合适。更进一步,我有时候会问,LinkedList.remove(Object)方法的时间复杂度是什么?有的人回答对了,有的人回答错了。回答错的应该是没有读过源码。java
理论上说,双向链表的删除的时间复杂度是O(1),你只须要将要删除的节点的前节点和后节点相连,而后将要删除的节点的前节点和后节点置为null便可,
[java]
//伪代码
node.prev.next=node.next;
node.next.prev=node.prev;
node.prev=node.next=null;
[/java]
这个操做的时间复杂度能够认为是O(1)级别的。可是LinkedList的实现是一个通用的数据结构,所以没有暴露内部的节点Entry对象,remove(Object)传入的Object实际上是节点存储的value,这里还须要一个查找过程:
[java]
public boolean remove(Object o) {
if (o==null) {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
//查找节点Entry
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
//删除节点
remove(e);
return true;
}
}
}
return false;
}
[/java]node
删除节点的操做就是刚才伪代码描述的:
[java]
private E remove(Entry<E> e) {
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
[/java]
所以,显然,LinkedList.remove(Object)方法的时间复杂度是O(n)+O(1),结果仍然是O(n)的时间复杂度,而非推测的O(1)复杂度。最坏状况下要删除的元素是最后一个,你都要比较N-1次才能找到要删除的元素。面试
既然如此,说LinkedList适合随机删减有个前提,链表的大小不能太大,若是链表元素很是多,调用remove(Object)去删除一个元素的效率确定有影响,一个简单测试,插入100万数据,随机删除1000个元素:
[java]
final List<Integer> list = new LinkedList<Integer>();
final int count = 1000000;
for (int i = 0; i < count; i++) {
list.add(i);
}
final Random rand=new Random();
long start=System.nanoTime();
for(int i=0;i<1000;i++){
//这里要强制转型为Integer,不然调用的是remove(int)
list.remove((Integer)rand.nextInt(count));
}
System.out.println((System.nanoTime()-start)/Math.pow(10, 9));
[/java]
在个人机器上耗时近9.5秒,删除1000个元素耗时9.5秒,是否是很恐怖?注意到上面的注释,产生的随机数强制转为Integer对象,不然调用的是 remove(int)方法,而非remove(Object)。若是咱们调用remove(int)根据索引来删除:
[java]
for(int i=0;i<1000;i++){
list.remove(rand.nextInt(list.size()-1));
}
[/java]
随机数范围要递减,防止数组越界,换成remove(int)效率提升很多,可是仍然须要2.2秒左右(包括了随机数产生开销)。这是由于 remove(int)的实现颇有技巧,它首先判断索引位置在链表的前半部分仍是后半部分,若是是前半部分则从head往前查找,若是在后半部分,则从 head日后查找(LinkedList的实现是一个环):
[java]
Entry<E> e = header;
if (index < (size >> 1)) {
//前一半,往前找
for (int i = 0; i <= index; i++)
e = e.next;
} else {
//后一半,日后找
for (int i = size; i > index; i--)
e = e.previous;
}
[/java]
最坏状况下要删除的节点在中点左右,查找的次数仍然达到n/2次,可是注意到这里没有比较的开销,而且比remove(Object)最坏状况下n次查找仍是好不少。数组
总结下,LinkedList的两个remove方法,remove(Object)和remove(int)的时间复杂度都是O(n),在链表元素不少而且没有索引可用的状况下,LinkedList也并不适合作随机增删元素。在对性能特别敏感的场景下,仍是须要本身实现专用的双向链表结构,真正实现 O(1)级别的随机增删。更进一步,jdk5引入的ConcurrentLinkedQueue是一个非阻塞的线程安全的双向队列实现,一样有本文提到的问题,有兴趣能够测试一下在大量元素状况下的并发随机增删,效率跟本身实现的特定类型的线程安全的链表差距是惊人的。安全
题外,ArrayList比LinkedList更不适合随机增删的缘由是多了一个数组移动的动做,假设你删除的元素在m,那么除了要查找m次以外,还须要往前移动n-m-1个元素。数据结构