LinkedList的侷限

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個元素。數據結構

相關文章
相關標籤/搜索