LinkList ArrayList 深入研究對比

最近深入瞭解了一下LinkList ArrayList的區別和性能。以往問及什麼時候用ArrayList什麼時候用LinkedList,往往答案是,當大量隨機檢索數據時使用ArrayList,當頻繁插入刪除操作時使用LinkedList。按照數據結構來說,確實是LinkedList在插入刪除操作時的複雜度要低於ArrayList。但在現實代碼實現以及實際執行性能上,卻不一定是這樣。根據查閱的資料整理如下。

首先要了解兩種List的不同應該先了解其代碼實現。
ArrayList是通過索引訪問的數組實現,而LinkedList使用過 鏈表原理實現。
ArrayList的get(i)不需要遍歷可以直接通過索引獲取數據,而LinkedList需要遍歷,遍歷到i索引位置時將當前位置數據返回。
網上有人貼了這樣一段代碼:

public static void main(String[] args) {
// TODO Auto-generated method stub
LinkedList list = new LinkedList();
for (int i =0;i<=1000000; i++) {
list.add(i);
}
long start = System.currentTimeMillis(); 
list.get(1);
long end = System.currentTimeMillis();
System.out.println(" time to traverse 1st element " + (end - start) );


start = System.currentTimeMillis(); 
list.get(999999);
end = System.currentTimeMillis();
System.out.println(" time take to traverse 999999th element " + (end - start) );
}

time to traverse 1st element 0
time to traverse 999999th element 0

他問既然LinkedList需要遍歷讀取數據,爲什麼我獲取第一個數據和最後一個數據用的時間是一樣的。
看一下LinkedList的get代碼就應該明白了,遍歷可能會從列表頭部或末尾開始進行,取決於i在列表的前半部還是後半部分:

public Object get(int index) { 
//check the index is valid first .. code not shown here 
Entry e = header; //starting node 
//go forwards or backwards depending on which is closer 
if (index < size/2) { 
for (int i = 0; i <= index; i++) 
e = e.next; } 
else { 
for (int i = size; i > index; i--) 
e = e.previous; 
} 
return e; 
}

同樣在特定索引位置插入數據時,也是先遍歷到那個位置再進行插入。這樣很容易理解,當很大數據量時(比如1W),在LinkedList前部和後部進行刪除插入操作,速度會很快,但是如果是在中部進行操作,測試結果就不那麼樂觀了,性能甚至不及ArrayList。

所以建議在數據量很大,同時存在鏈表頭部以及尾部的頻繁操作,並且很少執行中部數據操作時使用鏈表。其實這種情況也可以考慮使用ArrayDeque,因爲鏈表在新增數據時會創建額外對象,記錄上一node和下一個node的信息,會佔用額外空間,而ArrayDeque則不會增加額外空間,不會增加GC工作。其他情況在ArrayList初始化大小設置合理時使用ArrayList能獲得更好性能。

知道LinkedList的應用場景,再說說如何正確使用LinkedList。
首先是遍歷,遍歷LinkedList必須使用iterator不能使用for循環,因爲每次for循環體內通過get(i)取得某一元素時都需要對list重新進行遍歷,性能消耗極大。
另外不要試圖使用indexOf等返回元素索引,並利用其進行遍歷,例如下面的代碼:
final int startPos = lst.indexOf( first ) + 1; //使用indexlOf對list進行了遍歷,當結果爲空時會遍歷整個列表。

final ListIterator<String> iter = lst.listIterator( startPos ); //如果startPos的元素

在列表前半部,那這兩行代碼相當於平均要遍歷了列表1.25的長度,因爲 listIterator( startPos )也需要從頭遍歷,當到達指定元素後開始循環如下操作。

while ( iter.hasNext() ) { 
if ( iter.next().length() == 5 ) 
iter.remove(); 
}

實際測試:

public class TestList {
private final static int SIZE = 100000000;
private LinkedList<Integer> ll = new LinkedList<Integer>();
private ArrayList<Integer> al = new ArrayList<Integer>(1000000);
public static void main(String[] args) {
TestList testList = new TestList();
System.out.println(" time to add element to 1st of ArrayList " + String.valueOf(testList.TestAddToHeadOfArrayList() ));
System.out.println(" time to add element to 1st of LinkedList " + String.valueOf(testList.TestAddToHeadOfLinkedList()) );
}
public long TestAddToHeadOfArrayList() { 
for (int i = 0; i < SIZE; ++i) {
al.add(i);
}
long start = System.nanoTime();
al.add(0,-1);
long end = System.nanoTime();
return end - start;
}

public long TestAddToHeadOfLinkedList() {
for (int i = 0; i < SIZE; ++i) {
ll.add(i);
}
long start = System.nanoTime();
ll.add(0,-1);
long end = System.nanoTime();
return end - start;
}

}

使用如上代碼測試,測試環境:

CPU I-5 2.6GHZ 內存16G 硬盤SSD 250G JVM1.6
在1億條數據的al和ll列表前端執行插入,結果:

al — 51522813 納秒(當插入數據後正好列表空間已滿時,執行時間大概249400289納秒)
ll — 91185 納秒

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章