数组与链表

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

除了HashMap,ArrayList和LinkedList应该是使用最频繁的容器,类似的总结如下:

这两句话本身是没错的,但是要看场景。在不了解LinkedList和ArrayList原理的情况下,看到增删操作多就用LinkedList反而会使程序执行效率下降。相对来说,下面那句“什么都不知道,就用ArrayList”反而更适合初学者。

大家之前已经学习过《山寨Stream API》,有没有人感到疑惑,filter()明显属于“增删多”的操作,为什么不用LinkedList?

先卖个关子,后面解释。

要想在合适的场景使用合适的List,必须对它们底层数据结构有所了解。所以接下来我们一起学习数据结构中的两种线性结构:链表、数组。

链表的遍历

链表的存储空间不是连续的,每个节点会“记住”下一个节点的地址。这样的好处是计算机在为链表结构的容器分配内存空间时可以“见缝插针”,从而更加有效地利用内存。

但相比数组结构来说,由于内存分配是不连续的,上一个节点要找它的下一个节点时,需要根据地址去找。这就导致了链表结构的查询比数组结构要慢。

大家不要看上面的图好像是连续的,实际上可能是这样:

在Java中,LinkedList底层正是链表结构。这里考考大家,链表的遍历对应LinkedList的哪个方法呢?

你以为这叫链表的遍历?

LinkedList<String> list = new LinkedList<>();
for (int i = 0; i < list.size(); i++) {// do something... for example: list.get(i)
}

养兔子的大叔:其实上面包含了两层遍历。第一层是外面的for,而list.get(i)本身才是对链表的遍历,get(i)底层会按地址遍历找到第i个元素。

记住这个概念,后面在比较ArrayList和LinkedList的效率时它会出来“捣乱”。

LinkedList#getFirst() (很快)

LinkedList#getLast()(很快)

LinkedList内部会维护头尾节点,所以getFirst()、getLast()都是很快的。

LinkedList#get(index)(较慢,因为要通过node(index)方法遍历到i元素再取出,而链表是不连续的)

链表的插入与删除

链表的插入操作非常方便,只要解开A和B节点的联系,再让它们同时跟C节点重新建立联系即可:

删除同样方便:

但是,大家想一个问题:我想在第i个元素和第i+1个元素之间插入一个新元素。你觉得这个需求包含几个操作?

  • 先遍历找到第i个元素(遍历)
  • 把第i个元素和第i+1个元素的联系拆开,各自和新元素建立联系(插入)

所以,虽然我们分析问题时都是强调链表结构插入和删除比数组结构快,但理想化的链表节点插入和删除是不存在的,任何基于线性结构的容器,插入和删除的实现必然伴随着遍历。虽然链表对于某个节点的插入和删除确实比数组快,但是遍历相对较吃力,所以实际增删的效率并不能一概而论。

LinkedList#addFirst(e)/addLast(e)(很快)

LinkedLis#add(e)(很快,默认从链表尾部插入,此方法与addLast()等效)

LinkedList#set(i, e)(较慢,先遍历,后替换指定位置的元素为新元素)

LinkedList#add(i, e)(较慢,先遍历,后插入)

LinkedList#removeFirst()/removeLast()(很快)

LinkedList#remove()(很快,内部调用removeFirst())

LinkedList#remove(e)/remove(i)(较慢,内部会遍历)

LinkedList小结

  • 查询
    • 尽量使用getFirst()/getLast(),很快,因为内部维护了头尾节点
    • 避免使用get(index),内部包含遍历,较慢
  • 头尾插入
    • 尽量使用addFirst(e)/addLast(e)/add(e),都是对头尾节点的操作,很快
  • 中间插入/替换
    • 避免使用set(i, e)和add(i, e),内部需要先遍历再插入/替换
  • 删除
    • 尽量使用removeFirst()/remove()/removeLast(),都是对头尾节点的操作,很快
    • 避免使用remove(i)/remove(e),内部包含遍历,较慢

一句话:LinkedList不擅长遍历,但维护了头尾节点,尽量使用带有First/Last的方法,避免使用带索引的方法(带索引意味着需要遍历到该位置)。

数组的遍历

由于数组在结构上要求连续,所以计算机会为它分配连续的一片空间。遍历时不关心具体元素的地址,只要知道起始元素的地址以及目标元素的下标,即可快速找到目标元素。比如,一排房子,我只要知道第一户人家的地址,以及你家在第一户人家从左往右的第几家,那么我找到第一户人家后,往右数第N-1家就找到你家了。所以数组结构的遍历会优于链表结构的遍历,它不需要频繁寻找地址。

ArrayList#get(i):很快

两个细节:

  • rangeCheck()与我们最常见的IndexOutOfBoundsException有关
  • elementData(index)直接根据数组下标找到元素,由于存储连续,相比LinkedList的遍历要快很多!

数组的插入与删除

要讨论数组的插入和删除,总是离不开数组的拷贝和扩容。

比如我们使用数组时都是这样声明的:

int[] intArr = new int[5];

表示申请长度为5的数组,这意味着数组的长度是固定的。

现在我把数组都填满:

然后让我们考虑两种情况:

  • 再插入新元素
  • 删除元素

先说插入。在Java中,Array和ArrayList都是数组结构的。Array如果满了,就不能再插入了,否则就会抛“越界异常”。而ArrayList被称为“动态数组”,原因就在于它会自动扩容。

扩容的具体步骤是:

  • ArrayList申请新的长度的数组
  • 把原数组的元素拷贝到新数组
  • 把新元素插入到新数组

拷贝数组是一件比较耗时的操作,我不知道计算机底层会不会根据实际情况做优化:

在操作系统层面数组也仅仅是页内保证连续,所以具体有没有以上优化不清楚,仅作为讨论。总之,从ArrayList源码来看,扩容必然伴随元素拷贝,而拷贝是耗时的。大家只需知道这个即可。

而链表其实不存在所谓的长度限制,只需要把新的元素指向原链表的某个(对)元素即可,不涉及拷贝。

大致介绍数组结构的插入后,我们看看ArrayList相关的插入方法。

  • ArrayList#set(index, element):只是替换,不会扩容和拷贝
  • ArrayList#add(e):尾部插入,只有当数组满了才扩容
  • ArrayList#add(index, element):指定位置插入,不一定扩容,但会触发数组拷贝,尽量避免使用

强调几点:

  • 数组的元素替换速度比链表的替换快!首先,数组查询比链表快。其次,数组元素直接赋值覆盖完成替换,而链表要先解开地址引用
  • add(int index, E element)不一定会触发扩容,但几乎一定会发生拷贝
  • 数组的中间插入只会移动部分元素,头插入会移动所有元素。大家看看上面的System.arracopy(),当我们尾部插入时,index=size,所以length参数就是0,无需移动任何元素

接下来讨论数组的删除。

如果有人告诉你,数组的删除同样可能需要拷贝元素,你会不会很诧异?元素太多存不下,所以要扩容并拷贝元素,这很好理解,但是为什么删除也要拷贝元素??

这和数组的定义有关:空间连续。

你以为数组删除是这样的:

但是注意,new int[5]其实数组是有默认值0的。你根本无法把数组某个元素设为null,默认值就是0。你想清除原有元素可千万别用 arr[i] = 0,那样别人会以为arr[i]的值就是0。所以,我们这里讨论的删除是确确实实的把数组“截短”。由于数组要保证空间连续,所以会重新拷贝元素,把两边的数据合并:

ArrayList#remove(index)/remove(element):极有可能拷贝数组,除非尾部删除

ArrayList小结

  • 查询
    • 随便用,只有一个get(index),根据下标查询,很快
  • 插入
    • 尽量使用add(element),避免使用add(index, element),中间插入一定触发数组拷贝,较大概率触发扩容,扩容和拷贝不一定同时进行。是否取决于元素数量,而是否拷贝取决于本次插入位置,尾部插入无需拷贝
  • 替换
    • set(index, element),很快
  • 删除
    • 推荐循环删除时使用逆序遍历,这样可以从尾部删除,不会触发数组拷贝,禁止从头部删除

讲完了理论,接下来让我们写点代码验证下。

LinkedList VS ArrayList

纯粹的增删改是不存在的,必然伴随着遍历,这也是实际开发的常态,所以demo都会按照实际开发的习惯编写。

时间仅供对位比较,不要错位比较。比如,不要把查询和插入的时间拿来比,因为我有时查询里会打印数据,且查询只查了1w条,而插入可能是100w条。

查询比较

直接跑demo

@Test
public void testForEachInLinkedList() {// 准备10000条数据,不要问我为啥用String.valueOf(),当初不小心这样写的,不改了List<String> list = new LinkedList<>();for (int i = 0; i < 10000; i++) {list.add(String.valueOf(i));}long start = System.currentTimeMillis();// 测试普通for的查询效率for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));}System.out.println("普通for耗时:"+(System.currentTimeMillis()-start));// 推荐增强for,内部有优化
//    for (String s : list) {
//        System.out.println(s);
//    }
//    System.out.println("增强for耗时:"+(System.currentTimeMillis()-start));
}

LinkedList要靠地址找到下一个节点,速度较慢,而LinkedList#get(i)操作会触发内部的遍历,应该尽量避免使用。所以,对于LinkedList而言,无特殊情况都推荐使用增强for。

ArrayList使用增强for和普通for虽然差距不大,但还是建议使用增强for,除非你需要用到index。

@Test
public void testForEachInArrayList() {List<String> list = new ArrayList<>();// 插入10000条数据for (int i = 0; i < 10000; i++) {list.add(String.valueOf(i));}long start = System.currentTimeMillis();// ArrayList推荐使用普通for
//    for (int i = 0; i < list.size(); i++) {
//        System.out.println(list.get(i));
//    }
//    System.out.println("普通for耗时:"+(System.currentTimeMillis()-start));// 增强forfor (String s : list) {System.out.println(s);}System.out.println("增强for耗时:"+(System.currentTimeMillis()-start));
}

LinkedList 增强for多次查询结果:

87 109 114 82 85...

ArrayList 增强for多次查询结果:

74 142 118 78 90...

结论:都用增强for差不多

LinkedList使用普通for+get(i)会很慢,但使用增强for后得到显著提升,ArrayList普通for和增强for差不多。

整体来说,都用增强for的情况下,ArrayList和LinkedList查询效率差不多。

我的数据量太少了,大家自己测

插入比较

尾部插入:ArrayList胜

经过多次比较,得出一个意想不到的结果:ArrayList尾部插入效率高于LinkedList。我猜测,ArrayList本身只有数组满了才扩容,且由于是尾部插入不涉及数组拷贝,所以相对较快。而LinkedList由于插入时需要解绑元素并重新绑定新元素,效率反而低了(虽然是对尾结点操作)。

对结果有疑问的同学可以自己测一下:

@Test
public void testList() {List<String> list = new LinkedList<>();long start = System.currentTimeMillis();// 插入10000条数据for (int i = 0; i < 1000000; i++) {list.add(String.valueOf(i));}System.out.println(System.currentTimeMillis() - start);
}

头部插入:LinkedList胜

LinkedList头插入和尾插入是一样的:

ArrayList头插入效率极低,但是我相信没有人会故意头插入,毕竟我设计头插入这个案例都愣了几秒钟,才发现可以add(0, element)实现头插入。即使真的需要反过来,那么只要遍历时倒序遍历即可:

@Test
public void testList() {ArrayList<String> list = new ArrayList<>();long start = System.currentTimeMillis();// 头插入10000条数据for (int i = 0; i < 1000000; i++) {list.add(0, String.valueOf(i));}System.out.println(System.currentTimeMillis() - start);
}

随机位置插入:ArrayList胜

原因在于,对于每次随机,add(i, e)内部都要先遍历...所以即使数组底层需要拷贝扩容,无奈LinkedList的遍历实在太慢!

删除比较

尾部删除:ArrayList胜

因为ArrayList尾部删除既不会触发扩容,也无需拷贝,所以速度很快。

头部删除:LinkedList胜

和ArrayList的头插入相似,头删除会触发数组拷贝,但不扩容。

随机删除:ArrayList胜

还是那句话,LinkedList的删除优势比不过遍历劣势。

替换比较

顺序替换:ArrayList胜

set(i)需要内部遍历,这使得LinkedList效率不如ArrayList

随机替换:ArrayList胜

不测了

总结

理应来说LinkedList作为链表结构,插入删除操作应该比ArrayList效率高,但在遍历的大前提下,LinkedList只要涉及索引操作(index),由于get(i)/set(i, e)等方法内部需要遍历,最终表现往往不如ArrayList。

最后的结论就是,除非你要用list进行头插入或头删除,否则都是ArrayList快。但你觉得这种情况多吗?是什么需求这么变态呀?所以我在构建山寨Stream API时没有考虑LinkedList,也推荐大家平时不知道用哪种List时,优先选择ArrayList。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/173035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机服务器中了mallox勒索病毒如何处理,mallox勒索病毒解密文件恢复

科技技术的发展推动了企业的生产运营&#xff0c;网络技术的不断应用&#xff0c;极大地方便了企业日常生产生活&#xff0c;但网络毕竟是一把双刃剑&#xff0c;网络安全威胁一直存在&#xff0c;近期&#xff0c;云天数据恢复中心接到很多企业的求助&#xff0c;企业的计算机…

耶鲁博弈论笔记

编辑记录&#xff1a; 1126&#xff1a;开个新坑&#xff0c;耶鲁大学的博弈论课程&#xff0c; 和专业相关不大&#xff0c;纯兴趣&#xff0c;尽量写好一点吧 1. 首先指出博弈论是一种研究策略形式的方法&#xff0c;对于经济学中&#xff0c;完全竞争市场只能被动接受均衡…

Elasticsearch 的使用

一、简介 1.Shard&#xff08;分片&#xff09; 数据分散集群的架构模式&#xff0c;Elasticsearch 将一个 Index&#xff08;索引&#xff09;中的数据切为多个 Shard&#xff08;分片&#xff09;&#xff0c;分布在不同服务器节点上。 默认每个索引会分配5个主分片和1个副本…

C#的函数

可以发现首字母一般为动词 可以看出void是返回空值 这个就不是放回空值了 例如 函数修饰符 1、如上面的实例一样——无修饰符&#xff1a;如果一个参数没有用参数修饰符标记&#xff0c;则认为它将按值进行传递&#xff0c;这将以为着被调用的方法收到原始数据的一份副本。&am…

谈谈Redis的几种经典集群模式

目录 前言 主从复制 哨兵模式 分片集群 前言 Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许Redis在不同节点上同时提供服务&#xff0c;提高整体性能和可靠性。在Redis中提供集群方案总共有三种&#xff1a;主从复制、…

PyQt6把QTDesigner生成的UI文件转成python源码,并运行

锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计18条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话版…

CSDN动态发了但是主页面看不见已发的动态

问题描述&#xff1a; 今天在写csdn动态的时候&#xff0c;发了五个动态&#xff0c;但是主页面的“最近”看不到我发的动态&#xff0c;我还以为是csdn动态每天的发送量有数量限制。去这个地方点我的发现 右上角全是“审核中”的字样 按理说是不可能审核这么久的&#xff08…

Linux常用命令——bc命令

在线Linux命令查询工具 bc 算术操作精密运算工具 补充说明 bc命令是一种支持任意精度的交互执行的计算器语言。bash内置了对整数四则运算的支持&#xff0c;但是并不支持浮点运算&#xff0c;而bc命令可以很方便的进行浮点运算&#xff0c;当然整数运算也不再话下。 语法 …

IO和NIO的区别 BIO,NIO,AIO 有什么区别? Files的常用方法都有哪些?

文章目录 IO和NIO的区别BIO,NIO,AIO 有什么区别?Files的常用方法都有哪些&#xff1f; 今天来对java中的io, nio, bio, aio进行了解&#xff0c;有何区别。 IO和NIO的区别 NIO与IO区别 IO是面向流的&#xff0c;NIO是面向缓冲区的Java IO面向流意味着每次从流中读一个或多个字…

酷开科技 | 酷开系统,让你与家人共度美好时光!

在日渐繁忙的生活中&#xff0c;我们常常会忽略和家人朋友的相处时光&#xff0c;有时候&#xff0c;我们亟需一种休闲方式&#xff0c;让家庭成员能够围坐在一起&#xff0c;享受无忧无虑的温馨和欢笑。酷开科技&#xff0c;致力于为消费者提供舒适的产品和服务内容&#xff0…

岂曰无衣 汉家衣裳再现锦绣美景

——福州第五届1122汉服节出行盛大开幕11月25日下午&#xff0c;闽江之心海丝广场&#xff0c;一场盛大的汉服文化节——福州第五届1122汉服节出行活动在这里隆重开幕。这个被誉为“穿在身上的历史”的传统文化&#xff0c;在这片古老而神秘的土地上焕发出新的生机与活力。据了…

Win10电脑用U盘重装系统的步骤

在Win10电脑中&#xff0c;用户遇到了无法解决的系统问题&#xff0c;用户这时候就可以考虑重装Win10系统&#xff0c;这样即可轻松解决问题&#xff0c;从而满足自己的操作需求。接下来小编给大家详细介绍关于Win10电脑中用U盘重装系统的教程步骤。 准备工作 1. 一台正常联网可…

【古诗生成AI实战】之二——项目架构设计

[1] 项目架构 在我们深入古诗生成AI项目的具体实践之前&#xff0c;让我们首先理解整个项目的架构。本项目的代码流程主要分为三个关键阶段&#xff1a; 1、数据处理阶段&#xff1b;   2、模型训练阶段&#xff1b;   3、文本生成阶段。 第一步&#xff1a;在数据处理阶段…

免费分享一套基于springboot的餐饮美食分享平台系统,挺漂亮的

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于springboot的餐饮美食分享平台系统&#xff0c;分享下哈。 项目视频演示 【免费】基于springboot的餐饮美食分享平台 Java毕业设计_哔哩哔哩_bilibili【免费】基于springboot的餐饮美食分享平台 Java毕…

【SQL Server2019SSMS】安装与卸载手册

目录 &#x1f4cb;前言 ⛳️【SQL Serverssms】安装 1. SQL Server自定义安装 2. SSMS安装 ⛳️【SQL Server】卸载 &#x1f4cb;前言 &#x1f308;个人主页&#xff1a;Sarapines Programmer &#x1f525; 系列专栏&#xff1a;本期文章收录在《宝藏工具使用手册》&am…

校园导游程序及通信线路设计(结尾附着总源码)

校园导游程序及通信线路设计 摘  要 新生或来访客人刚到校园&#xff0c;对校园的环境不熟悉。就需要一个导游介绍景点&#xff0c;推荐到下一个景点的最佳路径等。随着科技的发展&#xff0c;社会的进步&#xff0c;人们对便捷的追求也越来越高。为了减少人力和时间。针对对…

skywalking 简单操作文档

1.1. 基础概念 1.1.1. 概述 SkyWalking是 apache基金会下面的一个开源 APM项目&#xff0c;为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标&#xff0c;并进行分布式追踪。通过这些调用链路以及指标&#xff0c;Skywalking APM会感知应用间关系和服务间关系…

Deep Learning(wu--46)

文章目录 ContentsBeginBasic逻辑回归SGD导数计算图&#xff08;反向传播&#xff09;向量化广播numpy Neural Network向量化激活函数梯度下降深层表示反向传播 Contents Begin Basic 逻辑回归 SGD 导数 计算图&#xff08;反向传播&#xff09; 向量化 广播 numpy Neural Netw…

Elastic Search的RestFul API入门:初识mapping

本节课旨在探讨Elasticsearch中Mapping的使用。在Elasticsearch中&#xff0c;Mapping是定义索引中字段&#xff08;Field&#xff09;的数据类型和属性的过程。它为Elasticsearch提供了一种途径&#xff0c;以解析和处理文档中的各个字段&#xff0c;以便在搜索、排序和聚合等…

ElasticSearch学习笔记(狂神说)

ElasticSearch学习笔记&#xff08;狂神说&#xff09; 视频地址&#xff1a;https://www.bilibili.com/video/BV17a4y1x7zq 在学习ElasticSearch之前&#xff0c;先简单了解一下Lucene&#xff1a; Doug Cutting开发是apache软件基金会 jakarta项目组的一个子项目是一个开放…