ArrayList与LinkedList增加元素(源码分析)

本文详细比较了ArrayList和LinkedList在添加元素(add, add(int), addAll)时的时间复杂度与执行效率,揭示了ArrayList的扩容机制对性能的影响,以及LinkedList在插入操作中的优势。
摘要由CSDN通过智能技术生成

ArrayList增加元素

首先在主函数中调用ArrayList的无参构造方法生成一个List实例list,并且发现共有三种增加元素的方法:

List<String> list = new ArrayList<>();
list.add("a");
list.add(0, "b");
list.addAll(Arrays.asList("c", "d"));

add(E e)

点进去第一个add方法:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

发现该方法只是通过扩容机制确保容量足够后(ArrayList扩容机制可参考这篇文章:ArrayList初始化及扩容机制),简单地将元素拼接到ArrayList的最后;

add(int index, E element)

点进去第二个add方法:

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

首先使用rangeCheckForAdd(index)来检查index是否合法,然后通过扩容机制来确保有足够的容量,然后使用System.arraycopy来将index及之后的所有元素复制到往后一位(即实现了向后的一位位移),然后将空出来的index处插入新元素即可;
由于涉及到元素的移动,因此该方法的时间复杂度较高;

addAll(Collection<? extends E> c)

点进去addAll方法

/**
 * Appends all of the elements in the specified collection to the end of
 * this list, in the order that they are returned by the
 * specified collection's Iterator.  The behavior of this operation is
 * undefined if the specified collection is modified while the operation
 * is in progress.  (This implies that the behavior of this call is
 * undefined if the specified collection is this list, and this
 * list is nonempty.)
 *
 * @param c collection containing elements to be added to this list
 * @return <tt>true</tt> if this list changed as a result of the call
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

可以看到,首先将集合c转为一个名为aObject[],然后将其长度传给newNum,利用扩容机制确保容量,然后直接将a复制到以下标为size开头的、长度为newNum的位置上,更新size即可;

三种方法运行所需时间测试

我们分别用这三种增加元素的方法来测试一下插入十万条数据需要多长时间:

int max = 100000;
ArrayList<String> list1 = new ArrayList<>();
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
    list1.add("a");
}
long endTime1 = System.currentTimeMillis();
System.out.println("The time needed using add(element) is: "+(endTime1 - startTime1));

ArrayList<String> list2 = new ArrayList<>();
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
    list2.add(0, "a");
}
long endTime2 = System.currentTimeMillis();
System.out.println(("The time needed using add(index, element) is: "+(endTime2 - startTime2)));

ArrayList<String> list3 = new ArrayList<>();
long startTime3 = System.currentTimeMillis();
list3.addAll(list1);
long endTime3 = System.currentTimeMillis();
System.out.println(("The time needed using addAll is: "+(endTime3 - startTime3)));

运行结果如下:

The time needed using add(element) is: 3
The time needed using add(index, element) is: 402
The time needed using addAll is: 0

结论:

  1. 使用add(element)方法需要大概3ms时间,这是因为在插入数据的过程中,仅仅在尾部插入,无需进行数据的复制操作,仅仅需要扩容操作即可;
  2. 使用add(index, element)方法需要的时间高达近400ms,是因为我们在头部插入数据,每次插入都需要将所有数据复制并移动到后一位,再插入数据,还需要不断进行扩容操作;并且随着list长度的增加,使用一次该方法所需的时间越来越长;
  3. 使用addAll方法所需时间最短,接近0ms,这是因为仅在尾部进行了插入数据,无需移动数据,且仅需一次扩容即可;

LinkedList增加元素

首先在主函数中调用LinkedList的无参构造方法生成一个List实例list(若生成LinkedList实例,则有更多关于队列操作的方法,但在这里我们只对比LinkedListArrayListList上的区别),并且发现共有三种增加元素的方法:

List<String> list = new LinkedList<>();
list.add("a");
list.add(0, "b");
list.addAll(Arrays.asList("c", "d"));

add(E e)

点进去第一个add方法:

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

继续点开linkLast

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

发现该方法只是简单地将元素拼接到LinkedList的最后;

add(int index, E element)

点进去第二个add方法:

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

首先使用checkPositionIndex(index)来检查index是否合法,如果index恰好等于size,直接将数据拼接到LinkedList的最后即可,否则使用linkBefore方法:

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

可以看到,在要插入的位置,让新元素的next指向原位于index的元素,让新元素的prev指向位于index之前一个元素,让原位于index的元素的prev指向新元素,若index之前一个元素为null,则新元素为头节点,否则让index之前一个元素的next指向新元素即可;不涉及元素的复制、移位;

addAll(Collection<? extends E> c)

点进去addAll方法:

/**
 * Appends all of the elements in the specified collection to the end of
 * this list, in the order that they are returned by the specified
 * collection's iterator.  The behavior of this operation is undefined if
 * the specified collection is modified while the operation is in
 * progress.  (Note that this will occur if the specified collection is
 * this list, and it's nonempty.)
 *
 * @param c collection containing elements to be added to this list
 * @return {@code true} if this list changed as a result of the call
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

继续点进去:

/**
 * Inserts all of the elements in the specified collection into this
 * list, starting at the specified position.  Shifts the element
 * currently at that position (if any) and any subsequent elements to
 * the right (increases their indices).  The new elements will appear
 * in the list in the order that they are returned by the
 * specified collection's iterator.
 *
 * @param index index at which to insert the first element
 *              from the specified collection
 * @param c collection containing elements to be added to this list
 * @return {@code true} if this list changed as a result of the call
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

可以看到,首先使用checkPositionIndex检查index,也就是size是否合法;然后集合c转为一个名为aObject[],然后将其长度传给newNum,判断是否为0;然后确定要插入的位置,使用for-each循环遍历要插入的元素,挨个插入即可;

三种方法运行所需时间测试

我们分别用这三种增加元素的方法来测试一下插入十万条数据需要多长时间:

int max = 100000;
LinkedList<String> list1 = new LinkedList<>();
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
    list1.add("a");
}
long endTime1 = System.currentTimeMillis();
System.out.println("The time needed using add(element) is: "+(endTime1 - startTime1));

LinkedList<String> list2 = new LinkedList<>();
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
    list2.add(0, "a");
}
long endTime2 = System.currentTimeMillis();
System.out.println(("The time needed using add(index, element) is: "+(endTime2 - startTime2)));

LinkedList<String> list3 = new LinkedList<>();
long startTime3 = System.currentTimeMillis();
list3.addAll(list1);
long endTime3 = System.currentTimeMillis();
System.out.println(("The time needed using addAll is: "+(endTime3 - startTime3)));

运行结果如下:

The time needed using add(element) is: 3
The time needed using add(index, element) is: 3
The time needed using addAll is: 7

结论:

  1. 使用add(element)方法需要大概3ms时间,几乎与ArrayList使用该方法时间一致,这是因为与ArrayList的该方法相近,在插入数据的过程中仅仅在尾部插入即可;
  2. 使用add(index, element)方法需要的时间仅需3ms,远远小于ArrayList的近400ms,是因为我们不需要像ArrayList那样,每次插入都需要将所有数据复制并移动到后一位,再插入数据,还需要不断进行扩容操作,我们只需要在要插入的位置与前后两个元素建立连接即可;
  3. 使用addAll方法所需时间接近7ms,该方法相较于前两个方法,所做判断较多;

结论

  1. ArrayList应尽量避免扩容操作,因为扩容操作需复制一次旧数据,所需时间较长;
  2. ArrayList应尽量避免在非尾部位置插入数据,因为会导致插入点之后的所有数据复制,并向后移位,所需时间非常长;
  3. 对于需要经常在中间新增元素的场景,应尽量使用LinkedList来实现,所需时间复杂度较低;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kuo-Teng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值