LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)

这篇具有很好参考价值的文章主要介绍了LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《LeetCode952三部曲之三》的终篇,先回顾一下前文的成果,看看我们之前已经优化到什么程度:
    LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)
  • 前文的优化思路是减小并查集数组的规模,带来的结果是节省内存、减少数组相关的执行次数,但从代码上分析,并查集数组处理所占比重并不多,所以造成此处整体优化效果一般
  • 所以,除了并查集,还要去寻找其他优化点,这就是本篇的主要内容

优化思路

  • 寻找优化点的方向很明确:重点关注时间复杂度高的代码块
  • 按照上述思路,很容易就找到了下图中的代码段,位于程序入口位置,计算每个数字的质因数,因为涉及到素数,所以时间复杂度较高,三个耗时操作是嵌套关系
    LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)
  • 上述方法的思路对每个数字做计算,找出质因数,例如找出99的质因数,需要从2开始一次次计算得出
  • 但实际上还有一个更简单的思路:99以内的质数是固定的25个,这25个中,其平方小于99的只有四个,既:2,3,5,7,所以寻找99的质即因数,就在这四个中找即可(漏掉的11,在后面的代码中会特别处理找回来)
  • 基于以上思路,计算质因数的代码就很简单了:
  1. 提前把100000以内的所有素数都找出来,放在名为primes的数组中
  2. 对于任意一个数字N,都用primes中的数字去做除法,能整除的就是N的质因数
  3. 记得像前面的99漏掉了11那样,把11找回来

编码

  • 接下来的代码,在前文的基础上修改
  • 首先增加三个静态变量,注释已详细说明其作用:
    // isPrime[3]=0,表示数字3是素数,isPrime[4]=1,表示数字4不是素数
    private static int[] isPrime = new int[100001];

    // 0-100001之间所有的素数都放入这里
    private static int[] primes = new int[100001];

    // 素数的数量,也就是primes中有效数据的长度
    private static int primeNum = 0;
  • 然后是一个静态代码块,一次性算处100000范围内所有素数,埃式或者欧拉式都可以,这里用了欧拉式
static {

        // 欧拉筛
        for(int i=2;i<=100000;i++) {
            if(isPrime[i]==0) {
                // i是素数,就放入primes数组中
                // 更新primes中素数的数量
                primes[primeNum++] = i;
            }

            for(int j=0;i*primes[j]<=100000;j++) {
                // primes[j]*i的结果是个乘积,这样的数字显然不是素数,所以在isPrimes数组中标注为1
                isPrime[primes[j]*i] = 1;

                // 如果i是primes中某个素数的倍数,就没有必要再计算了,退出算下一个,
                // 例如i=8的时候,其实在之前i=4时就已经计算出8不是素数了
                if(i%primes[j]==0) {
                    break;
                }
            }
        }

        // 经过以上代码,0-100001之间所有素数都放入了primes中
    }
  • 上述代码只会在类加载后执行一次,执行完毕后,1到100000之间的所有素数都计算出来并放入primes中,数量是primeNum,在后面的计算中直接拿来用即可
  • 接下来是最关键的地方了,前面截图中对每个数字计算质因数的代码,可以替换掉了,新的代码如下,可见逻辑已经简化了,从数组primes中取出来做除法即可:
        // 对数组中的每个数,算出所有质因数,构建map
        for (int i=0;i<nums.length;i++) {
            int cur = nums[i];

            // cur的质因数一定是primes中的一个
            for (int j=0;j<primeNum && primes[j]*primes[j]<=cur;j++) {
                if (cur%primes[j]==0) {
                    map.computeIfAbsent(primes[j], key -> new ArrayList<>()).add(i);

                    // 要从cur中将primes[j]有关的倍数全部剔除,才能检查下一个素数
                    while (cur%primes[j]==0) {
                        cur /= primes[j];
                    }
                }
            }

            // 能走到这里依然不等于1,是因为for循环中的primes[j]*primes[j]<<=cur导致了部分素数没有检查到,
            // 例如6,执行了for循环第一轮后,被2除过,cur等于3,此时j=1,那么primes[j]=3,因此 3*3无法小于cur的3,于是退出for循环,
            // 此时cur等于3,应该是个素数,所以nums[i]就能被此时的cur整除,那么此时的cur就是nums[i]的质因数,也应该放入map
            if (cur>1) {
                map.computeIfAbsent(cur, key -> new ArrayList<>()).add(i);
            }
        }
  • 另外,对于之前99取质因数漏掉了11的问题,上述代码也有详细说明:检查整除结果,大于1的就是漏掉的
  • 完整的提交代码如下
package practice;

import java.util.*;

/**
 * @program: leetcode
 * @description:
 * @author: za2599@gmail.com
 * @create: 2022-06-30 22:33
 **/
public class Solution {

    // 并查集的数组, fathers[3]=1的意思是:数字3的父节点是1
//    int[] fathers = new int[100001];
    int[] fathers;

    // 并查集中,每个数字与其子节点的元素数量总和,rootSetSize[5]=10的意思是:数字5与其所有子节点加在一起,一共有10个元素
//    int[] rootSetSize = new int[100001];
    int[] rootSetSize;

    // map的key是质因数,value是以此key作为质因数的数字
    // 例如题目的数组是[4,6,15,35],对应的map就有四个key:2,3,5,7
    // key等于2时,value是[4,6],因为4和6的质因数都有2
    // key等于3时,value是[6,15],因为6和16的质因数都有3
    // key等于5时,value是[15,35],因为15和35的质因数都有5
    // key等于7时,value是[35],因为35的质因数有7
    Map<Integer, List<Integer>> map = new HashMap<>();

    // 用来保存并查集中,最大树的元素数量
    int maxRootSetSize = 1;

    // isPrime[3]=0,表示数字3是素数,isPrime[4]=1,表示数字4不是素数
    private static int[] isPrime = new int[100001];

    // 0-100001之间所有的素数都放入这里
    private static int[] primes = new int[100001];

    // 素数的数量,也就是primes中有效数据的长度
    private static int primeNum = 0;


    static {

        // 欧拉筛
        for(int i=2;i<=100000;i++) {
            if(isPrime[i]==0) {
                // i是素数,就放入primes数组中
                // 更新primes中素数的数量
                primes[primeNum++] = i;
                System.out.println(i + "-" + i*i);
            }

            for(int j=0;i*primes[j]<=100000;j++) {
                // primes[j]*i的结果是个乘积,这样的数字显然不是素数,所以在isPrimes数组中标注为1
                isPrime[primes[j]*i] = 1;

                // 如果i是primes中某个素数的倍数,就没有必要再计算了,退出算下一个,
                // 例如i=8的时候,其实在之前i=4时就已经计算出8不是素数了
                if(i%primes[j]==0) {
                    break;
                }
            }
        }

        // 经过以上代码,0-100001之间所有素数都放入了primes中
    }


    /**
     * 带压缩的并查集查找(即寻找指定数字的根节点)
     * @param i
     */
    private int find(int i) {
        // 如果执向的是自己,那就是根节点了
        if(fathers[i]==i) {
            return i;
        }

        // 用递归的方式寻找,并且将整个路径上所有长辈节点的父节点都改成根节点,
        // 例如1的父节点是2,2的父节点是3,3的父节点是4,4就是根节点,在这次查找后,1的父节点变成了4,2的父节点也变成了4,3的父节点还是4
        fathers[i] = find(fathers[i]);
        return fathers[i];
    }

    /**
     * 并查集合并,合并后,child会成为parent的子节点
     * @param parent
     * @param child
     */
    private void union(int parent, int child) {
        int parentRoot = find(parent);
        int childRoot = find(child);

        // 如果有共同根节点,就提前返回
        if (parentRoot==childRoot) {
            return;
        }

        // child元素根节点是childRoot,现在将childRoot的父节点从它自己改成了parentRoot,
        // 这就相当于child所在的整棵树都拿给parent的根节点做子树了
        fathers[childRoot] = fathers[parentRoot];

        // 合并后,这个树变大了,新增元素的数量等于被合并的字数元素数量
        rootSetSize[parentRoot] += rootSetSize[childRoot];

        // 更像最大数量
        maxRootSetSize = Math.max(maxRootSetSize, rootSetSize[parentRoot]);
    }

    public int largestComponentSize(int[] nums) {
        // 对数组中的每个数,算出所有质因数,构建map
        for (int i=0;i<nums.length;i++) {
            int cur = nums[i];

            // cur的质因数一定是primes中的一个
            for (int j=0;j<primeNum && primes[j]*primes[j]<=cur;j++) {
                if (cur%primes[j]==0) {
                    map.computeIfAbsent(primes[j], key -> new ArrayList<>()).add(i);

                    // 要从cur中将primes[j]有关的倍数全部剔除,才能检查下一个素数
                    while (cur%primes[j]==0) {
                        cur /= primes[j];
                    }
                }
            }

            // 能走到这里依然不等于1,是因为for循环中的primes[j]*primes[j]<<=cur导致了部分素数没有检查到,
            // 例如6,执行了for循环第一轮后,被2除过,cur等于3,此时j=1,那么primes[j]=3,因此 3*3无法小于cur的3,于是退出for循环,
            // 此时cur等于3,应该是个素数,所以nums[i]就能被此时的cur整除,那么此时的cur就是nums[i]的质因数,也应该放入map
            if (cur>1) {
                map.computeIfAbsent(cur, key -> new ArrayList<>()).add(i);
            }
        }
        
        fathers = new int[nums.length];
        rootSetSize = new int[nums.length];

        // 至此,map已经准备好了,接下来是并查集的事情,先要初始化数组
        for(int i=0;i< fathers.length;i++) {
            // 这就表示:数字i的父节点是自己
            fathers[i] = i;
            // 这就表示:数字i加上其下所有子节点的数量等于1(因为每个节点父节点都是自己,所以每个节点都没有子节点)
            rootSetSize[i] = 1;
        }

        // 遍历map
        for (int key : map.keySet()) {
            // 每个key都是一个质因数
            // 每个value都是这个质因数对应的数字
            List<Integer> list = map.get(key);

            int size = list.size();

            // 超过1个元素才有必要合并
            if (size>1) {
                // 取第0个元素作为父节点
                int parent = list.get(0);

                // 将其他节点全部作为地0个元素的子节点
                for(int i=1;i<size;i++) {
                    union(parent, list.get(i));
                }
            }
        }

        return maxRootSetSize;
    }
}
  • 改动完成,提交试试,如下图,左边是前文的成绩,右边是本次优化后的成绩,从122ms优化到96ms,从超51%优化到超91%,优化效果明显

LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)

  • 至此,《LeetCode952三部曲》全部完成,如果您正在刷题,希望此系列能给您一些参考

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...05086920)文章来源地址https://www.toymoban.com/news/detail-693656.html

到了这里,关于LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Go语言基准测试(benchmark)三部曲之三:提高篇

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos -《Go语言基准测试(benchmark)三部曲》已近尾声,经历了《基础篇》和《内存篇》的实战演练,相信您已熟练掌握了基准测试的常规操作以及各种参数的用法,现在可以学习一些进阶版的技能了,在面

    2024年02月06日
    浏览(44)
  • 人脸识别三部曲

    引用文121本 开始运行时,输入待录入的人脸姓名。 按下s键后,开始录入人脸图像,录入两百张后,结束程序。

    2024年02月07日
    浏览(44)
  • 【Docker学习三部曲】——进阶篇

    1️⃣ 什么是 Docker-Compose ? Docker Compose 是 Docker 官方提供的 一个用于定义和运行多个容器的工具 ,它采用了声明式的语法定义单个应用程序的多个容器以及它们之间的相互关系和依赖关系。 使用 Docker Compose ,您可以通过一个配置文件来管理多个 Docker 容器,从而更轻松地部署

    2023年04月25日
    浏览(41)
  • 【Docker学习三部曲】—— 入门篇

    1️⃣ 什么是 docker ? Docker 是 一种运行应用程序的平台,它可以使应用程序在容器中不受环境差异的影响进行部署和运行 。Docker 的流行度越来越高,是因为它可以帮助在不同的开发者和开发团队之间实现代码的共享和协同开发,并且大大 简化了应用程序的部署,提高了可移

    2023年04月20日
    浏览(49)
  • JavaCV人脸识别三部曲之二:训练

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本文是《JavaCV人脸识别三部曲》的第二篇,前文《视频中的人脸保存为图片》咱们借助摄像头为两位群众演员生成大量人脸照片,如下图,群众演员A的照片保存在 E:temp20211218\\001 man ,B的照片保存

    2024年02月11日
    浏览(37)
  • Java版人脸跟踪三部曲之二:开发设计

    如何开发Java版人脸跟踪应用?本篇给出了设计大纲,并解释了相关的重要知识点 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《Java版人脸跟踪三部曲》系列的第二篇,前文体验了人脸跟踪的效果,想要编码实现这样的效果,咱们需要做

    2024年02月12日
    浏览(41)
  • vscode上的git三部曲+git pull操作

    git三部曲:git add .、git commit -m \\\'\\\'、git push,命令在连接远程仓库的本地仓库路径下的终端执行。 vscode上的可视化操作如下:  1、对仓库里的文件做更改,让仓库操作的地方有变化。 2、 点击+号,让文件进入缓存,此步骤相当于终端执行命令git add .  3、在这里输入信息并点击

    2024年02月11日
    浏览(39)
  • Go语言基准测试(benchmark)三部曲之一:基础篇

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos Go的标准库内置的testing框架提供了基准测试(benchmark)功能,可以用来验证本地方法在串行或者并行执行时的基准表现,帮助开发者了解代码的真实性能情况,例如一个方法执行一次的平均耗时,还能

    2024年02月06日
    浏览(50)
  • 【C++系列P4】‘类与对象‘-三部曲——[类](2/3)

     前言 大家好吖,欢迎来到 YY 滴 C++系列 ,热烈欢迎! 【 \\\'类与对象\\\'-三部曲】的大纲主要内容如下 : 如标题所示,本章是【 \\\'类与对象\\\'-三部曲】三章中的第二章节—— 类章节 ,主要内容如下: 目录 一.类 1.类的组成与计算类的大小(含结构体内存对齐规则) 二. 空类的大小

    2024年02月08日
    浏览(40)
  • 数据结构:堆的三部曲(二)top K问题

    top k问题解决的是获取前几个最值的问题。 我们知道 堆的功能主要是选数,选出最大值或者最小值 。那么我们每次获取堆顶元素后,再将剩余元素调整成堆,就可以选出次大的数,如果我们只想要前k个最大值或者最小值,就只需要获取堆顶元素k次,调整k次。比如王者荣耀

    2024年02月02日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包