细说ArrayList和LinkedList

一、ArrayList

1.底层基本数据结构:Object数组

transient Object[] elementData; 

2.ArrayList初始化容量

由于ArrayList底层是基于数组的,因此在初始化时需要制定数组的初始化容量,ArrayList可以通过构造函数指定初始容量,如果不指定则默认初始容量为10,需要注意的是,当是默认容量的时候,创建一个ArrayList后,不会马上将底层Object数据初始化成10,而是指定成一个空数组。

无参构造器源码如下:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

3.ArrayList的扩容

我们知道使用ArrayList可以不需要考虑其容量限制的往集合中加入数据,其实现原理是每次add数据的时候都会检查list的大小(size)是否超过容量,如果超过容量则会进行扩容操作。其扩容源码如下:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);//右移一位相当于除以2
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到,如果原容量是10,则扩容后的容量为10+10/2=15,在确定了newCapacity后,会将原elementData数组中的数据通过调用Arrays.copyOf方法复制到容量为newCapacity的数组中,并且把新数组的引用赋值给elementData。由于在扩容中需要复制数据,因此如果在数据量大的时候,也是比较耗时的。

4.ArrayList的add(index,elem)插入操作

由于ArrayList底层是基于Object数组来存储数据,因此在往指定下标中插入一个数据时需要先将当前下标之后的数据一次后移一个位置,ArrayList中采用复制的方式,将(index,size-1)区间的数据复制到(index+1,size)区间。

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++;
}

由于该操作需要进行数据复制,因此在数据量大,插入操作频繁的场景中使用ArrayList是比较耗时的。

5.ArrayList的remove(index)删除操作

和插入操作同样的道理,在删除某个index下的数据时,需要将(index+1,size-1)之间的数据向前依次移动一个位置,ArrayList采用复制的方式完成,即将(index+1,size-1)区间的值,复制到(index,size-2)之间,并将size-1下标对应的值设置成null。核心代码如下:

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

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

番外篇:注意到elementData[–size] = null后边的注释‘clear to let GC do its work’,大概意思是说‘明确让GC开展工作’,那为什么将引用设置成null会明确让GC开展工作呢? 这其实是将size-1下标对应的引用设置成null使其不再指向原来的值,则通过根可达性算法(GC Root)可以将size-1下标中的原有值判定为垃圾,然后又垃圾回收器回收。

6.ArrayList是否适合做队列?数组呢?

ArrayList不适合做队列,队列是一种FIFO的数据接口,那么如果使用ArrayList作为队列的话,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。

定长数组是比较适合作为队列的,我们可以使用双标记位来分别标记队头和队尾,如果到达数组边界则可以折回,充分利用存储空间,具体实现可看另一篇文章。

二、LinkedList
1.底层基本数据结构:双向链表

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;
        }
    }

2.LinkedList由于底层是基于双向链表的,因此不需要指定初始化容量,并且不存在扩容机制。

3.LinkedList的add(index,elem)方法

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

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

由于LinkedList中记录了链表中最后一个元素的引用,因此如果是在最后一个元素出插入新数据,则不需要遍历整个链表,则直接可以执行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++;
    }

如果不是最后元素处插入数据,则需要从链表头部开始遍历到指定下标的节点处,我们看一下遍历的源码:

Node<E> node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

整个遍历的设计思路是,如果index<size/2那么则从第一个节点开始向后遍历,如过index>=size/2则从最后一个节点开始向前遍历,这种设计思想在数据量大得情况下可以说是极大的降低了遍历时间
当遍历到指定下标处的节点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++;
    }

4.LinkedList的remove(index)方法
remove方法,删除指定位置上的元素,和add方法一样也同样需要调用node方法来遍历到指定下边处的节点,然后进行链表节点的删除操作。

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;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

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

        x.item = null;
        size--;
        modCount++;
        return element;
    }

三、对比ArrayList和LinkedList
由于ArrayList和LinkedList底层数据结构
由于ArrayList和LinkedList底层数据结构不同,导致了ArrayList和LinkedList在存取速度、使用场景上有所差异,我们通过实现来比较一下ArrayList和LinkedList头插、中间插入、尾部插入的性能。

分别对10 0000,100 0000,1000 0000条数据进行插入操作。

1.头插

public static void main( String[] args ) throws InterruptedException {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        list.add(0, i);
    }
    System.out.println("ArrayList耗时: " + (System.currentTimeMillis() - startTime));

    LinkedList<Integer> list1 = new LinkedList<Integer>();
    long startTime1 = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        list1.addFirst(i);
    }
    System.out.println("LinkedList耗时: " + (System.currentTimeMillis() - startTime1));

}

在这里插入图片描述
如坐标图所示,在头插的场景中ArrayList的性能远远低于LinkedList,这是由于头部插入式ArrayList需要进行大量的数据复制操作,而LinkedList只需要节点的next、prev字段的赋值操作,因此如果你的场景中存在大量的头部插入数据操作,考虑使用LinkedList。
2.尾插

public static void main( String[] args ) throws InterruptedException {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        list.add(i);
    }
    System.out.println("ArrayList耗时: " + (System.currentTimeMillis() - startTime));

    LinkedList<Integer> list1 = new LinkedList<Integer>();
    long startTime1 = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        list1.addLast(i);
    }
    System.out.println("LinkedList耗时: " + (System.currentTimeMillis() - startTime1));

}

在这里插入图片描述
如图所见,在大量的尾部插入操作的场景中,ArrayList的整体性能比LinkedList的性能好,这是因为尾部插入时ArrayList不存在大量数据复制操作,相较而言LinkedList需要大量创建node以及next、prev操作,因此在大量尾部插入操作的场景中更适合使用ArrayList。
3.中间插入

public static void main( String[] args ) throws InterruptedException {
        ArrayList<Integer> list = new ArrayList<Integer>();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            list.add(list.size()>>1,i);
        }
        System.out.println("ArrayList耗时: " + (System.currentTimeMillis() - startTime));

        LinkedList<Integer> list1 = new LinkedList<Integer>();
        long startTime1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            list1.add(list1.size()>>1,i);
        }
        System.out.println("LinkedList耗时: " + (System.currentTimeMillis() - startTime1));

    }

在这里插入图片描述
如图所见,在存在大量中间插入操作的场景中,ArrayList比LinkedList的性能更好一些,这是因为虽然ArrayList需要进行大量的数据复制操作,但是LinkedList也需要进行大量的遍历寻找目标节点,这在数据量比较大的情况下也是非常耗时的。

<think>嗯,用户想比较C#中的ArrayListLinkedList的特点及适用场景。首先,我需要确认C#中这些数据结构的具体实现。不过,C#的ArrayListLinkedList可能与Java中的有所不同,需要先查证。 根据引用内容,Java中的ArrayList是基于动态数组,LinkedList是双向链表。那在C#中,ArrayList应该也是动态数组,而LinkedList可能同样是链表结构。不过C#的List<T>类似于JavaArrayList,而LinkedList<T>是专门的双向链表。可能用户提到的ArrayList是旧的非泛型集合,而通常推荐使用泛型版本List<T>,但这里还是按用户的问题来回答。 接下来,性能对比方面,引用提到ArrayList在随机访问快,插入删除慢;LinkedList则相反。这同样适用于C#的情况。需要说明两者的底层结构差异带来的性能差异,比如数组的连续内存链表的节点链接。 然后,适用场景部分,引用中的信息指出ArrayList适合查询多、增删少的场景,LinkedList适合频繁插入删除。需要结合C#的具体情况,比如在中间操作时LinkedList的性能优势,但头部或尾部可能ArrayList也有不同的表现,比如C#的List<T>在尾部添加效率高,因为动态数组会预留空间。 另外,用户可能混淆了C#中的ArrayListList<T>,因为ArrayList是非泛型的,而List<T>是泛型版本,更常用。需要指出这一点,但根据问题描述,还是以ArrayListLinkedList对比为主。 还要注意C#的LinkedList<T>是否支持索引访问,可能不支持,因此随机访问效率低。需要强调两者的访问方式差异,比如LinkedList需要遍历节点,而ArrayList可以直接通过索引。 最后,需要总结两者的优缺点,并给出使用场景建议,比如处理大量中间插入删除用LinkedList,而需要快速随机访问用ArrayList或List<T>。同时提醒用户考虑线程安全等因素,可能推荐使用泛型集合。 可能需要检查C#文档确认具体细节,比如动态数组的扩容机制,LinkedList的方法等,确保回答准确。同时,引用中的资料虽然来自Java,但数据结构原理相通,可以类比,但要指出C#中的实现差异。</think>### C# ArrayListLinkedList 特点及性能对比 #### 1. **底层结构差异** - **`ArrayList`**(非泛型动态数组): - 底层基于动态数组实现,内存连续分配,支持快速随机访问(时间复杂度 $O(1)$)。 - 插入/删除元素时,若涉及中间位置操作,需要移动后续元素,时间复杂度为 $O(n)$[^1][^4]。 - 扩容时需重新分配内存并复制数据,可能影响性能[^3]。 - 示例代码: ```csharp ArrayList list = new ArrayList(); list.Add(10); // 动态扩容 int value = (int)list[0]; // 直接索引访问 ``` - **`LinkedList<T>`**(泛型双向链表): - 底层基于双向链表,内存非连续分配,每个节点包含前驱后继指针。 - 插入/删除元素时,仅需修改相邻节点指针,时间复杂度为 $O(1)$(需先定位到操作位置)。 - 随机访问需从头/尾遍历链表,时间复杂度为 $O(n)$[^5]。 --- #### 2. **性能对比** | **操作** | **`ArrayList`** | **`LinkedList<T>`** | |-------------------|---------------------|---------------------| | 随机访问(索引) | $O(1)$ | $O(n)$ | | 头部插入/删除 | $O(n)$ | $O(1)$ | | 尾部插入/删除 | $O(1)$(均摊) | $O(1)$ | | 中间插入/删除 | $O(n)$ | $O(n)$(需遍历定位)| | 内存占用 | 连续内存,无指针开销 | 分散内存,每个节点含指针 | --- #### 3. **适用场景** - **优先选择 `ArrayList`/`List<T>`**: - 需要频繁随机访问元素(如按索引查询)。 - 尾部插入/删除操作较多(动态数组优化了尾部操作)。 - 示例场景:数据缓存、批量数据处理。 - **优先选择 `LinkedList<T>`**: - 频繁在头部或中间插入/删除元素(如实现队列、栈或双向链表结构)。 - 不需要按索引访问,而是通过遍历操作(如实现 LRU 缓存淘汰算法)[^5]。 - 示例场景:实时日志记录、撤销操作历史记录。 --- #### 4. **实际测试结论** - **查询性能**:`ArrayList` 的索引访问速度显著优于 `LinkedList<T>`。 - **插入/删除性能**: - 在头部操作时,`LinkedList<T>` 性能优势明显(无需移动元素)[^5]。 - 在中间操作时,若已定位到节点,`LinkedList<T>` 效率更高;否则需额外遍历时间。 --- #### 5. **总结建议** - 若需**泛型支持**,优先使用 `List<T>`(C# 中更现代的替代方案,类型安全且性能优于 `ArrayList`)。 - 若需**频繁增删且无需索引访问**,选择 `LinkedList<T>`。 - 若需**高性能随机访问或尾部操作**,选择 `List<T>` 或 `ArrayList`(非泛型场景)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

即刻实现工作室

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

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

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

打赏作者

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

抵扣说明:

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

余额充值