如果再有人给你说 “ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”,你可以让他滚了!

    这是一个极其不负责任的总结,关键是你会在很多地方看到这样的结论。

    害,我一开始学 Java 的时候,也问过一个大佬,“ArrayList 和 LinkedList 有什么区别?”他就把“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”甩给我了,当时觉得,大佬好牛逼啊!

    后来我研究了 ArrayList 和 LinkedList 的源码,发现还真的是,前者是数组,后者是 LinkedList,于是我对大佬更加佩服了!

    直到后来,我亲自跑程序验证了一遍,才发现大佬的结论太草率了!根本就不是这么回事!

    先来给大家普及一个概念——。

    增删改查,对应到 ArrayList 和 LinkedList,就是 add(E e)、remove(int index)、add(int index, E element)、get(int index),我来给大家一一分析下,它们对应的时间复杂度,也就明白了“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”这个结论很荒唐的原因

    对于 ArrayList 来说

    1) 方法的时间复杂度为 $O(1)$,因为是直接从底层数组根据下标获取的,和数组长度无关。

    这也是 ArrayList 的最大优点。

    2)add(E e) 方法会默认将元素添加到数组末尾,但需要考虑到数组扩容的情况,如果不需要扩容,时间复杂度为 $O(1)$。

    1. public boolean add(E e) {
    2. modCount++;
    3. add(e, elementData, size);
    4. return true;
    5. }
    6. private void add(E e, Object[] elementData, int s) {
    7. if (s == elementData.length)
    8. elementData = grow();
    9. elementData[s] = e;
    10. size = s + 1;
    11. }
    1. private Object[] grow(int minCapacity) {
    2. int oldCapacity = elementData.length;
    3. if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    4. int newCapacity = ArraysSupport.newLength(oldCapacity,
    5. minCapacity - oldCapacity, /* minimum growth */
    6. oldCapacity >> 1 /* preferred growth */);
    7. return elementData = Arrays.copyOf(elementData, newCapacity);
    8. } else {
    9. return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    10. }
    11. }

    3)add(int index, E element) 方法将新的元素插入到指定的位置,考虑到需要复制底层数组(根据之前的判断,扩容的话,数组可能要复制一次),根据最坏的打算(不管需要不需要扩容,System.arraycopy() 肯定要执行),所以时间复杂度为 $O(n)$。

    1. public void add(int index, E element) {
    2. rangeCheckForAdd(index);
    3. modCount++;
    4. final int s;
    5. Object[] elementData;
    6. if ((s = size) == (elementData = this.elementData).length)
    7. elementData = grow();
    8. System.arraycopy(elementData, index,
    9. elementData, index + 1,
    10. s - index);
    11. elementData[index] = element;
    12. size = s + 1;
    13. }

    来执行以下代码,把沉默王八插入到下标为 2 的位置上。

    System.arraycopy() 执行完成后,下标为 2 的元素为沉默王四,这一点需要注意。也就是说,在数组中插入元素的时候,会把插入位置以后的元素依次往后复制,所以下标为 2 和下标为 3 的元素都为沉默王四。

    之后再通过 elementData[index] = element 将下标为 2 的元素赋值为沉默王八;随后执行 size = s + 1,数组的长度变为 7。

    ArrayList 重拳出击,把 LinkedList 干翻在地 - 图2

    4)remove(int index) 方法将指定位置上的元素删除,考虑到需要复制底层数组,所以时间复杂度为 $O(n)$。

    1. public E remove(int index) {
    2. Objects.checkIndex(index, size);
    3. final Object[] es = elementData;
    4. @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    5. fastRemove(es, index);
    6. return oldValue;
    7. }
    8. private void fastRemove(Object[] es, int i) {
    9. modCount++;
    10. final int newSize;
    11. if ((newSize = size - 1) > i)
    12. }

    对于 LinkedList 来说

    1)get(int index) 方法的时间复杂度为 $O(n)$,因为需要循环遍历整个链表。

    1. public E get(int index) {
    2. checkElementIndex(index);
    3. return node(index).item;
    4. }
    5. LinkedList.Node<E> node(int index) {
    6. // assert isElementIndex(index);
    7. if (index < (size >> 1)) {
    8. LinkedList.Node<E> x = first;
    9. for (int i = 0; i < index; i++)
    10. x = x.next;
    11. return x;
    12. } else {
    13. LinkedList.Node<E> x = last;
    14. for (int i = size - 1; i > index; i--)
    15. x = x.prev;
    16. return x;
    17. }
    18. }

    下标小于链表长度的一半时,从前往后遍历;否则从后往前遍历,这样从理论上说,就节省了一半的时间。

    如果下标为 0 或者 list.size() - 1 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 getFirst()getLast() 方法。

    1. public E getFirst() {
    2. final LinkedList.Node<E> f = first;
    3. if (f == null)
    4. throw new NoSuchElementException();
    5. return f.item;
    6. }
    7. public E getLast() {
    8. final LinkedList.Node<E> l = last;
    9. if (l == null)
    10. throw new NoSuchElementException();
    11. return l.item;
    12. }

    first 和 last 在链表中是直接存储的,所以时间复杂度为 $O(1)$。

    2)add(E e) 方法默认将元素添加到链表末尾,所以时间复杂度为 $O(1)$。

    1. public void add(int index, E element) {
    2. checkPositionIndex(index);
    3. if (index == size)
    4. linkLast(element);
    5. else
    6. linkBefore(element, node(index));
    7. }

    如果下标为 0 或者 list.size() - 1 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 addFirst()addLast() 方法。

    1. public void addFirst(E e) {
    2. linkFirst(e);
    3. }
    4. private void linkFirst(E e) {
    5. final LinkedList.Node<E> f = first;
    6. final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
    7. if (f == null)
    8. else
    9. f.prev = newNode;
    10. size++;
    11. modCount++;
    12. }

    linkFirst() 只需要对 first 进行更新即可。

    1. public void addLast(E e) {
    2. linkLast(e);
    3. }
    4. void linkLast(E e) {
    5. final LinkedList.Node<E> l = last;
    6. final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
    7. last = newNode;
    8. if (l == null)
    9. first = newNode;
    10. else
    11. l.next = newNode;
    12. size++;
    13. modCount++;
    14. }

    linkLast() 只需要对 last 进行更新即可。

    需要注意的是,有些文章里面说,LinkedList 插入元素的时间复杂度近似 $O(1)$,其实是有问题的,因为 add(int index, E element) 方法在插入元素的时候会调用 node(index) 查找元素,该方法之前我们之间已经确认过了,时间复杂度为 $O(n)$,即便随后调用 linkBefore() 方法进行插入的时间复杂度为 $O(1)$,总体上的时间复杂度仍然为 $O(n)$ 才对。

    4)remove(int index) 方法将指定位置上的元素删除,考虑到需要调用 node(index) 方法查找元素,所以时间复杂度为 $O(n)$。

    1. public E remove(int index) {
    2. checkElementIndex(index);
    3. return unlink(node(index));
    4. }
    5. E unlink(LinkedList.Node<E> x) {
    6. // assert x != null;
    7. final E element = x.item;
    8. final LinkedList.Node<E> next = x.next;
    9. final LinkedList.Node<E> prev = x.prev;
    10. if (prev == null) {
    11. first = next;
    12. } else {
    13. prev.next = next;
    14. x.prev = null;
    15. }
    16. if (next == null) {
    17. last = prev;
    18. } else {
    19. next.prev = prev;
    20. x.next = null;
    21. }
    22. x.item = null;
    23. size--;
    24. modCount++;
    25. return element;

    通过时间复杂度的比较,以及源码的分析,我相信大家在选择的时候就有了主意,对吧?

    需要注意的是,如果列表很大很大,ArrayList 和 LinkedList 在内存的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。

    查询的时候,ArrayList 比 LinkedList 快,这是毋庸置疑的;插入和删除的时候,LinkedList 因为要遍历列表,所以并不比 ArrayList 更快。反而 ArrayList 更轻量级,不需要在每个元素上维护上一个和下一个元素的地址。

    但是,请注意,如果 ArrayList 在增删改的时候涉及到大量的数组复制,效率就另当别论了,因为这个过程相当的耗时。

    对于初学者来说,一般不会涉及到百万级别的数据操作,如果真的不知道该用 ArrayList 还是 LinkedList,就无脑选择 ArrayList 吧!


    这是《Java 程序员进阶之路》专栏的第 60 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。

    这么好的东西,还不 star 下?