1.1 ArrayList 和 LinkedList 的区别是什么?
ArrayList比LinkedList访问要更快,LikedList要比ArrayList插入要更快 ~~~ 吗?
我想你点击进来想要看到的回答一定不会想要是这个,下面我们按照老规矩 图解+源码+时间分析+空间分析 来带你逐步了解这个问题的本质
1.2 时间复杂度
(本篇章是不了解这部分的同学准备的,如果已经具备这部分的知识请移步至下一个篇章)
大家常说的 BIG-O: O(1)、O(n) 、 O(n^2)都是什么意思呢?下面我来给各位通俗易懂的讲解他们的场景。
O(1) 常数时间
int[] arr = new int[] {0,2,3,4};
sout(arr[2]);
对于数组的访问就是一种 O(1) 的时间复杂度,不需要经过任何循环
O(n[^1]) 线性时间
int[] arr = new int[] {0,2,3,4};
// 寻找指定元素的下标
int findNum = 3;
for (int i = 0; i < arr.length ; i++){
if(arr[i] == findNum){
sout(i);
}
}
对于数组中寻找指定的元素下标就是一种 O(n) 的时间复杂度,需要遍历整个数组(注意这里我们只遍历了3次,但是对于时间复杂度的分析都是只考虑最坏的情况)
O(n^2) 线性时间
int[] arr = new int[] {0,2,3,4};
int[] arr2 = new int[] {1,5,3,10};
// 寻找arr相较于arr2中第一个出现重复的元素下标
int findNum = 3;
for (int i = 0; i < arr.length ; i++){
for (int j = 0; j < arr2.length ; j++){
if(arr[i] == arr2[j]){
sout(i);
}
}
}
在O(n)的循环基础每次遍历操作中仍然需要遍历列表
======================正文开始=======================
我们将从get头部、get尾部、get随机、add头部、add尾部、add随机下标方法来解读ArrayList、LinkedList
2.1 ArrayList
(时间复杂度的分析讲解完毕后我们来正式进入这篇问题的内容)
ArrayList的构建基于数组来完成,数组天然就有访问快的优势。内存空间连续,不仅可以通过计算内存地址的方式就找到特定下标的元素,而且迎合计算机CPU读取。但是一个List需要考虑的不单单只有读取,还有考虑插入。数组的内存空间是在创建时就已经申请好了的,无法再变更大小,所以使用数组来构建集合还要考虑一个扩容的问题。
ArrayList扩容
ArrayList的扩容需要申请更大的内存空间,然后将原来数组的元素依次拷贝到新的数组中,线性时间O(n)
头部get
直接拿到arr[0]的元素,非常简单,时间复杂度为O(1)
尾部get
直接拿到arr[size - 1]的元素,非常简单,时间复杂度为O(1)
随机get
直接拿到arr[参数下标]的元素,非常简单,时间复杂度为O(1)
头部add
ArrayList 的添加元素就开始变得有点困难了。在添加元素时需要把所有的元素都向后移动一位然后再 arr[0] = element 如果这个集合的 size 等于数组的 length 就需要扩容了,时间复杂度O(n)、O(n^2)
尾部add
如果在空闲充足的情况下只需要使用 arr[size - 1] = element 时间复杂度为O(1),但是如果空间已满就需要扩容的情况下时间复杂度为O(n)
随机add
与头部add基本保持一致
总结:ArryList在元素访问时的效率非常高,基本上都是常数时间。但是在添加时可能需要挪动部分的元素以及扩容操作,这系列基本都是线性级别的。并且在元素删除时无法立刻对删除元素的内存进行GC只有等整个数组被GC
2.2 LinkedList
使用双向链表实现,可以直接访问头节点以及尾节点(first、last), 内存空间并不连续无需考虑扩容问题,每个元素需要一个Node对象包裹
头部get
直接可以访问first节点,时间复杂度为O(1)
尾部get
直接可以访问last节点,时间复杂度为O(1)
随机get
需要从first或者是last开始获取。但是内部做了优化,如果get的部分小于size的一半从first开始读取,如果get的部分大于size的一半从last开始读取。时间按复杂度为O(n)
头部add
更改当前节点为 first 节点,并将当前的 next 指向之前的节点,之前的头节点 prev 指向当前节点,时间按复杂度为O(1)
尾部add
更改当前节点为 last 节点,并将当前的 next 指向之前的节点,之前的头节点 prev 指向当前节点,时间按复杂度为O(1)
随机add
需要从first或者是last开始获取,获取到指定下标上的元素,并将它以及它后面元素的指针修改。但是内部做了优化,如果add的部分小于size的一半从first开始读取,如果get的部分大于size的一半从last开始读取。时间按复杂度为O(n)
总结:在操作头尾元素时效率为常数时间,如果需要随机 添加|获取一个下标上的元素最坏的情况需要遍历半数的列表节点。LinkedList的操作都不需要考虑扩容的逻辑,并且一个元素被删除GC能够立马回收。
3.1 对比
操作 | ArrayList | LinkedList |
---|---|---|
头部get | 1 | 1 |
尾部get | 1 | 1 |
随机get | 1 | n |
头部add&set | n | 1 |
尾部add&set | 1 | 1 |
随机add&set | n | n |
是否需要扩容 | 是 | 否 |
内存空间是否连续 | 是 | 否 |
如果你需要在头部、尾部插入、修改,并且需要遍历整个元素时可以选择LinkedList
如果你需要获取非常频繁时可以选择ArrayList
在真实场景很多时候是依靠取舍来选择的,理解场景再选择所需要的List对象