Java 面试高频题:ArrayList 和LinkedList有啥区别

一句话概括区别

1、ArrayList

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问
数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数
组的数据复制到新的存储空间中。
当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

2、LinkedList

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除随机访问和遍历速度比较

另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆
栈、队列和双向队列使用。

详细分析

1、继承关系图

继承关系图

ArrayList 和LinkedList都实现了List接口,LinkedList还实现了Queue队列接口,队列是一种特殊的线性表。Queue:一个队列就是一个先进先出(FIFO)的数据结构。

2、数据结构上的对比

  • ArrayList 基于动态数据的数据结构,数组是连续的内存存储,下标从0开始,查找快速,插入慢。定义数组时,数组长度已定,查找某个元素,直接访问所在下标即可,插入元素的话,就要把所插入位置后的元素都向后移动。删除原理一样,也有元素移动。

构造函数:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

我们来看一下数组的数据结构
在数组元素2前插入一个元素

  • LinkedList 是一个双向链表、链表插入元素时,只需要对指针进行修改即可,而数组要移动数据来填补被删除的对象的空间。

我们来看一下双向链表的数据结构

双向链表插入

3、性能分析。

源码分析(增删)

  • 对于随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处
  • 对于新增和删除操作add和remove,LinkedList比较占优势,只需要对指针进行修改即可,而ArrayList要移动数据来填补被删除的对象的空间

ArrayList

(1)数组末尾插入元素,ArrayList只需要容量加一,在数组最后插入元素即可

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 容量+1
    elementData[size++] = e;
    return true;
}

(2)数组中插入元素,ArrayList容量加一,复制到一个新数组,在插入位置插入元素。

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // 容量+1
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);//复制到一个新的数组
    elementData[index] = element;
    size++;
}

(3)删除
如果删除的元素在数组中,和插入元素一样,需要复制到一个新的数组,如果删除的元素在数组末尾,不执行复制到新数组的代码。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);// 准备删除的元素

    int numMoved = size - index - 1;//开始移动的位置,
    //如果删除的元素在队尾,不执行if分支
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);//复制到一个新数组,不包括删除的数组
    elementData[--size] = null; // 清空最后一位,告诉GC回收

    return oldValue;//返回删除的元素
}

LinkedList

Node节点一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。每个结点都有一个前驱和后继结点,并且在 LinkedList中也定义了两个变量分别指向链表中的第一个和最后一个结点。

    /**
     * Pointer to first node.指向第一个节点
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.指向最后一个节点
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
    ...
    private static class Node<E> {
        E item;//节点值
        Node<E> next;//后一个节点
        Node<E> prev;//前一个节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

(1)插入新元素

public void add(E e) {
    checkForComodification();
    lastReturned = null;
    if (next == null)
        linkLast(e);//增加到队尾
    else
        linkBefore(e, next);//增加到队中
    nextIndex++;
    expectedModCount++;
}

队尾
定义节点的前驱后继,链表不为空的情况下,直接把最后一个节点的next指向新节点

void linkLast(E e) {
    final Node<E> l = last;//最后一个节点
    // 参考下面Node节点,定义时,已经把前后节点定义好。
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;//新节点赋值给队尾
   // 链表为null的情况下,赋值给第一个节点,否则赋值给下一个节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
}

队中
定义节点的前驱后继,在链表不为空的情况下,把插入位置的前一个节点的next指向新节点。

void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;//succ的前节点
    // 参考下面Node节点,定义时,已经把前后节点定义好。
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;//新节点赋值给succ的前节点
    // succ处于第一个节点,把新节点赋给第一节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;//succ不是第一个节点,把succ的next指向新节点
    size++;
    modCount++;
}

(2)删除

目标元素为头节点,将链表第一个节点指向删除的元素的后继;
尾节点,将链表最后一个节点前驱指向删除的元素的前驱;
中间节点,前一元素的后继指向目标元素的后继,将前一元素的前驱指向目标元素的前驱。

   public boolean remove(Object o) {
        if (o == null) {
       // 遍历目标节点
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
        // 遍历目标节点
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
   /**
     * Unlinks non-null node x.  不为null的节点
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;//删除的目标元素
        final Node<E> next = x.next;//后继
        final Node<E> prev = x.prev;//前驱
        // 前驱为nulll,说明为头节点,将链表第一个节点指向删除的元素的后继
        // 不是头节点,将前一元素的后继指向目标元素的后继
        // 目标元素的前驱置null
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        
        // 后继为nulll,说明为尾节点,将链表最后一个节点前驱指向删除的元素的前驱
        // 不是尾节点,将前一元素的前驱指向目标元素的前驱
        // 目标元素的后继置null
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
       // 目标元素置null,链表长度减1,modCount自加
        x.item = null;
        size--;
        modCount++;
        return element;
    }

时间复杂度

操作数组链表
随机访问O(1)O(N)
头部插入O(N)O(1)
尾部插入O(N)O(1)
头部删除O(1)O(1)
尾部删除O(1)O(1)

一句话总结

查询用ArrayList,插入删除用LinkedList。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值