ArrayList 底层是数组,LinkedList 底层是链表。
所以我分为两个角度回答这个问题:
- 数组和链表有什么区别?
- ArrayList 和 LinkedList 有什么区别?
数组和链表有什么区别?
数组
数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据(Object 数组有点特殊)。
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过寻址公式(a[i]_address = base_address + i * data_type_size),计算出该元素存储的内存地址。
因为数组使用是一组连续的内存空间,所以它可以借助 CPU 的缓存机制,预读数组中的数据,弥补内存访问速度过慢与CPU执行速度快之间的差异。
数组为了保持内存数据的连续性,插入、删除的平均情况时间复杂度为O(n)。
假设数组长度为 n。
如果数组是有序的,在数组第k(k<n)个位置插入一个新的元素,此时就需要将第k~n的元素都往后挪一位。此时插入的时间复杂度是O(n)。若在数组尾部插入的时间复杂度为O(1)。
如果数组是无序的,只需要将新元素放入数组末尾然后和第k个位置的元素交换位置。在这个场景下,在第 k 个位置插入一个元素的时间复杂度就会降为 O(1)。
如果我们要删除第 k(k<n) 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。此时删除的时间复杂度是O(n)。若在数组尾部删除时间复杂度为O(1)。
但是如果我们可以将多次删除操作集中在一起执行,删除的效率就会提高很多。我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。
这和 JVM 标记清除垃圾回收算法的核心思想很像。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
链表
与数组不同的地方是链表并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,链表本身没有大小的限制,天然地支持动态扩容。每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。
链表要想随机访问第 k 个元素,需要根据指针从头节点开始遍历,时间复杂度是 O(n)。
链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以在链表中插入和删除一个数据的时间复杂度是 O(1)。
链表在删除值等于某个给定值的结点时,需要先找到这个结点,再删除它,所以总的时间复杂度是O(n)。
链表还分为单链表、循环链表、双向链表。双向链表和其他两种链表的区别在于,每个结点还多保存了一个前驱指针指向前面的结点,双向链表寻找前驱节点的时间复杂度为O(1)。
LinkedHashMap 就是通过双向链表和散列表这两种数据结构组合实现的。LinkedHashMap 可以通过设置accessOrder为true并重写removeEldestEntry方法实现 LRU(最近最少使用策略) 缓存淘汰策略。
小结
所以只是简单地说数组查找效率高,链表增删效率高是不严谨的,需要具体情况具体分析。不过业务开发的话,直接一把梭,使用容器类就好了。
ArrayList 和数组的区别
ArrayList可以将数组的很多操作的细节封装起来。比如数组插入、删除数据时需要搬移其他数据等。而且ArrayList还支持动态扩容。数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。
如果我们申请了大小为 10 的数组,当第 11 个数据需要存储到数组中时,我们就需要重新分配一块更大的空间,将原来的数据复制过去,然后再将新数据插入。
如果使用 ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为 1.5 倍大小(JDK 1.8)。
因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,可以在创建 ArrayList 的时候就指定数据大小。
ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而拆箱和装箱有一定的性能消耗。
ArrayList 类和 LinkedList 类的区别
LinkedList 的实现是基于双向循环链表的,且头结点是个哨兵结点,不存放数据。
LinkedList 对返回双向链表中指定位置处的节点的方法进行了优化,源码中先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历,从而提高一定的效率(实际上效率还是很低,具体原因上文中分析过了)。
LinkedList 实现了 Deque 接口,因此也可以作为栈、队列和双端队列来使用。
ArrayList是基于数组实现的List类。会自动的进行扩容,采用Arrays.copyOf()实现。
他们都不是线程安全的。
线程安全的 list 可以用 Vector 和 CopyOnWriteArrayList。