java数据结构arraylist和linkedlist

java数据结构arraylist和linkedlist

一,首先看看内存结构图:

二,详细描述:


ArrayList: 可以看作是能够自动增长容量的数组
ArrayList 的toArray方法返回一个数组
ArrayList 的asList方法返回一个列表
ArrayList 底层的实现是Array, 数组扩容实现
ArrayList 线程不安全
ArrayList 必须是连续的
ArrayList 只能在数组末尾添加数据
ArrayList 查询快,增删慢
ArrayList 在初始化的时候指定长度肯定是要比不指定长度的性能好很多,这样不用重复的申请空间, 复制数组, 销毁老的分配空间了

LinkedList可以看做为一个双向链表(内部每个元素都双向指向下一个元素

LinkedList实现了Deque接口和List接口

LinkedList也是线程不安全

LinkedList可以不连续

LinkedList查询慢,增删快

LinkList可以很方便在链表头或者链表尾插入数据,或者在指定结点前后插入数据,add()方法默认在链表尾部插入数据

三,查询快慢的原因:

ArrayList从原理上就是数据结构中的数组,也就是内存中一片连续的空间,这意味着,当我get(index)的时候,我可以根据数组的(首地址+偏移量),直接计算出我想访问的第index个元素在内存中的位置。

LinkedList可以简单理解为数据结构中的链表(说简单理解,因为其实是双向循环链表),在内存中开辟的不是一段连续的空间,而是每个元素有一个[元素|和下一元素地址]这样的内存结构。当get(index)时,只能从首尾元素开始,依次获得下一个元素的地址。

用时间复杂度表示的话,ArrayList的get(n)是o(1),而LinkedList是o(n)。

首先看看ArrayList查询的原理

先看一下ArrayList的get方法源代码:

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

        return elementData(index);
    }

看到ArrayList的get方法只是从数组里面拿一个位置上的元素罢了。我们有结论,ArrayList的get方法的时间复杂度是O(1),O(1)的意思也就是说时间复杂度是一个常数,和数组的大小并没有关系,只要给定数组的位置,直接就能定位到数据。

其实熟悉C、C++或者对指针理解的朋友一定很好理解为什么,我解释一下为什么对数组使用get就快。

在计算机底层,数据都是有地址的,就像人有住址一样。假设我写了这么一句代码:

int[3] ints = {1, 3, 5};

在Java中一个int型数据是4个字节,此时计算机内部做的事情是,在内存空间中找到一块连续的、足以存放3个4字节也就是12字节的数组的内存空间,并返回该内存空间的首地址。比方说该内存空间的首地址是0x00吧,那么那么1就放在0x00~0x03中、3就放在0x04~0x07中、5就放在0x08~0x0B中。如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致

如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致

如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致

这时就很简单了,取ints[1]的时候,计算机就会算出ints[1]的数据是存放在以0x04开头,占据4个字节空间的内存中,因此,计算机会从0x04~0x07这块地址空间中读取数据出来。

整个过程,和数组有多大,并没有关系,计算机做的只是算出起始地址-->去该地址中取数据而已,因此我们看到使用普通for循环遍历ArrayList的速度很快,也很稳定。

再看看linkedList查询的原理

因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致

因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致

因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致

如下图的linkedList元素内存分布,根本无法用get(index)快速存到元素,只能从头到尾一个一地访问

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

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

原因就在第6~第7行,第11~第12行的两个for循里面,以前者为例:

1、get(0),直接拿到0位的Node0的地址,拿到Node0里面的数据

2、get(1),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,拿到Node1里面的数据

3、get(2),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,从1位的Node1中找到下一个2位的Node2的地址,找到Node2,拿到Node2里面的数据。

后面的以此类推。

也就是说,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。假如我有10个数据,那么LinikedList将要查询1+2+3+4+5+5+4+3+2+1=30次数据,相比ArrayList,却只需要查询10次数据就行了,随着LinkedList的容量越大,差距会越拉越大。其实使用LinkedList到底要查询多少次数据,大家应该已经很明白了,来算一下:按照前一半算应该是(1 + 0.5N) * 0.5N / 2,后一半算上即乘以2,应该是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低阶项和首项系数,得出结论,LinikedList遍历的时间复杂度为O(N2)N为LinkedList的容量

时间复杂度有以下经验规则:

O(1) < O(log2N) < O(n) < O(N * log2N) < O(N2) < O(N3) < 2N < 3N < N!

前四个比较好、中间两个一般、后3个很烂。也就是说O(N2)是相对糟糕的一种时间复杂度了,N大一点,程序就会执行得比较慢。

后记

切记一定不要使用普通for循环去遍历LinkedList使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。

四,增删元素快慢的原因:

arraylist 顺序表: 需要申请连续的内存空间保存元素,可以通过内存中的物理位置直接找到元素的逻辑位置。在顺序表中间插入or删除元素需要把该元素之后的所有元素向前or向后移动如果继续添加元素,但是数组已满,没有空闲位置的时候,ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组。这个过程非常复杂耗时!


linkedlist 双向链表: 不需要申请连续的内存空间保存元素,需要通过元素的头尾指针找到前继与后继元素(查找元素的时候需要从头or尾开始遍历整个链表,直到找到目标元素)。在双向链表中插入or删除元素不需要移动元素,只需要改变相关元素的头尾指针即可但是如果在linkedlist中间位置增删元素效率就会慢很多,因为linkedlist需要查询定位到中间位置(这个过程很慢)

五,实测数据对比:

ArrayList尾部插入100000个元素耗时:26ms

LinkedList尾部插入100000个元素耗时:28ms

ArrayList头部插入100000个元素耗时:859ms

LinkedList头部插入100000个元素耗时:15ms

ArrayList中间插入100000个元素耗时:1848ms

LinkedList中间插入100000个元素耗时:15981ms

ArrayList头部读取100000个元素耗时:7ms

LinkedList头部读取100000个元素耗时:11ms

ArrayList尾部读取100000个元素耗时:12ms

LinkedList尾部读取100000个元素耗时:9ms

ArrayList中间读取100000个元素耗时:13ms

LinkedList中间读取100000个元素耗时:11387ms

ArrayList的查找性能绝对是一流的,无论查询的是哪个位置的元素。
ArrayList除了尾部插入的性能较好外(位置越靠后性能越好),其他位置性能就不如人意了。
LinkedList在头尾查找、插入性能都是很棒的,但是在中间位置进行操作的话,性能就差很远了,而且跟ArrayList完全不是一个量级的。

下面是源码分析:

ArrayList尾部插入

add(E e)方法

    public boolean add(E e) {
        // 检查是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 直接在尾部添加元素
        elementData[size++] = e;
        return true;
    }

可以看出,对ArrayList的尾部插入,直接插入即可,无须额外的操作。

LinkedList尾部插入

LinkedList中定义了头尾节点

    /**
     * Pointer to first node.
     */
    transient Node first;/**
     * Pointer to last node.
     */transient Node last;

add(E e)方法,该方法中调用了linkLast(E e)方法

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

linkLast(E e)方法,可以看出,在尾部插入的时候,并不需要从头开始遍历整个链表,因为已经事先保存了尾结点,所以可以直接在尾结点后面插入元素

    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        // 先把原来的尾结点保存下来
        final Node l = last;// 创建一个新的结点,其头结点指向lastfinal Node newNode = new Node<>(l, e, null);// 尾结点置为newNode
        last = newNode;if (l == null)
            first = newNode;else// 修改原先的尾结点的尾结点,使其指向新的尾结点
            l.next = newNode;
        size++;
        modCount++;
    }

总结

对于尾部插入而言,ArrayList与LinkedList的性能几乎是一致的

ArrayList头部插入

add(int index, E element)方法,可以看到通过调用系统的数组复制方法来实现了元素的移动。所以,插入的位置越靠前,需要移动的元素就会越多

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 把原来数组中的index位置开始的元素全部复制到index+1开始的位置(其实就是index后面的元素向后移动一位)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 插入元素
        elementData[index] = element;
        size++;
    }

LinkedList头部插入

add(int index, E element)方法,该方法先判断是否是在尾部插入,如果是调用linkLast()方法,否则调用linkBefore(),那么是否真的就是需要重头开始遍历呢?我们一起来看看

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

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

在头尾以外的位置插入元素当然得找出这个位置在哪里,这里面的node()方法就是关键所在,这个函数的作用就是根据索引查找元素,但是它会先判断index的位置,如果index比size的一半(size >> 1,右移运算,相当于除以2)要小,就从头开始遍历。

否则,从尾部开始遍历。从而可以知道,对于LinkedList来说,操作的元素的位置越往中间靠拢,效率就越低

    Node node(int index) {
        // assert isElementIndex(index);

        if (index > 1)) {
            Node x = first;for (int i = 0; i                 x = x.next;return x;
        } else {
            Node x = last;for (int i = size - 1; i > index; i--)
                x = x.prev;return x;
        }
    }

这个函数的工作就只是负责把元素插入到相应的位置而已,关键的工作在node()方法中已经完成了

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

总结

  • 对于LinkedList来说,头部插入和尾部插入时间复杂度都是O(1),中间插入删除都很慢
  • 但是对于ArrayList来说,头部的每一次插入都需要移动size-1个元素,效率很慢
  • 但是如果都是在最中间的位置插入的话,ArrayList速度比LinkedList的速度快近10倍

ArrayList、LinkedList查找

  • 这就没啥好说的了,对于ArrayList,无论什么位置,都是直接通过索引定位到元素,时间复杂度O(1)
  • 而对于LinkedList查找,其核心方法就是上面所说的node()方法,所以头尾查找速度极快越往中间靠拢效率越低

原文地址

https://blog.csdn.net/weixin_39932330/article/details/111252225

https://www.cnblogs.com/jingpeng77/p/13532248.html

https://blog.csdn.net/Jsir_jsaf/article/details/82315819

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值