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循环。但是在大型软件的开发中,考虑的不仅仅是运行速度问题,为了保障软件的正确性等其他一些特性时,内置迭代器有可能也是一个不错的选择。总之,循环遍历方式的选择需要我们根据实践和经验,做出合理的判断。