Java 记忆链表,LinkedList 的升级版

news2025/4/20 10:05:57

文章目录

  • 记忆链表 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 指向当前正在遍历的元素 x
            if (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.coloradmin.cn/o/2319590.html

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

相关文章

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…

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…

数据结构——顺序栈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 获取栈顶元…

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…

MySQL身份验证的auth_socket插件

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

使用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博客

【技术简析】触觉智能RK3506 Linux星闪网关开发板:重新定义工业物联新标杆

在工业智能化与物联网深度融合的今天&#xff0c;深圳触觉智能推出首款搭载瑞芯微RK3506芯片的Linux星闪网关开发板&#xff0c;为大家技术解析。 RK3506-国产芯的硬核实力 作为瑞芯微2024年第四季度推出的入门级工业芯片平台&#xff0c;RK3506以三核Cortex-A7&#xff08;1.…

YOLO-UniOW: 高效通用开放世界目标检测模型【附论文与源码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

面向对象(进阶)(‘封装‘,‘多态‘,‘对象属性‘,‘类属性‘,‘类方法‘,‘对象方法‘及其应用场景)

‘封装’,多态’及其应用场景 一, 封装及其应用场景 封装的定义 属于面向对象的三大特征之一, 就是隐藏对象的属性和现实细节, 仅对外提供公共的访问方式.(我们学的 函数, 类, 都是封装的体现). 封装的格式 __属性名 __属性名()封装的好处和弊端 好处 提高代码的安全性.…

算法模型从入门到起飞系列——广度优先遍历(BFS)

文章目录 前言一、广度优先遍历&#xff08;BFS&#xff09;简介1.1 广度优先遍历&#xff08;BFS&#xff09;的特点1.2 广度优先遍历&#xff08;BFS&#xff09;的要素 二、广度优先遍历&#xff08;BFS&#xff09;& 深度优先遍历&#xff08;DFS&#xff09;2.1 广度优…

<项目> 主从Reactor模型的高并发服务器

目录 Reactor 概念 分类 单Reactor单线程 单Reactor多线程 多Reactor多线程 项目介绍 项目规划 模块关系 实现 TimerWheel -- 时间轮定时器 定时器系统调用 时间轮设计 通用类型Any Buffer Socket Channel Poller EventLoop&#xff08;核心&#xff09; eventfd 设计思路 …

注意力机制,本质上是在做什么?

本文以自注意机制为例&#xff0c;输入一个4*4的矩阵 如下&#xff1a; input_datatorch.tensor([[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,16] ],dtypetorch.float) 得到Q和K的转置如下。 此时&#xff0c;计算QK^T ,得到如下结果 第一行第一个位置就是第一条样本和第…