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.rhkb.cn/news/38927.html

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

相关文章

【数据分享】2000—2024年我国省市县三级逐年归一化植被指数(NDVI)数据(年平均值/Shp/Excel格式)

之前我们分享过2000-2024年我国逐年的归一化植被指数&#xff08;NDVI&#xff09;栅格数据&#xff0c;该逐年数据是取的当年月归一化植被指数&#xff08;NDVI&#xff09;的年平均值。&#xff01;该数据来源于NASA定期发布的MOD13A3数据集&#xff01;很多小伙伴拿到数据后…

MySQL索引

目录 索引的引入 再次理解MySQL数据操作 索引 页内目录 页间目录 索引结构为什么要采用B树&#xff1f; 聚簇索引和非聚簇索引 聚簇索引 非聚簇索引 主键索引和非主键索引 索引相关操作 创建主键索引 创建唯一键索引 创建普通索引 查询索引 删除索引 索…

数据分析面试--京东

1.考察日期函数的应用 select Order_date, count(distinct user_id) as uv from (select user_id, Order_date, row_number() over(partition by user_id order by Order_date) as new_tagfrom ord where date_diff(current_date(), Order_date)<30 ) t where new_tag1 gro…

从零到一开发一款 DeepSeek 聊天机器人

AI聊天机器人 目标设计方案系统架构技术选型功能模块 实现代码环境配置安装依赖 核心代码API 请求函数主循环函数 功能扩展1. 情感分析2. 多语言支持3. 上下文记忆4. 用户身份识别 总结附录 目标 开发一个智能聊天机器人&#xff0c;旨在为用户提供自然、流畅的对话体验。通过…

常见中间件漏洞攻略-Jboss篇

一、CVE-2015-7501-Jboss JMXInvokerServlet 反序列化漏洞 第一步&#xff1a;开启靶场 第二步&#xff1a;访问该接口&#xff0c;发现直接下载&#xff0c;说明接⼝开放&#xff0c;此接⼝存在反序列化漏洞 http://47.103.81.25:8080/invoker/JMXInvokerServlet 第三步&…

题解:AT_abc170_f [ABC170F] Pond Skater

题目描述 アメンボのすぬけ君は南北 H マス東西 W マスの長方形の形をしたグリッド状の池に住んでいます。北から i 番目、西から j 番目のマスをマス (i,j) とします。 いくつかのマスには蓮の葉が浮かんでおり、すぬけ君はそれらのマスには入ることができません。 cij​ が…

Kafka日志管理系统深度解析

Kafka日志管理系统深度解析 在分布式消息队列领域&#xff0c;Kafka因其高性能、可扩展性和可靠性而广受欢迎。而日志管理系统是Kafka的核心基础设施&#xff0c;它直接决定了Kafka的性能表现和可靠性保证。 分段式存储设计 Kafka采用分段式存储设计&#xff0c;将每个分区的…

DeepSeek、Grok 与 ChatGPT 4.5:新一代大模型架构与推理能力深度解析

近年来&#xff0c;大语言模型&#xff08;LLM&#xff09;领域发展迅猛&#xff0c;DeepSeek、Grok 以及 OpenAI 最新发布的 ChatGPT 4.5 都是该领域的代表性产品。本文将从架构设计、推理能力、训练策略等方面&#xff0c;对三者进行技术对比&#xff0c;探讨其优势与潜在的应…

Oracle数据库性能优化全攻略:十大关键方向深度解析与实践指南

文章目录 一、SQL查询优化二、索引优化三、内存管理四、I/O优化五、分区表与分区索引六、并行处理七、统计信息管理八、锁与并发控制九、数据库参数调优十、应用设计优化结论 在当今数据驱动的时代&#xff0c;数据库的性能优化成为了确保企业应用高效运行的关键。Oracle作为业…

Git 使用SSH登陆

一、SSH介绍 SSH连接相比于HTTP连接会简单一点&#xff0c;因为SSH连接通过了私钥与公钥进行身份认证&#xff0c;这样就不需要像HTTP一样&#xff0c;每次clone或者操作仓库都需要输入密码 其中私钥和密钥是需要在自己电脑上生成的&#xff0c;通过命令即可生成一个私钥和一个…

openharmony中hilog实证记录说明(3.1和5.0版本)

每次用这个工具hilog都有一些小用法记不清&#xff0c;需要花一些时间去查去分析使用方法&#xff0c;为了给丰富多彩的生活留出更多的时间&#xff0c;所以汇总整理共享来了&#xff0c;它来了它来了~~~~~~~~~ 开始是想通过3.1来汇总的&#xff0c;但实际测试发现openharmony…

UDP 协议

文章目录 UDP 协议简介数据包格式UDP 通信流程抓包分析参考 本文为笔者学习以太网对网上资料归纳整理所做的笔记&#xff0c;文末均附有参考链接&#xff0c;如侵权&#xff0c;请联系删除。 UDP 协议 UDP 是一种面向无连接的传输层协议&#xff0c;属于 TCP/IP 协议簇的一种。…

数据结构之链表(双链表)

目录 一、双向带头循环链表 概念 二、哨兵位的头节点 优点&#xff1a; 头节点的初始化 三、带头双向链表的实现 1.双链表的销毁 2.双链表的打印 3.双链表的尾插和头插 尾插&#xff1a; 头插&#xff1a; 4.双链表的尾删和头删 尾删&#xff1a; 头删&#xff1a; …

内存取证之windows-Volatility 3

一&#xff0c;Volatility 3下载 1.安装Volatility 3。 要求&#xff1a;python3.7以上的版本&#xff0c;我的是3,11&#xff0c;这里不说python的安装方法 使用 pip 安装 Volatility 3&#xff1a; pip install volatility3 安装完成后&#xff0c;验证安装&#xff1a; v…

Unity的JSON工具类+LitJson的引入及使用

C#使用JSON数据 数据存储&#xff08;序列化&#xff09;&#xff1a;将C#的数据格式&#xff0c;转化为JSON字符串&#xff0c;存储或传输 数据使用&#xff08;反序列化&#xff09;&#xff1a;将JSON字符串中存储的数据&#xff0c;转化为C#可用的数据格式&#xff0c;实现…

WX小程序

下载 package com.sky.utils;import com.alibaba.fastjson.JSONObject; import org.apache.http.NameValuePair; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.Cl…

MyBatis 中 #{} 和 ${} 的区别详解

目录 1. #{} 和 ${} 的基本概念 1.1 #{} 1.2 ${} 2. #{} 和 ${} 的工作原理 2.1 #{} 的工作原理 2.2 ${} 的工作原理 3.共同点&#xff1a;动态 SQL 查询 4. 区别&#xff1a;处理方式和适用场景 4.1 处理方式 4.2 适用场景 &#xff08;1&#xff09;#{} 的适用场景…

【蓝桥杯速成】| 10.回溯切割

前面两篇内容我们都是在做有关回溯问题的组合应用 今天的题目主题是&#xff1a;回溯法在切割问题的应用 题目一&#xff1a;分割回文串 问题描述 131. 分割回文串 - 力扣&#xff08;LeetCode&#xff09; 给你一个字符串 s&#xff0c;请你将 s 分割成一些 子串&#xff…

数据结构之双向链表-初始化链表-头插法-遍历链表-获取尾部结点-尾插法-指定位置插入-删除节点-释放链表——完整代码

数据结构之双向链表-初始化链表-头插法-遍历链表-获取尾部结点-尾插法-指定位置插入-删除节点-释放链表——完整代码 #include <stdio.h> #include <stdlib.h>typedef int ElemType;typedef struct node{ElemType data;struct node *next, *prev; }Node;//初化链表…

开源视频剪辑工具,无损编辑更高效

LosslessCut 是一款基于 FFmpeg 开发的跨平台开源视频剪辑工具&#xff0c;致力于无损处理音视频文件。它无需重新编码即可完成剪切、合并、轨道编辑等操作&#xff0c;极大地保留了原始文件的质量&#xff0c;特别适合处理大体积视频&#xff0c;如无人机拍摄素材或长时录制内…