zoukankan      html  css  js  c++  java
  • 探索学习LinkedList和ArrayList的差别

    LinkedList & ArrayList

    文章内容有什么问题欢迎联系讨论(请标注原因)
    mail: wgh0807@qq.com
    微信: hello-wgh0807
    qq: 490536401

    前段时间,以前写的基于ES的搜索提示被发现有问题,在老大看代码排查问题的时候,突然扭头问了我一句:“你为什么会用LinkedList?是习惯吗?还是考虑插入和删除效率?”
    这个问题问的我心头一惊,回答是因为考虑插入和删除效率。老大并没有多说什么,只是告诉我以后注意。最后排查到的问题也和这个无关。

    但是这个问题始终在我脑海中盘旋——我为什么会常用LinkedList?

    大学第一个接触的是C,然后是C++,然后才是java和Python。在C/C++日常练习中,最常用的就是数组,C/C++的数组都不可以扩容,所以当学习到指针以后,我仿佛打开了新世界的大门,经常自己写链表去代替比较复杂的数组。
    后来,我接触了java,接触了集合,发现java本身就处于新世界,系统提供可以随意扩容的链表。相对ArrayList需要后台扩容,我更中意于LinkedList,久而久之,就习惯使用LinkedList了。

    那么问题来了,为什么老大会要求少用LinkedList这个我认为的新世界的产物呢?

    以下将以我的角度尝试探索两个集合的差别。

    文档描述对比

    两者在文档中的描述十分相似。都实现了List、Cloneable、Serializable接口,都用size实现技术,都可以容纳包括null在内的所有元素,都实现了所有可选的List操作,都是线程不安全的。

    当然二者也有一定的差异。在ArrayList文档中提到:

    The size, isEmpty, get, set, iterator, and listIterator operations run in constant time. The add operation runs in amortized constant time, that is, adding n elements requires O(n) time. All of the other operations run in linear time (roughly speaking). The constant factor is low compared to that for the LinkedList implementation.

    其中提到,ArrayList的size、isEmpty、get、set、Iterator和ListIterator操作时间复杂度是O(1),增加对象(add)的时间复杂度是O(n),按照下标删除时间复杂度为O(n^2)。

    对于LinkedList而言,add、size、isEmpty、ListIterator方法的时间复杂度是O(1),set、get、根据下标删除方法时间复杂度是O(N)。

    以上算法都没有考虑常数、系数,以ArrayList的下标删除为例,应该为n^2/2

    从这里我们可以看出,LinkedList比ArrayList强大之处在于增加和删除操作(删除操作需要考虑n和待删除下标的值,如果待删除下标较小,LinkedList会比ArrayList强大,但是在中间或者后边的话就有待商榷)。ArrayList比LinkedList强大之处在于set、get方法。

    基本存储结构

    LinkedList的存储结构:

    linkedList存储结构

    每个节点都是双向节点,可以指向前一个节点和后一个节点。黄色代表前序节点对象,蓝绿色代表后续节点对象,红色代表当前节点的存储区域,玫红色 代表变量和常量

    LinkedList在内存中的存储方式:

    一看图感觉简单多了,不就是两个指针中间存放数据嘛,这么一想的话。

    但是,prev和next都是Node<E>类型,而且java也没有指针呀??

    它们在内存应该长成这个样子??

    理想的内存中存储的格式

    实际上这里需要考虑到java的内存存储形式,有栈、堆、常量池。。。

    栈中一般存储局部变量;常量池顾名思义存储常量;堆自然而然就承担了这一个重任。(以下是我对此处存储的个人理解,如果有问题记得按照文章标题联系我)

    目测就是这样。

    ArrayList的存储结构:

    存储数据的方式为数组,可以直接通过下标进行调用。长度不够时需要进行扩容;删除中间值时需要进行整理。

    此处推荐查看两个类的常用方法的底层实现,试了一下做动图太累了。。。

    实验对比

    实验代码

    实验代码可见GIthub,欢迎提出修改意见。 https://github.com/wgh0807/experiment/blob/master/src/test1906/test1.java

    实验环境

    系统平台: mac OSX 10.14.5

    java版本: 1.8.0_131

    实验设计

    本次实验数据量为一百万条整型数据(1-1000000)。实验对象为三个:默认参数的LinkedList、初始化参数为一百万的ArrayList 和 没有参数的ArrayList。

    每个对象分别测试以下功能所需时间:

    1. 创建一百万次对象所需要的时间
    2. 顺序增加一百万条数据所用时间
    3. 随机读取一千条需要消耗的时间
    4. 随机更新一千条数据的信息所用时间
    5. 删除前一千条数据所用时间
    6. 删除最后一千条数据所用时间
    7. 随机删除一千条数据所用时间

    实验结果

    依次为默认参数的LinkedList、参数为1000000(一百万)的ArrayList和默认参数的ArrayList。实验中显示时间的单位为毫秒。实验结果受系统、资源分配等多方面因素影响,实验结果可能波动较大,本次实验只使用同次运行结果进行比对。同次运行结果也可能受到上述因素影响,但影响力有限,本次试验忽略不计。

    根据实验结果我们可以看到:

    1. 创建一百万次对象而言,有/无参数的ArrayList时间消耗极大。
    2. 顺序增加而言,总体上LinkedList表现较为优异。
    3. 随机读取而言,LinkedList时间消耗较大,不明显。
    4. 随机更新而言,LinkedList时间消耗极大
    5. 删除前一千条数据,两项ArrayList时间消耗极大
    6. 删除最后一千条数据,三项结果无明显差异
    7. 随机删除而言,LinkedList时间消耗极大

    结果解释

    1. 查看构造方法。发现LinkedList方法体为空;无参ArrayList构造对内部数组进行了初始化;整型参数ArrayList构造方法对传入参数进行了合法性判断,之后对数组进行初始化。由于ArrayList逻辑较为复杂,所以时间耗费最大。
    2. 查看顺序增加方法(add(E e))。对于LinkedList,add模块的代码量共14行,直接获取了最后一个元素,给其增加下一个节点。而ArrayList的add功能先进行了数组扩容检查,如果需要扩容则进行数组复制,代码量较大。这就是时间花销LinkedList<有参ArrayList< 无参ArrayList 的原因。
    3. 查看其get(int index)方法。LinkedList先判断下标是否存在,然后判断位置距离开始节点近还是结束节点近,之后使用for循环获取目标对象并获取值(item)。ArrayList中,首先判断下标是否存在,存在即以数组的形式访问,节省了很多时间。
    4. 查看set(int index)方法,这个方法的差距和3中相同,主要存在于获取目的节点。LinkedList使用了for循环,ArraysLink使用数组访问方式,ArraysLink在随机访问时(尤其是靠近中间值)效率会明显优于LinkedList。
    5. 删除前一千条数据需要查看remove(int index),在两端删除对于LinkedList影响较小;但是对于ArrayList来说,虽然查找对象较快,但是需要将后续节点向前紧缩,需要新建数组对象并使用copy方法对其赋值,造成了时间消耗极大的结果。
    6. 删除前一千条数据需要查看remove(int index),在两端删除对于LinkedList影响较小;对于ArrayList来说,删除尾部对象也不需要重新构建数组,所以没有明显差距。
    7. 随机删除需要查看remove(int index)方法,LinkedList和ArrayList差距主要还是在于查找对象。找到对象后删除的方法相对而言ArrayList简单一些,所以ArrayList速率会更快一些。

    总结

    时间方面

    LinkedList 优势在于灵活可变,但是随机行为对其影响较大,需要执行for循环。虽然底层已经做了一次折半查找,但是还是比不上直接从数组获取。利用其灵活的特点,在增加、删除(除最后元素外的删除)操作时表现良好,但是对于获取系列操作(尤其是获取1/4和3/4区域数据)时较为缓慢。

    ArrayList优势在于稳定连续,底层使用数组实现使得随机行为得到很好的支持。但是对于破坏其稳定性的行为,如增加、删除操作,对其影响较大。删除操作的影响和其删除元素的位置有关,如果是在最后,则压力较小,只需要置空即可;如果在中间或者前部,则需要使用copy方法,将剩余元素重新组建成为一个新的数组。增加操作则需要判断是否超过现有数组长度,如果超过则需要定义新的较长数组并将当前数组的值拷贝到新的数组中再执行设置值的操作。

    空间方面

    LinkedList结构较大,并且用的变量较多,较为占用空间。

    ArrayList结构无参默认结构简单,空间较小,使用多为常量,占用空间较小

    日常使用

    日常更多的使用ArrayList,性价比更高。但是对于增删操作较为集中的情况中,可以选用LinkedList。同时需要注意,链表删除不一定比数组快,需要根据实际情况进行判断。

    感谢阅读,如文中有任何问题、建议,欢迎进行讨论
    Written By wgh0807

  • 相关阅读:
    Linux Shell 重定向输入和输出
    NDK Cmake
    测试
    20行Python代码爬取王者荣耀全英雄皮肤
    SSH开发模式——Struts2(第一小节)
    JavaWeb开发——软件国际化(动态元素国际化)
    JavaWeb开发——软件国际化(文本元素国际化)
    DBUtils框架的使用(下)
    DBUtils框架的使用(上)
    SSH开发模式——Struts2(第二小节)
  • 原文地址:https://www.cnblogs.com/wgh0807/p/11201739.html
Copyright © 2011-2022 走看看