ArrayList和LinkedList的区别(底层的数据结构、随机访问元素的性能、插入元素和删除元素的性能、内存占用、使用场景)

0. 前言

ArrayList 和 LinkedList 的区别,本质上就是数组和链表的区别,相信学过数据结构的同学都知道数组和链表的区别

如果是细问 ArrayList 和 LinkedList 在某个方面上的区别,大部分同学都能够答出来

但如果问的是 ArrayList 和 LinkedList 的区别,很多同学就答得很差了,可能会东扯一段,西扯一段,甚至还会出现说到某一部分的时候,发现上一部分说得又有点问题,又继续补充上一部分的情况

说到底,之所以回答得不是很好,是因为缺少了归纳总结这一环节,今天带大家一起总结一下 ArrayList 和 LinkedList 的区别

1. 底层的数据结构

  • ArrayList 底层采用数组来存储元素
  • LinkedList 底层采用双向链表来存储元素

2. 随机访问元素的性能

  • ArrayList 在随机访问元素上更高效,因为 ArrayList 可以根据下标来计算元素在数组中的位置
  • LinkedList 在随机访问元素上性能较差,因为 LinkedList 需要从头部或尾部开始遍历链表,找到元素在链表中的位置

我们来测试一下 ArrayList 和 LinkedList 在随机访问元素上的性能差异

创建 ArrayList 和 LinkedList,分别往两个列表中插入 10 万条数据,接着随机访问列表中的元素

在这里插入图片描述

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;

public class ReadTests {

    private static final int SIZE = 100000;

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < SIZE; i++) {
            arrayList.add(i);
        }

        LinkedList<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < SIZE; i++) {
            linkedList.add(i);
        }

        long start = System.currentTimeMillis();
        readArrayList(arrayList);
        long end = System.currentTimeMillis();
        System.out.println("ArrayList read time: " + (end - start) + "ms");

        start = System.currentTimeMillis();
        readLinkedList(linkedList);
        end = System.currentTimeMillis();
        System.out.println("LinkedList read time: " + (end - start) + "ms");
    }

    private static void readArrayList(ArrayList<Integer> arrayList) {
        Random random = new Random();
        for (int i = 0; i < SIZE; i++) {
            arrayList.get(random.nextInt(arrayList.size()));
        }
    }

    private static void readLinkedList(LinkedList<Integer> linkedList) {
        Random random = new Random();
        for (int i = 0; i < SIZE; i++) {
            linkedList.get(random.nextInt(linkedList.size()));
        }
    }

}

测试结果如下(当然,严谨的做法是做多次测试)

在这里插入图片描述

可以看到,ArrayList 随机访问元素的性能是 LinkedList 的几百倍

3. 插入元素和删除元素的性能

  • ArrayList 在数组尾部添加元素或删除元素的性能较好,因为在数组尾部添加元素或删除元素不涉及数组中元素的移动,但是在数组中间或数组头部插入元素或删除元素的时候,ArrayList 会涉及到数组中元素的移动,性能相对较低
  • LinkedList 在任意位置插入元素或删除元素的性能比较好,因为只需要调整链表中指针的指向

4. 内存占用

  • ArrayList 使用数组来存储元素,占用的空间是连续的,可能会产生内存碎片

  • LinkedList 通过链表来存储元素,每个元素都包含前后节点的引用,占用的空间相对较大

5. 使用场景

  • ArrayList 更适合随机访问操作频繁的场景
  • LinkedList 更适合插入和删除操作频繁的场景,尤其是插入和删除操作发生在列表的头部时

6. 频繁地进行插入操作,ArrayList和LinkedList哪个性能更好

按照上面的分析,如果需要频繁地进行插入操作,LinkedList 的性能是更好的,因为进行插入操作时, LinkedList 只需要调整链表中指针的指向,而 ArrayList 进行插入操作时,可能会涉及到数组元素的移动,性能较差

然而,事实真的如此吗,我们来做一个测试

6.1 在列表头部频繁地进行插入操作

在这里插入图片描述

import java.util.ArrayList;
import java.util.LinkedList;

public class HeadInsertTests {

    private static final int SIZE = 100000;

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();

        LinkedList<Integer> linkedList = new LinkedList<>();

        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            arrayList.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("ArrayList head insert time: " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            linkedList.addFirst(i);
        }
        end = System.currentTimeMillis();
        System.out.println("LinkedList head insert time: " + (end - start) + "ms");
    }

}

测试结果如下

在这里插入图片描述

不出意外,在列表头部频繁地进行插入操作,LinkedList 的性能比 ArrayList 高

6.2 在列表尾部频繁地进行插入操作

在这里插入图片描述

import java.util.ArrayList;
import java.util.LinkedList;

public class TailInsertTests {

    private static final int SIZE = 100000;

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();

        LinkedList<Integer> linkedList = new LinkedList<>();

        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            arrayList.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("ArrayList tail insert time: " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            linkedList.addLast(i);
        }
        end = System.currentTimeMillis();
        System.out.println("LinkedList tail insert time: " + (end - start) + "ms");
    }

}

测试结果如下

在这里插入图片描述

可以看到,在列表尾部频繁地进行插入操作,虽然 LinkedList 的性能还是比 ArrayList 高,但是差距已经非常小


我们把数据量提到 100 万,再次进行测试

在这里插入图片描述

可以看到数据量提到 100 万时,在列表尾部频繁地进行插入操作,ArrayList 的性能已经远远高于 LinkedList


但是,我们忽略了一个因素,ArrayList 的括容操作也会造成一定的性能消耗,那如果我们在创建 ArrayList 时就指定数组的大小呢

在这里插入图片描述


测试结果如下

在这里插入图片描述

可以发现 ArrayList 的性能又提升了一点点

如果我们事前能够预料到数据量的范围,可以为 ArrayList 指定一个大小,避免因为扩容而造成不必要的性能开销

6.3 在列表的随机位置频繁地进行插入操作

在这里插入图片描述

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;

public class RandomInsertTests {

    private static final int SIZE = 100000;

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();

        LinkedList<Integer> linkedList = new LinkedList<>();

        Random random = new Random();

        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            int randomNumber = random.nextInt(SIZE);
            int randomIndex = random.nextInt(arrayList.size() + 1);
            arrayList.add(randomIndex, randomNumber);
        }
        long end = System.currentTimeMillis();
        System.out.println("ArrayList random insert time: " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            int randomNumber = random.nextInt(SIZE);
            int randomIndex = random.nextInt(linkedList.size() + 1);
            linkedList.add(randomIndex, randomNumber);
        }
        end = System.currentTimeMillis();
        System.out.println("LinkedList random insert time: " + (end - start) + "ms");
    }

}

测试结果如下

在这里插入图片描述

为什么在列表的随机位置频繁地进行插入操作,ArrayList 的性能比 LinkedList 还要高呢,插入操作不是 LinkedList 的优势吗

光从算法层面来讲

  • 数组随机访问的时间复杂度是 O(1),插入操作的时间复杂度是 O(n)
  • 链表随机访问的时间复杂度是 O(n),插入操作的时间复杂度是 O(1)

但 LinkedList 插入的时间复杂度并不是 O(1),我们看一下 LinkedList 的 add 方法的源码

在这里插入图片描述

在这里插入图片描述

LinkedList 在插入元素前,会花费 O(n) 的时间复杂度去找到要插入的位置

7. 扩展:影响ArrayList和LinkedList性能的因素

事实上,ArrayList 和 LinkedList 在性能上的差别还与内存和缓存有很大的关联

ArrayList 在内存中是紧凑排列的,可以利用空间局部性,这意味着 ArrayList 比 LinkedList 更适合缓存,而 LinkedList 是分布在整个内存上的,不适合缓存


空间局部性的概念(人工智能给出的回答,仅供参考):

在计算机科学中,空间局部性(Spatial Locality)是指当程序访问了某个存储位置或指令时,那么在不久的将来很可能访问与其存储位置相邻的数据或指令。这一概念基于程序执行的统计特性,也是计算机体系结构中缓存设计的重要基础

具体来说,空间局部性体现在以下方面:

  1. 编程中的体现:例如,在循环中访问数组时,由于数组在内存中是连续存储的,程序依次访问数组元素,这就体现了空间局部性。访问 array[i]array[i+1] 时,程序会访问相邻的内存地址
  2. 实际编程开发的影响:理解空间局部性对于编写高性能的程序非常关键。现代处理器利用多级缓存来提高性能,而空间局部性可以显著提高缓存命中率,减少内存访问延迟
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

聂 可 以

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

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

打赏作者

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

抵扣说明:

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

余额充值