ArrayList和LinkedList在三種遍歷方法中的效率測試
前言:對於線性表List而言,熟知的有兩種結構,順序表和鏈表,在Java中對應的也就是ArrayList和LinkedList兩種List類型。而在Java中,我們知道常用的遍歷數組方法有最樸素的for循環,內置的迭代器(形如 for (String str: S) )和顯式的迭代器(Iterator)這三種,這篇博客主要是對這三種遍歷方法進行測試後進行一些分析和總結。
一. 數據生成
final static String str="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; //保證字符串只含有數字和字母
public static String MakeString() {
Random random = new Random();
int len = random.nextInt(10)+1; //隨機字符串長度
StringBuffer s = new StringBuffer();
for (int i = 0; i < len; ++i) {
int x = random.nextInt(62); //隨機取str字符串中的一個字符
s.append(str.charAt(x));
}
return s.toString();
}
代碼如上,可以構造一個長度在十以內,只含數字和字母的隨機字符串。本實驗中使用的數據量規模爲100w,數據採用文本輸出的方式寫入data.txt文件中,便於接下來的測試使用。
二. 測試環節
- 我們先來看ArrayList的測試,直接上代碼
//傳統遍歷
beginTime = System.currentTimeMillis();
int Size = S.size();
for (int k = 0; k < 1000; ++k) {
for(int i = 0; i < Size; i++) {
str = S.get(i);
}
}
endTime = System.currentTimeMillis();
System.out.printf("普通for用時:%d\n", endTime-beginTime);
//內置迭代
beginTime = System.currentTimeMillis();
for (int k = 0; k < 1000; ++k) {
for(String s : S) {
str = s;
}
}
endTime = System.currentTimeMillis();
System.out.printf("內置迭代用時:%d\n", endTime-beginTime);
//顯式迭代
Iterator<String> it;
beginTime = System.currentTimeMillis();
for (int k = 0; k < 1000; ++k) {
it = S.iterator();
while(it.hasNext()) {
str = it.next();
}
}
endTime = System.currentTimeMillis();
System.out.printf("顯式迭代用時:%d\n", endTime-beginTime);
對於100w的數據,每種遍歷方式測試1000次。爲了保證測試結果的相對準確,如上的測試一共進行了五次,得到如下測試結果(單位:ms)
普通for | 內置迭代器 | 顯式迭代器 | |
---|---|---|---|
第一次 | 3799 | 5923 | 5984 |
第二次 | 3649 | 3727 | 5925 |
第三次 | 3690 | 3934 | 5947 |
第四次 | 3601 | 3697 | 5857 |
第五次 | 3913 | 4016 | 6238 |
- LinkedList的測試和ArrayList一致,只是在測試普通for循環時要注意超時的攔截,下面只給出普通for循環的測試代碼
beginTime = System.currentTimeMillis();
for (int k = 0; k < 1000; ++k) {
for(int i = 0; i < Size; i++) {
str = S.get(i);
if (System.currentTimeMillis()-beginTime >= 20000) { //如果運行時間超過了20s,就直接跳出循環
System.out.printf("TLE at the %d time", k+1);
break;
}
}
if (System.currentTimeMillis()-beginTime >= 20000) break;
}
測試發現for循環在第一次遍歷(100w的數據規模)中就已經超過了20s,這個結果結合鏈式線性表的結構分析並不奇怪;之後兩種循環方式的結果見下表(單位:ms)
內置迭代器 | 顯式迭代器 | |
---|---|---|
第一次 | 16188 | 16225 |
第二次 | 17034 | 15881 |
第三次 | 17091 | 15895 |
第四次 | 17246 | 15840 |
第五次 | 17192 | 16164 |
3.結果比較
我們將兩次的測試結果整理成折線圖
橫向比較,可以直觀地看出,ArrayList的遍歷速度優於LinkedList;
縱向比較,對於ArrayList來說,for循環一直穩定在一個較快的速度上,而顯式迭代器則相反穩定在一個較慢的速度上,只有內置迭代器比較奇怪,第一次測試中速度較慢,但是之後的速度基本上和for循環在同一水平上;對於LinkedList而言,數據又不太一樣,忽略for循環,內置和顯式迭代器速度相近,甚至內置迭代器速度還要快一些。
三. 測試分析
- ArrayList
一些資料中說,在遍歷ArrayList時,普通for循環要比iterator快,但是這種論斷也只停留在一個實踐和經驗的判斷上,沒有理論支撐;更讓人費解的是內置的迭代器在經過第一次較慢的遍歷,之後的遍歷速度都大大加快了,不知道是不是經過了系統的優化。這樣的猜想主要有兩個原因:一是我在一些其他類似的測試中看到有人把內置迭代器的彙編代碼反彙編後,得到的Java代碼與顯式迭代器基本一致,這也就驗證了爲什麼第一次測試中,兩種迭代器的速度相近;二是在官方API文檔中,有這樣一個論述:
Generic list algorithms are encouraged to check whether the given listis an instanceof this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable performance.
也許Java編譯器在經過第一次的poor performance後進行了調整?這只是我的一個猜想,不過我覺得這大概率是不正確的解釋。希望讀者在評論中能給予我這個蒟蒻一個解答。
- LinkedList
對於鏈表,我能解釋的也就只有最慢的for循環。根據上學期的數據結構知識,我們知道對於鏈表而言,不能直接查詢隨機位置的元素,所以一個O(n)的遍歷硬生生變成了O(n^2). 而兩種迭代器的表現足以驗證我對ArrayList的猜想很可能不正確(如果Java編譯器能優化ArrayList的迭代,怎麼不順便把LinkedList一起優化了呢)😦
四. 總結
ArrayList和LinkedList作爲兩種常用的數組結構,有着各自的優勢和不足,例如ArrayList適合數組隨機位置的查詢,LinkedList適合數組的增刪;而單論數組的遍歷速度,最優的方式無疑是ArrayList的普通for循環。但是在大型軟件的開發中,考慮的不僅僅是運行速度問題,爲了保障軟件的正確性等其他一些特性時,內置迭代器有可能也是一個不錯的選擇。總之,循環遍歷方式的選擇需要我們根據實踐和經驗,做出合理的判斷。