文章目录
0. 前言
ArrayList 和 LinkedList 的区别,本质上就是数组和链表的区别,相信学过数据结构的同学都知道数组和链表的区别
如果是细问 ArrayList 和 LinkedList 在某个方面上的区别,大部分同学都能够答出来
但如果问的是 ArrayList 和 LinkedList 的区别,很多同学就答得很差了,可能会东扯一段,西扯一段,甚至还会出现说到某一部分的时候,发现上一部分说得又有点问题,又继续补充上一部分的情况
说到底,之所以回答得不是很好,是因为缺少了归纳总结这一环节,今天带大家一起总结一下 ArrayList 和 LinkedList 的区别
1. 底层的数据结构
- ArrayList 底层采用数组来存储元素
- LinkedList 底层采用双向链表来存储元素
2. 随机访问元素的性能
- ArrayList 在随机访问元素上更高效,因为 ArrayList 可以根据下标来计算元素在数组中的位置
- LinkedList 在随机访问元素上性能较差,因为 LinkedList 需要从头部或尾部开始遍历链表,找到元素在链表中的位置
我们来测试一下 ArrayList 和 LinkedList 在随机访问元素上的性能差异
创建 ArrayList 和 LinkedList,分别往两个列表中插入 10 万条数据,接着随机访问列表中的元素
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;
public class ReadTests {
private static final int SIZE = 100000;
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < SIZE; i++) {
arrayList.add(i);
}
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < SIZE; i++) {
linkedList.add(i);
}
long start = System.currentTimeMillis();
readArrayList(arrayList);
long end = System.currentTimeMillis();
System.out.println("ArrayList read time: " + (end - start) + "ms");
start = System.currentTimeMillis();
readLinkedList(linkedList);
end = System.currentTimeMillis();
System.out.println("LinkedList read time: " + (end - start) + "ms");
}
private static void readArrayList(ArrayList<Integer> arrayList) {
Random random = new Random();
for (int i = 0; i < SIZE; i++) {
arrayList.get(random.nextInt(arrayList.size()));
}
}
private static void readLinkedList(LinkedList<Integer> linkedList) {
Random random = new Random();
for (int i = 0; i < SIZE; i++) {
linkedList.get(random.nextInt(linkedList.size()));
}
}
}
测试结果如下(当然,严谨的做法是做多次测试)
可以看到,ArrayList 随机访问元素的性能是 LinkedList 的几百倍
3. 插入元素和删除元素的性能
- ArrayList 在数组尾部添加元素或删除元素的性能较好,因为在数组尾部添加元素或删除元素不涉及数组中元素的移动,但是在
数组中间或数组头部
插入元素或删除元素的时候,ArrayList 会涉及到数组中元素的移动,性能相对较低 - LinkedList 在任意位置插入元素或删除元素的性能比较好,因为只需要调整链表中指针的指向
4. 内存占用
-
ArrayList 使用数组来存储元素,占用的空间是连续的,可能会产生内存碎片
-
LinkedList 通过链表来存储元素,每个元素都包含前后节点的引用,占用的空间相对较大
5. 使用场景
- ArrayList 更适合随机访问操作频繁的场景
- LinkedList 更适合插入和删除操作频繁的场景,尤其是插入和删除操作发生在列表的头部时
6. 频繁地进行插入操作,ArrayList和LinkedList哪个性能更好
按照上面的分析,如果需要频繁地进行插入操作,LinkedList 的性能是更好的,因为进行插入操作时, LinkedList 只需要调整链表中指针的指向,而 ArrayList 进行插入操作时,可能会涉及到数组元素的移动,性能较差
然而,事实真的如此吗,我们来做一个测试
6.1 在列表头部频繁地进行插入操作
import java.util.ArrayList;
import java.util.LinkedList;
public class HeadInsertTests {
private static final int SIZE = 100000;
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
LinkedList<Integer> linkedList = new LinkedList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
arrayList.add(0, i);
}
long end = System.currentTimeMillis();
System.out.println("ArrayList head insert time: " + (end - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
linkedList.addFirst(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList head insert time: " + (end - start) + "ms");
}
}
测试结果如下
不出意外,在列表头部频繁地进行插入操作,LinkedList 的性能比 ArrayList 高
6.2 在列表尾部频繁地进行插入操作
import java.util.ArrayList;
import java.util.LinkedList;
public class TailInsertTests {
private static final int SIZE = 100000;
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
LinkedList<Integer> linkedList = new LinkedList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
arrayList.add(i);
}
long end = System.currentTimeMillis();
System.out.println("ArrayList tail insert time: " + (end - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
linkedList.addLast(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList tail insert time: " + (end - start) + "ms");
}
}
测试结果如下
可以看到,在列表尾部频繁地进行插入操作,虽然 LinkedList 的性能还是比 ArrayList 高,但是差距已经非常小
我们把数据量提到 100 万,再次进行测试
可以看到数据量提到 100 万时,在列表尾部频繁地进行插入操作,ArrayList 的性能已经远远高于 LinkedList
但是,我们忽略了一个因素,ArrayList 的括容操作也会造成一定的性能消耗,那如果我们在创建 ArrayList 时就指定数组的大小呢
测试结果如下
可以发现 ArrayList 的性能又提升了一点点
如果我们事前能够预料到数据量的范围,可以为 ArrayList 指定一个大小,避免因为扩容而造成不必要的性能开销
6.3 在列表的随机位置频繁地进行插入操作
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;
public class RandomInsertTests {
private static final int SIZE = 100000;
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
LinkedList<Integer> linkedList = new LinkedList<>();
Random random = new Random();
long start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
int randomNumber = random.nextInt(SIZE);
int randomIndex = random.nextInt(arrayList.size() + 1);
arrayList.add(randomIndex, randomNumber);
}
long end = System.currentTimeMillis();
System.out.println("ArrayList random insert time: " + (end - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
int randomNumber = random.nextInt(SIZE);
int randomIndex = random.nextInt(linkedList.size() + 1);
linkedList.add(randomIndex, randomNumber);
}
end = System.currentTimeMillis();
System.out.println("LinkedList random insert time: " + (end - start) + "ms");
}
}
测试结果如下
为什么在列表的随机位置频繁地进行插入操作,ArrayList 的性能比 LinkedList 还要高呢,插入操作不是 LinkedList 的优势吗
光从算法层面来讲
- 数组随机访问的时间复杂度是 O(1),插入操作的时间复杂度是 O(n)
- 链表随机访问的时间复杂度是 O(n),插入操作的时间复杂度是 O(1)
但 LinkedList 插入的时间复杂度并不是 O(1),我们看一下 LinkedList 的 add 方法的源码
LinkedList 在插入元素前,会花费 O(n) 的时间复杂度去找到要插入的位置
7. 扩展:影响ArrayList和LinkedList性能的因素
事实上,ArrayList 和 LinkedList 在性能上的差别还与内存和缓存有很大的关联
ArrayList 在内存中是紧凑排列的,可以利用空间局部性,这意味着 ArrayList 比 LinkedList 更适合缓存,而 LinkedList 是分布在整个内存上的,不适合缓存
空间局部性的概念(人工智能给出的回答,仅供参考):
在计算机科学中,空间局部性(Spatial Locality)是指当程序访问了某个存储位置或指令时,那么在不久的将来很可能访问与其存储位置相邻的数据或指令。这一概念基于程序执行的统计特性,也是计算机体系结构中缓存设计的重要基础
具体来说,空间局部性体现在以下方面:
- 编程中的体现:例如,在循环中访问数组时,由于数组在内存中是连续存储的,程序依次访问数组元素,这就体现了空间局部性。访问
array[i]
和array[i+1]
时,程序会访问相邻的内存地址 - 实际编程开发的影响:理解空间局部性对于编写高性能的程序非常关键。现代处理器利用多级缓存来提高性能,而空间局部性可以显著提高缓存命中率,减少内存访问延迟