当前位置: 首页 > 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 指向当前正在遍历的元素 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.kler.cn/a/595256.html

相关文章:

  • PostgreSQL_数据表结构设计并创建
  • 使用 Ansys Fluent 评估金属管道腐蚀
  • 1204. 【高精度练习】密码
  • 《Python实战进阶》No42: 多线程与多进程编程详解(上)
  • 【漫话机器学习系列】153.残差平方和(Residual Sum of Squares, RSS)
  • LeetCode 2680.最大或值:位运算
  • 如何在IPhone 16Pro上运行python文件?
  • 【UI设计】一些好用的免费图标素材网站
  • el-select下拉框,搜索时,若是匹配后的数据有且只有一条,则当失去焦点时,默认选中该条数据
  • ngx_http_conf_port_t
  • 每天学一个 Linux 命令(6):shutdown
  • QT学习笔记3
  • ⭐算法OJ⭐二叉树的后序遍历【树的遍历】(C++实现)Binary Tree Postorder Traversal
  • 强大的AI网站推荐(第二集)—— V0.dev
  • 解释下Cumulative Layout Shift (CLS)以及如何优化?
  • JavaScript(JS)单线程影响速度
  • Linux:gsd-account进程异常内存泄漏排查
  • 背包问题——多重背包(C语言)
  • go中的文件、目录的操作
  • vscode/cursor中python运行路径设置 模块导入问题