当前位置: 首页 > article >正文

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

相关文章:

Java 记忆链表,LinkedList 的升级版

文章目录 记忆链表 MemoryLinkedList实战源代码 众所周知&#xff0c;ArrayList 和 LinkedList 是 Java 集合中两个基本的数据结构&#xff0c;对应数据结构理论中的数组和链表。但在这两个数据结构&#xff0c;开发者们通常使用 ArrayList&#xff0c;而不使用 LinkedList。JD…...

【构建CV图像识别系统】从传统方法到深度学习

目录 1. 图像的基本概念1.1 像素与色彩1.2 过滤与卷积 2. 图像分类与检测3. 图像特征的提取3.1 全局特征3.2 局部特征3.2.1 边缘&#xff08;Edge&#xff09;3.2.2 角点&#xff08;Corner&#xff09;3.2.3 SIFT 特征 4. 传统方法与深度学习在图像识别中的应用4.1 基于传统方…...

.net core集成MQTT服务端

程序作为MQTT的服务端&#xff0c;也是WebApi 接口地址&#xff0c;在Web页面中MQTTJS用的是Websocker协议&#xff0c;在Winfrom中用MQTT协议。导致程序需要启动两个端口。直接上代码 创建服务 引用包&#xff1a;MQTTnet&#xff0c;MQTTnet.AspNetCore&#xff0c;这包最新…...

poetry安装与使用

文章目录 安装方法创建虚拟环境其他常用命令从 poetry.lock 中安装第三方依赖包 安装方法 安装命令&#xff08;全局安装&#xff0c;不要在虚拟环境中安装&#xff0c;方便后面创建环境使用&#xff09; pip install poetry修改虚拟环境路径&#xff08;首次使用poetry时执行&…...

UVM config机制及uvm_resource_pool

目录 1. uvm_config_db 类源码 1.1 set 1.2 get 2. uvm_resource_pool 2.1 uvm_resource_pool::set 2.2 uvm_resource 3. usage 4. 小结 uvm提供一种uvm_config_db机制使得在仿真中通过变量设置来修改环境,使环境更加灵活。本文主要介绍uvm_config_db#(type)::get/set…...

JAVA学习*接口

接口 在生活中我们常听说USB接口&#xff0c;那接口是什么呢&#xff1f; 在Java中&#xff0c;接口相当于多个类的一种公共规范&#xff0c;是一种引用数据类型。 定义接口 public interface IUSB {public static final String SIZE "small";public abstract vo…...

Day11 动态规划入门

动态规划 就是 : 给定一个问题&#xff0c;我们把它拆成一个个子问题&#xff0c;直到子问题可以直接解决。然后把子问题的答案保存起来&#xff0c;以减少重复计算。再根据子问题答案反推&#xff0c;得出原问题解的一种方法. 记忆化搜索 暴力dfs 记录答案 动态规划入门思…...

WPF UI元素保存为图像文件

WPF UI元素保存为图像文件 实现功能示例代码使用示例关键代码说明WPF UI元素保存为图像文件 实现功能 将WPF界面元素(如控件、布局容器)的当前视觉内容保存为图像文件适用场景:截取控件的实时显示内容(如图表、界面快照);将动态生成的UI元素导出为图片用于分享、存档或打…...

指令型样本或偏好型样本有什么区别和联系

两者都是基于给定文本生成的训练样本&#xff0c;但侧重点和用途不同&#xff1a; 指令型样本&#xff08;Instruction-based samples&#xff09; 结构&#xff1a;通常是一个简单的指令和对应的回答&#xff0c;例如一对“问题&#xff0d;答案”或“指令&#xff0d;回答”。…...

neo4j-如何让外部设备访问wsl中的neo4j

WSL 运行在一个虚拟网络环境中&#xff0c;它的 IP 只能被宿主 Windows 访问&#xff0c;外部设备无法直接访问 WSL 的端口。你需要在 Windows 上转发端口&#xff0c;让外部设备可以访问 Windows 并映射到 WSL。 1. 获取 WSL 的 IP 地址 在 WSL 中运行以下命令获取其 IP 地址…...

Python实验:读写文本文件并添加行号

[实验目的] 熟练掌握内置函数open()的用法&#xff1b;熟练运用内置函数len()、max()、和enumerate()&#xff1b;熟练运用字符串的strip()、ljust()和其它方法&#xff1b;熟练运用列表推导式。 [实验和内容] 1.编写一个程序demo.py&#xff0c;要求运行该程序后&#xff0…...

IDEA导入jar包后提示无法解析jar包中的类,比如无法解析符号 ‘log4j‘

IDEA导入jar包后提示无法解析jar包中的类 问题描述解决方法 问题描述 IDEA导入jar包的Maven坐标后&#xff0c;使用jar中的类比如log4j&#xff0c;仍然提示比如无法解析符号 log4j。 解决方法 在添加了依赖和配置文件后&#xff0c;确保刷新你的IDE项目和任何缓存&#xff…...

抖音用户视频批量下载工具开发全解析

一、逆向工程原理剖析 1.1 抖音Web端防护体系 抖音采用五层防御机制保护数据接口: graph LRA[浏览器指纹检测] --> B[请求参数签名]B --> C[Cookie动态验证]C --> D[请求频率限制]D --> E[IP信誉评级] 1.2 核心参数解密 参数名称作用原理生成方式有效期x-bogu…...

数据结构——顺序栈seq_stack

前言&#xff1a;大家好&#x1f60d;&#xff0c;本文主要介绍了数据结构——顺序栈 目录 一、概念 1.1 顺序栈的基本概念 1.2 顺序栈的存储结构 二、基本操作 2.1 结构体定义 2.2 初始化 2.3 判空 2.4 判满 2.5 扩容 2.6 插入 入栈 2.7 删除 出栈 2.8 获取栈顶元…...

LangChain其它五类组件详解(1)—— 文档加载器(Document loaders)

LangChain其它五类组件详解(1)—— 文档加载器(Document loaders) 前言本篇摘要15. LangChain其它五类组件详解15.1 文档加载器(Document loaders)15.1.1 文档加载概述15.1.2 加载Markdown1. 基本用法2. 保留元素参考文献前言 本系列文章主要介绍WEB界面工具Gradio。Gra…...

JVM常见面试总结

JVM&#xff08;Java虚拟机&#xff09;是Java程序运行的核心&#xff0c;掌握JVM相关知识对于Java开发者至关重要。以下是JVM常见的面试问题总结&#xff1a; 1. JVM内存模型 问题&#xff1a;JVM的内存结构分为哪些部分&#xff1f; 答案&#xff1a; 方法区&#xff08;Met…...

美团Leaf分布式ID生成器使用教程:号段模式与Snowflake模式详解

引言 在分布式系统中&#xff0c;生成全局唯一ID是核心需求之一。美团开源的Leaf提供了两种分布式ID生成方案&#xff1a;号段模式&#xff08;高可用、依赖数据库&#xff09;和Snowflake模式&#xff08;高性能、去中心化&#xff09;。本文将手把手教你如何配置和使用这两种…...

python3.13.2安装详细步骤(附安装包)

文章目录 前言一、python3.13.2下载二、python3.13.2安装详细步骤1.查看安装文件2.启动安装程序3.安装模式选择4.自定义安装配置5.高级选项设置6.执行安装7.开始安装8.安装完成8.打开软件9.安装验证 前言 在数字化时代&#xff0c;Python 已成为不可或缺的编程语言。无论是开发…...

AI-Talk开发板之更换串口引脚

一、默认引脚 CSK6011A使用UART0作为Debug uart&#xff0c;AI-Talk开发板默认使用的GPIOA2和GPIOA3作为Debug uart的RX和TX&#xff0c;通过连接器CN6引出。 二 、更换到其它引脚 查看60xx_iomux_v1.0可以&#xff0c;UART0的tx和rx可以映射到很多管脚上。 结合AI-Talk开发板…...

深度解读DeepSeek:源码解读 DeepSeek-V3

深度解读DeepSeek&#xff1a;开源周&#xff08;Open Source Week&#xff09;技术解读 深度解读DeepSeek&#xff1a;源码解读 DeepSeek-V3 深度解读DeepSeek&#xff1a;技术原理 深度解读DeepSeek&#xff1a;发展历程 文章目录 整体流程模型初始化模型前向传播MoE https:/…...

JavaIO流的使用和修饰器模式(直击心灵版)

系列文章目录 JavaIO流的使用和修饰器模式 文章目录 系列文章目录前言一、字节流&#xff1a; 1.FileInputStream(读取文件)2.FileOutputStream(写入文件) 二、字符流&#xff1a; 1..基础字符流:2.处理流&#xff1a;3.对象处理流&#xff1a;4.转换流&#xff1a; 三、修饰器…...

爬虫入门re+bs4

目录 前言 1. 导入必要的库 2. 定义获取网页HTML内容的函数 get_html 3. 定义获取数据的函数 get_data 4. 定义获取文章正文内容的函数 content_text 5. 定义获取单条课程数据的函数 get_one_course_data 6. 定义保存数据的函数 save_data 7. 定义文件名合法化处理函数 sanitiz…...

【WebGL】texImage2D函数

参数 从像素数据加载纹理 gl.texImage2D(target, level, internalformat, width, height, border, format, type, source);从图像元素加载纹理 gl.texImage2D(target, level, internalformat, format, type, image);target gl.TEXTURE_2D&#xff08;2D 纹理&#xff09; T…...

北斗设备启动流程与时长解析

北斗卫星导航系统作为我国自主研发的全球卫星导航系统&#xff0c;广泛应用于交通、通信、农业等多个领域。今天&#xff0c;我们就来详细探讨一下北斗设备的启动流程以及不同启动方式下的时长。 一、北斗设备的启动流程 北斗设备的启动流程可以分为以下几个关键步骤&#xf…...

MySQL身份验证的auth_socket插件

在Ubuntu 20.04 LTS上&#xff0c;MySQL 8.0默认使用auth_socket插件进行身份验证&#xff0c;可能存在意想不到的情况。 一、auth_socket插件 在使用sudo mysql或通过sudo切换用户后执行任何MySQL命令时&#xff0c;不需要输入密码或错误密码都可以正常登入mysql数据库&…...

openstack安装部署

在OpenStack的安装和部署中&#xff0c;你需要按照一定的步骤来完成整个环境的搭建。OpenStack是一个开源的云计算平台&#xff0c;它提供了基础设施即服务&#xff08;IaaS&#xff09;的能力&#xff0c;包括计算、存储和网络等资源的管理。下面是一些基本的步骤来安装和部署…...

【日志库】—— log4cpp 部署套路

部署&#xff1a; 1、安装log4cpp&#xff0c;执行如下指令进行编译安装 log4cpp的官网是&#xff1a; http://log4cpp.sourceforge.net/ wget https://nchc.dl.sourceforge.net/project/log4cpp/log4cpp-1.1.x%20%28new%29/log4cpp-1.1/log4cpp-1.1.3.tar.gz tar xzvf log4cpp…...

使用Gitee Go流水线部署个人项目到服务器指南

使用Gitee Go流水线部署个人项目到服务器指南 前言&#xff01;&#xff01;&#xff01; 本文解决的问题&#xff1a; 你有一台ECS服务器&#xff0c;你在上面部署了一个Java服务也就是一个jar&#xff0c;你觉着你每次手动本地打包&#xff0c;上传&#xff0c;在通过命令去…...

BlockChain.java

BlockChain 区块链&#xff0c;举个栗子 注意啦&#xff0c;列子里面的hashcode相等&#xff0c;但是字符串是不一样的哦&#xff0c;之前有记录这个问题 String.hashCode()-CSDN博客...

SystemVerilog 数据类型

1、内建数据类型 verilog有两种基本的数据类型&#xff1a;变量和线网&#xff0c;他们各自都可以有四种取值&#xff1a;0 1 z x&#xff1b; RTL代码使用 变量 来存放组合和时序值&#xff1b;变量可以是单bit或者是多bit的无符号数 reg [7:0] m&#xff0c; 32bit的有符号…...