Java 记忆链表,LinkedList 的升级版

文章目录

  • 记忆链表 MemoryLinkedList
  • 实战
  • 源代码

  众所周知,ArrayList 和 LinkedList 是 Java 集合中两个基本的数据结构,对应数据结构理论中的数组和链表。但在这两个数据结构,开发者们通常使用 ArrayList,而不使用 LinkedList。JDK 1.2 源码 LinkedList 的作者 Josh Bloch 也几乎不使用 LinkedList。

  为什么会这样呢?从数据结构理论中上看,它们之间应该只是各有千秋。其中,ArrayList 的定标访问效率应该优于 LinkedList,而 LinkedList 的插入和删除效率应该优于 ArrayList 才对。简单来说,ArrayList 的读性能应该优于 LinkedList,而 LinkedList 的写性能应该优于 ArrayList。但实际上,ArrayList 的大多数指标都要优于 LinkedList。

  出现这一现象的原因有很多。在内存占用来说,Java 的对象都是匿名的,有名称的变量实际储存的是该变量的指针。因此 ArrayList 和 LinkedList 相当于一个指针数组,而指针本身的数据占用是很小的。由于 LinkedList 每个结点的数据结构过于复杂,有很多额外的数据,如上一个结点的指针、下一个结点的指针。因为储存实际数据元素本身用的就是指针,因此如果除去实际存储那部分有用的数据,LinkedList 占用的的空间将会是 ArrayList 的 2 ~ 3 倍。

  内存占用还会体现在时间上。ArrayList 因为内存占用小,在数据量小的情形下,插入和删除时移动整片区域也实际不会耗费多少时间。

  另外,就删除来言,一般都是要先查找到目标元素之后才能删除,因此 LinkedList 的理论删除效率也没有比 ArrayList 高多少。

  此外,操作系统可能会对内存进行优化(局部性原理),缓存之前用到数据的附近位置的数据,这对 ArrayList 这种底层为连续内存的数据结构非常有利。

  总而言之,相比于 ArrayList,LinkedList 是比较糟糕的,通常大家不会使用 LinkedList。

记忆链表 MemoryLinkedList

  虽然通常 LinkedList 比 ArrayList 要差,不过我们通常是根据实际场景选择技术,而不能根据刻板印象一概而论。在大数据的定点删除,LinkedList 将会优于 ArrayList。但是,LinkedList 每次删除都要从头开始遍历,有时候,这是没有必要的。而这种从每次头开始遍历会大大削弱 LinkedList 的效率优势。

  想象这样的一种情况,需要根据某个条件,在 LinkedList 中删除多个元素。这种情况下,如果每次删除都头开始遍历,就非常浪费时间。因为已经遍历过的区域实际上不存在需要删除的元素。

  如果可以在遍历时记住上次遍历的位置就可以解决这一情况。为此,笔者在 LinkedList 的基础上,开发了一种记忆链表 MemoryLinkedList,它可以记住上次遍历的位置,让下一次遍历时可以从上次中止遍历的地方开始。此外,它还提供一种闭包用于进行基本操作的嵌套,如在遍历时删除当前元素、进行子遍历等等。


核心代码如下:

/*** 提供带自定义比较器的判断是否包含的方法。这个自定义比较器用于判断表中的元素是否相等。* 通常在元素为数组时使用本方法,因为数组的 equals 方法无法重写,* 而数组的原 equals 方法只是一种指针比较,因此可以使用本方法来弥补这一缺陷** @since 2023-2-5*/
public boolean contains(E other, Comparator<E> comparator) {if (other == null) {for (Node<E> x = this.first; x != null; x = x.next) {if (x.item == null) {return true;}}} else {for (Node<E> x = this.first; x != null; x = x.next) {if (comparator.compare(x.item, other) == 0) {return true;}}}return false;
}/*** 记忆点** 指向没有被遍历的第一个元素的位置** @since 2023-1-15*/
private transient Node<E> memory = this.first;/*** 快照记忆点** @since 2023-1-15*/
private transient Node<E> snapshotMemory;/*** 开启记忆模式** 调用后面的所有“记忆”函数之前,必须先调用本方法。记忆模式只对记忆函数起作用,其它函数不受影响,因此记忆模式不需要关闭** 个人自增方法** @since 2023-1-15*/
public MemoryLinkedList<E> setMemoryMode() {return this.resetMemory();
}/*** @since 2023-1-15*/
public MemoryLinkedList<E> resetMemory() {this.memory = this.first;return this;
}/*** 保存当前 memory 的一个快照** @since 2023-1-15*/
public MemoryLinkedList<E> saveSnapshotMemory() {this.snapshotMemory = this.memory;return this;
}/*** 将快照 memory 加载到 memory 中。此方法必须在 saveSnapshotMemory 至少调用一次后才能调用** @since 2023-1-15*/
public MemoryLinkedList<E> loadSnapshotMemory() {this.memory = this.snapshotMemory;return this;
}/*** 遍历。此方法不是记忆方法** Function:此方法的入参代表遍历的每个元素,返回值代表是否继续循环(true 代表继续循环)** 个人自增方法** @since 2023-1-15*/
public MemoryLinkedList<E> traverse(ListTraverseProcess<E> process) {for (Node<E> x = this.first; x != null; x = x.next) {if (!process.foreach(x.item)) {break;}}return this;
}/*** 遍历。此方法是记忆方法。在遍历正常到表尾时,此方法会自动重置记忆点** Function:此方法的入参代表遍历的每个元素,返回值代表是否继续循环(true 代表继续循环)** 个人自增方法** @since 2023-1-15*/
public MemoryLinkedList<E> traverseContinuously(ListTraverseProcess<E> process) {Node<E> xN = null;for (Node<E> x = this.memory; x != null; x = xN) {xN = x.next; // 这是为了防止下面的 apply 方法中含删除本结点的操作导致本结点的指针无效,因此需要提前保存下一个结点if (!process.foreach(x.item)) { // 当执行 apply 方法时,memory 指向当前正在遍历的元素 xif (xN == null) {this.resetMemory(); // 此处说明当前遍历的是表尾元素,因此需要重置记忆点} else {this.memory = xN; // 保存遍历元素的下一个元素的位置}return this;}this.memory = xN; // 遍历时,快照记忆点必须立刻保存,而不能等到退出循环的那一刻才保存}this.resetMemory(); // 重置记忆点return this;
}/*** 删除。此方法是记忆方法** 个人自增方法** @param needFeedback 如果为 true,则删除失败时会抛出异常。如果为 false,什么也不会发生* @since 2023-1-15*/
public MemoryLinkedList<E> removeContinuously(Object obj, boolean needFeedback) {for (Node<E> x = this.memory; x != null; x = x.next) {if (Objects.equals(x.item, obj)) {this.memory = x.next; // 保存删除元素的下一个元素的位置。只有在退出循环的那一刻才需要保存this.unlink(x);return this;}}if (needFeedback) {throw new RuntimeException("没有找到删除元素,删除失败");}return this;
}

实战

  纸上得来终觉浅,没有实战的讲解没有任何意义。这里结合具体代码来介绍记忆链表 MemoryLinkedList 的使用。

  笔者在早年间曾经进行过大量的彩票、股票研究。其中有这样一个场景,需要列出所有满足指定条件双色球号码。这里采取的策略是,先使用 MemoryLinkedList 收集所有的双色球号码,然后遍历每一个号码,从中去掉不符条件的号码。因为双色球总号码数量巨大,所以不适合基于连续内存的 ArrayList。由于在一次遍历中,需要大量的删除操作,因此也不适合原始的 LinkedList。于是,笔者编写的 LinkedList 升级版,记忆链表 MemoryLinkedList 就派上用场了。

  双色球的投注规则是,从红色球‌的 1 ~ 33 号码中选择‌ 6 个,从蓝色球‌的 1 ~ 16 号码中选择‌ 1 个。


下面的代码给出了红球 胆码 的筛选过程。

/*** 胆码。numbers 中所有的号码都要存在,因此 numbers 的长度不能超过 6** @since 2023-1-14*/
public DcRedLottery braveNumber(int... numbers) {this.redResults.setMemoryMode().traverseContinuously(ele -> {if (!IntMathSetUtil.contain(ele, numbers)) {this.redResults.removeContinuously(ele, false);}return true;});return this;
}

下面的代码给出了红球 杀码 的筛选过程。

/*** 杀码。去掉所有含 numbers 中任意一个元素的组合** @since 2023-1-14*/
public DcRedLottery killNumber(int... numbers) {this.redResults.setMemoryMode().traverseContinuously(ele -> {// 此处需要求交集,不是用包含来判断if (IntMathSetUtil.intersect(ele, numbers).length >= 1) {this.redResults.removeContinuously(ele, false);}return true;});return this;
}

下面的代码给出了红球 复式 的筛选过程。

/*** 复式。从 numbers 中选择 choiceNum 个号码作为胆码** @param only 为 true 代表 numbers 中只能选择 choiceNum 个号码,numbers 中的其它号码不能存在* @since 2023-1-19*/
public DcRedLottery multiple(int[] numbers, int choiceNum, boolean only) {// 为了提高效率,此层判断必须放到外面if (only) {this.redResults.setMemoryMode().traverseContinuously(ele -> {if (!(IntMathSetUtil.intersect(ele, numbers).length == choiceNum)) {this.redResults.removeContinuously(ele, false);}return true;});} else {this.redResults.setMemoryMode().traverseContinuously(ele -> {if (!(IntMathSetUtil.intersect(ele, numbers).length >= choiceNum)) {this.redResults.removeContinuously(ele, false);}return true;});}return this;
}

下面是一些更复杂的情况。

/*** 横向号码连续情况。本方法允许 continuousNum 或 groupNum 出现大于的情况。* 比如,* 就算限定 2 连续,也可以出现 3 连续。* 就算限定 2 连续为 2 个,则也可以允许 2 连续出现 3 个。* 就算限定 2 连续为 2 个,则也可以允许 2 连续出现 1 个,然后 3 连续出现 1 个。** @param continuousNum 有 continuousNum 个号码连续* @param groupNum 对于 continuousNum 个号码连续的情况,出现了 groupNum 组(不连续的号码群称为一组)* @since 2023-12-17*/
public DcRedLottery hContinuousBigger(int continuousNum, int groupNum) {this.redResults.setMemoryMode().traverseContinuously(ele -> {var groups = GroupAnalysisUtil.generateGroups(ele);var distribution = GroupAnalysisUtil.continuousDistribution(groups);int sum = 0;// 统计连续数不小于 continuousNum 的组数for (int num = continuousNum; num < distribution.length; ++num) {sum += distribution[num];}if (sum < groupNum) {this.redResults.removeContinuously(ele, false);return true;}return true;});return this;
}

下面的代码了筛选出了满足如下条件的双色球号码。

  • 至少有一个号码与 ref 的距离为 1 及以内。其中,ref = {1, 2, 10, 22, 24, 25}
  • 3 区比为 2:3:1
  • 奇偶比为 2:4
  • 有且只有两个号码连续
int[] ref = {1, 2, 10, 22, 24, 25};
var redLottery = DcRedLottery.getInstance().selectAll().near(ref, 1, ComparativePredicate.MOST, 1, ComparativePredicate.LEAST).section3Proportion(2, 3, 1).parity(2, 4).hContinuousEqually(2, 1);

从下图可以看出,满足上述条件的号码有 10244 个。

在这里插入图片描述

源代码

  记忆链表 MemoryLinkedList 的源代码被收录在笔者的开源项目 jdk-enhance 中,可免费下载。GitHub 地址:https://github.com/wangpai-common-util-Java/jdk-enhance

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

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

相关文章

《白帽子讲 Web 安全》之开发语言安全深度解读

目录 引言 1.PHP 安全 1.1变量覆盖 1.2空字节问题 1.3弱类型 1.4反序列化 1.5安全配置 2Java 安全 2.1Security Manager 2.2反射 2.3反序列化 3Python 安全 3.1反序列化 3.2代码保护 4.JavaScript 安全 4.1第三方 JavaScript 资源 4.2JavaScript 框架 5.Node.…

鸿蒙HarmonyOS NEXT应用崩溃分析及修复

鸿蒙HarmonyOS NEXT应用崩溃分析及修复 如何保证应用的健壮性&#xff0c;其中一个指标就是看崩溃率&#xff0c;如何降低崩溃率&#xff0c;就需要知道存在哪些崩溃&#xff0c;然后对症下药&#xff0c;解决崩溃。那么鸿蒙应用中存在哪些崩溃类型呢&#xff1f;又改如何解决…

分析K8S中Node状态为`NotReady`问题

在Kubernetes&#xff08;k8s&#xff09;集群中&#xff0c;Node状态为NotReady通常意味着节点上存在某些问题&#xff0c;下面为你分析正常情况下节点应运行的容器以及解决NotReady状态的方法。 正常情况下Node节点应运行的容器 1. kubelet kubelet是节点上的核心组件&…

第六届机电一体化技术与智能制造国际学术会议(ICMTIM 2025)

重要信息 4月11-13日 南京江北新区工业大学亚朵酒店 www.icmtim.org&#xff08;点击了解参会投稿等&#xff09; 简介 由南京工业大学主办&#xff0c;南京工业大学电气工程与控制科学学院、中国矿业大学、黑龙江大学、江苏省自动化学会承办的第六届机电一体化技术…

INT202 Complexity of Algroithms 算法的复杂度 Pt.2 Search Algorithm 搜索算法

文章目录 1.树的数据结构1.1 有序数据(Ordered Data)1.1.1 有序字典&#xff08;Ordered Dictonary&#xff09;1.1.1.1 排序表&#xff08;Sorted Tables&#xff09; 1.2 二分查找&#xff08;Binary Search&#xff09;1.2.1 二分查找的时间复杂度 1.3 二叉搜索树&#xff0…

【AVRCP】蓝牙链路控制器(LC)与AVRCP互操作性要求深度解析

目录 一 、Link Controller&#xff08;LC&#xff09;概述 1.1 LC的定义与功能 1.2 LC在蓝牙技术中的重要性 二、Link Controller&#xff08;LC&#xff09;互操作性要求 2.1 互操作性要求概述 2.2 物理层互操作性要求 2.3 链路管理互操作性要求 2.4 其他互操作性要求…

高级背景抠图工具(python)

这是一个专业的图像背景处理工具,基于Python开发,主要功能包括:1. 智能背景去除 - 使用rembg库的深度学习模型自动识别并移除图片背景。 2. 背景自定义 - 支持纯色背景替换,保留透明通道(Alpha通道)。3. 高级参数调节 - 提供5种专业级图像处理参数。4. 实时预览 - 双窗口…

如何设计外贸邮件开发信主题

开发信是打开客户大门的第一步&#xff0c;而邮件主题则是决定客户是否打开邮件的关键。一个吸引人的主题不仅能提高打开率&#xff0c;还能为后续沟通打下良好基础。 一、突出价值和利益 邮件主题要直接传达收件人能从中获得的价值和利益&#xff0c;引起他们的兴趣和关注。…

wordpress表单插件CF7调用方式

Contact Form 7(CF7)是WordPress中非常流行的表单插件&#xff0c;以下是其常见的调用方式&#xff1a; 通过短代码调用 在页面或文章编辑器中添加&#xff1a;完成表单设置后&#xff0c;复制表单对应的短代码&#xff0c;然后在需要显示表单的页面或文章的编辑器中直接粘贴…

快速入手-基于Django的主子表间操作mysql(五)

1、如果该表中存在外键&#xff0c;结合实际业务情况&#xff0c;那可以这么写&#xff1a; 2、针对特殊的字典类型&#xff0c;可以这么定义 3、获取元组中的字典值和子表中的value值方法 4、对应的前端页面写法

网络运维学习笔记(DeepSeek优化版) 021 HCIA-Datacom新增知识点03园区网典型组网架构及案例实战

文章目录 园区网典型组网架构及案例实战1 园区网定义2 园区网络典型架构3 各层级协议与技术4 项目生命周期管理5 小型园区网络设计框架5.1 组网方案设计5.2 IP地址规划5.3 园区内部的路由设计5.4 NAT设计5.5 WLAN设计5.6 安全设计5.7 运维管理设计 6 小型园区的实施方案与运维手…

1.8 函数的连续性和间断点

1.连续的定义 2.间断点的定义 3.间断点的分类

基于Arm GNU Toolchain编译生成的.elf转hex/bin文件格式方法

基于Arm GNU Toolchain编译生成的.elf转hex/bin文件格式方法 已经弃用的版本&#xff08;Version 10.3-2021.10&#xff09;&#xff1a;gcc-arm-none-eabi&#xff1a;https://developer.arm.com/downloads/-/gnu-rmArm GNU Toolchain当前版本&#xff1a;https://developer.a…

希尔排序中的Hibbard序列

一 定义 Hibbard序列的每个元素由以下公式生成: h_k = 2^k - 1 其中k从1开始递增,序列为:1, 3, 7, 15, 31, 63, … 二 生成方式 起始条件:k=1,对应h_1=2^1-1=1 递推公式:每次k增加1,计算 h_{k+1}=2^{k+1}-1 示例:前5项…

失败的面试经历(ʘ̥∧ʘ̥)

一.面向对象的三大特性 1.封装&#xff1a;将对象内部的属性私有化&#xff0c;外部对象不能够直接访问&#xff0c;但是可以提供一些可以使外部对象操作内部属性的方法。 2.继承&#xff1a;类与类之间会有一些相似之处&#xff0c;但也会有一些异处&#xff0c;使得他们与众…

算法及数据结构系列 - 二分查找

系列文章目录 算法及数据结构系列 - BFS算法 文章目录 二分查找框架思路经典题型二分查找寻找左侧边界寻找右侧边界 刷题875. 爱吃香蕉的珂珂1011. 在 D 天内送达包裹的能力392. 判断子序列 二分查找 框架思路 int binarySearch(int[] nums, int target) {int left 0, righ…

SpringBoot的启动原理?

大家好&#xff0c;我是锋哥。今天分享关于【SpringBoot的启动原理&#xff1f;】面试题。希望对大家有帮助&#xff1b; SpringBoot的启动原理&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring Boot的启动原理主要是通过 SpringApplication 类来…

代码随想录第55期训练营第八天|LeetCode344.反转字符串、541.反转字符串II、卡码网:54.替换数字

前言 这是我参加的第二次训练营&#xff01;&#xff01;&#xff01;爽&#xff01;这次我将更加细致的写清每一道难题&#xff0c;不仅是提升自己&#xff0c;也希望我自己的写的文章对读者有一定的帮助&#xff01; 打卡代码随想录算法训练营第55期第八天&#xff08;づ&a…

Json的应用实例——cad 二次开发c#

以下是一个使用AutoCAD C#.NET API实现你需求的示例代码&#xff0c;代码实现了提示用户选择一个实体&#xff0c;将一些字符串变量及其对应的值组成JSON格式数据存储到实体的扩展数据&#xff08;XData&#xff09;中&#xff0c;并在弹出窗口中显示该实体的所有扩展数据信息。…

Springboot的jak安装与配置教程

目录 Windows系统 macOS系统 Linux系统 Windows系统 下载JDK&#xff1a; 访问Oracle官网或其他JDK提供商网站&#xff0c;下载适合Windows系统的JDK版本。网站地址&#xff1a;Oracle 甲骨文中国 | 云应用和云平台点击进入下滑&#xff0c;点击进入下载根据自己的系统选择&…