在程序界有着这样一条众所周知的结论:数组在随机存取方面要比链表快,而链表在处理结点的频繁差插入删除时性能要优于数组。
这个结论的前半部分,我是认可的,数组有按照索引值随机访问的能力,效率当然比链表的顺藤摸瓜要高。但结论的后半部分就有问题了,这里我们来对其进行抽丝剥茧式的探讨。
1、谬论的产生
首先来分析这个错误论断的产生,数组在计算机中的存储是一块连续的内存,通过索引访问;而链表则是不连续的存储单元,通过指针关联,这个基本知识相比大家都明白。
数组中执行插入操作时,将插入点之后的元素依次后移以为,然后将新元素插入到腾出的位置,而进行删除操作时,则是将指定位置的元素删掉,之后的元素依次前移。
而对于链表,处理方式就简单了。需要在某个节点之后插入节点的时候,只需将新节点后继指向新节点即可。删除节点则更为简单,只需要让被删除点的前驱将后继指针指向被删节点的后继即可。
稍微思考一下,所有人都会认为数组在这方面效率很差,我也曾是这样认为。无论是链表还是数组,插入和删除操作都需要两个步骤:找到位置和进行具体操作,上面的比较只是对比了执行具体操作的效率,忽略了找到位置所花的时间。
对于数组来说,查找指定位置就是计算索引,这在计算机中的实现时间是可以忽略不计的,主要的时间将花费在进行具体操作上。而对于链表来说,找到指定的位置时,整个操作就基本上完成了。因为对链表而言执行具体操作的时间与寻找指定位置相比,几乎可以忽略不计。因此上面的比较中计算了数组操作的大头而只计算了链表操作的小头,这样的比较是明显有失公平的。
2、谬论的证明
话不多说看代码
查询效率比较
public static void main(String[] args) {
// 记录开始时间得数组
long[] beginTime = new long[2];
// 记录结束时间得数组
long[] endTime = new long[2];
// 创建一个Integer对象的数组
Integer[] integerArr = new Integer[100000];
// 为Integer对象数组的每个元素赋值
for (int i = 0; i < 100000; i++) {
integerArr[i] = i;
}
// 随机数,用于产生随机访问位置,对ArrayList进行访问
Random r = new Random();
// 创建ArrayList对象
List list = new ArrayList(Arrays.asList(integerArr));
beginTime[0] = System.currentTimeMillis();
int scope = 100000;
for (int i=0; i<100000; i++) {
// 随机进行100000访问
list.get(r.nextInt(scope));
}
endTime[0] = System.currentTimeMillis();
// 对LinkedList进行访问
list = new LinkedList(Arrays.asList(integerArr));
// scope = 1000000;
beginTime[1] = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
list.get(r.nextInt(scope));
}
endTime[1] = System.currentTimeMillis();
System.out.println("ArrayList 运行花费时间: " + (endTime[0] - beginTime[0]));
System.out.println("LinkedList 运行花费时间: " + (endTime[1] - beginTime[1]));
}
运行结果:
ArrayList 运行花费时间: 9
LinkedList 运行花费时间: 5553
这里可以看出ArrayList 和LinkedList 查询之间的差距。证实了数组在随机存取方面要比链表快
删除效率比较
public static void main(String[] args) {
// 记录开始时间得数组
long[] beginTime = new long[2];
// 记录结束时间得数组
long[] endTime = new long[2];
// 创建一个Integer对象的数组
Integer[] integerArr = new Integer[100000];
// 为Integer对象数组的每个元素赋值
for (int i = 0; i < 100000; i++) {
integerArr[i] = i;
}
// 随机数,用于产生随机访问位置,对ArrayList进行访问
Random r = new Random();
// 创建ArrayList对象
List list = new ArrayList(Arrays.asList(integerArr));
beginTime[0] = System.currentTimeMillis();
int scope = 100000;
for (int i=0; i<100000; i++) {
// 根据scope随机删除
list.remove(r.nextInt(scope));
scope--;
}
endTime[0] = System.currentTimeMillis();
// 对LinkedList进行访问
list = new LinkedList(Arrays.asList(integerArr));
scope = 100000;
beginTime[1] = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
// 根据scope随机删除
list.remove(r.nextInt(scope));
scope--;
}
endTime[1] = System.currentTimeMillis();
System.out.println("ArrayList 运行花费时间: " + (endTime[0] - beginTime[0]));
System.out.println("LinkedList 运行花费时间: " + (endTime[1] - beginTime[1]));
}
运行结果:
ArrayList 运行花费时间: 450
LinkedList 运行花费时间: 4809
插入效率比较
public static void main(String[] args) {
// 记录开始时间得数组
long[] beginTime = new long[2];
// 记录结束时间得数组
long[] endTime = new long[2];
// 创建一个Integer对象的数组
Integer[] integerArr = new Integer[100000];
// 为Integer对象数组的每个元素赋值
for (int i = 0; i < 100000; i++) {
integerArr[i] = i;
}
// 随机数,用于产生随机访问位置,对ArrayList进行访问
Random r = new Random();
// 创建ArrayList对象
List list = new ArrayList(Arrays.asList(integerArr));
beginTime[0] = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
// 随机进行100000访问
list.add(r.nextInt());
}
endTime[0] = System.currentTimeMillis();
// 对LinkedList进行访问
list = new LinkedList(Arrays.asList(integerArr));
beginTime[1] = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
list.add(r.nextInt());
}
endTime[1] = System.currentTimeMillis();
System.out.println("ArrayList 运行花费时间: " + (endTime[0] - beginTime[0]));
System.out.println("LinkedList 运行花费时间: " + (endTime[1] - beginTime[1]));
}
运行结果:
ArrayList 运行花费时间: 9
LinkedList 运行花费时间: 7
这里插入效率显示两者相差无几,但是如果继续增加数据量,ArrayList 的效率明显比 LinkedList 效率要高
通过上面删除和插入的效率比较可以证实,“链表在处理结点的频繁差插入删除时性能要优于数组”这句话是错误的。所以建议大家在开发时,如果没有特殊要求,存储方式尽量使用数组类存储方式。
参考资料
《java 程序员职场全攻略》