java数据结构与算法刷题-----LeetCode204. 计数质数

java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

在这里插入图片描述

  1. 吐槽一下:打表整活的兄弟们,你们知道我们提交后的心情吗?发现前面居然有如此高手写出如此高效的算法,正要好好观摩一下,一看居然是打表操作的时侯的心情吗。(打表:就是看到官方测试用例少,不断提交,此时会提示哪个用例出错,然后针对这个用例直接返回对应答案的操作。)
    在这里插入图片描述

埃氏筛

解题思路:时间复杂度O( n ∗ l o g 2 l o g 2 n n*log_2{log_2n} nlog2log2n),空间复杂度O( n n n)
  1. 一个素数的倍数一定是合数。例如2的倍数4,6,8…都是合数
  2. 如果依次按素数从小到大,将其倍数都标识出来,最后会将所有合数都找到
  3. 最好的是:当我们依照从小到大(从2开始)标识倍数,每次遇到没有被标识的数,就是一个质数。但是前提是你严格按照顺序。
  4. 有了上面的依据,我们就可以规划算法了
  1. 初始创建一个数组isPrim[]用来标识当前数字是否是一个质数。如果是就是true。初始我们认为所有数字都是质数
  2. 从2开始,从小到大,如果当前数字i的isPrim[i] = true,就将倍数都标识出来(false)
  3. 最后统计true的个数就是质数的个数

优化思路

  1. 找比n小的质数。需要从小到大枚举到哪里?

sqrt(n),也就是 n \sqrt{n} n ,用10举例,10的一半是5,5本身是质数,但是2*5就已经是10了,我们找的是比10小的。因此,最多就遍历到 n \sqrt{n} n 就可以规划好所有10以内的数。例如2这个质数可以规划2*2 = 4,2*3=6,2*4=8.3这个质数可以规划3*2=6,3*3 = 9,5这个质数什么都不可以规划了
因此,只需要规划到 n \sqrt{n} n 。我们就找到小于n的所有的合数4,6,8,9

  1. 找质数i的倍数每次都从2开始吗?

2这个质数可以规划2*2 = 4,2*3=6,2*4=8,3这个质数可以规划2*3=6,3*3 = 9.我们发现3*2=6规划了两次,2规划时1次,3规划时又一次
也就是i这个质数,用比自己小的倍数规划,一定是重复操作。因此从自己本身作为倍数是可以防止这个重复的,也就是i*i开始规划

代码

在这里插入图片描述

class Solution {
    public int countPrimes(int n) {
        boolean[] isPrim = new boolean[n];//是否是质数
        Arrays.fill(isPrim, true);//初始默认都是质数
        // 从 2 开始枚举到 sqrt(n)。
        for (int i = 2; i * i < n; i++) {
            // 如果当前是素数
            if (isPrim[i]) {
                // 就把从 i*i 开始,i 的所有倍数都设置为 false。
                //因为比i小的都会被之前的数枚举到,例如枚举2的时候,一定将2*2,2*3,2*4,2*4....2*i都枚举了。
                //因此我们没必枚举2*i,直接从i*i枚举i*i,i*(i+1),i*(i+2)
                for (int j = i * i; j < n; j+=i) {//j = i*i, i*(i+1) = i*i+i = j+i. i*(i+2) = i*i+2i = j+i+i
                    isPrim[j] = false;//合数设置为false
                }
            }
        }

        // 计数,统计质数的个数
        int cnt = 0;
        for (int i = 2; i < n; i++) {
            if (isPrim[i]) {
                cnt++;
            }
        }
        return cnt;
    }
}

线性筛

埃氏筛的问题在于,虽然我们从i*i这个倍数开始规划合数可以避免比i小的倍数的重复操作。但是比i大的倍数还是会重复。例如3*15=45,5*9 = 45

而线性筛之所以叫线性筛,是因为它的时间复杂度是线性的 O ( n ) O(n) O(n).也就是它所有元素只会访问一次。例如45只会访问一次。

利用性质:从左到右 ( 2 , + ∞ ) (2,+\infin) 2+依次,遇到质数x就用<=它的所有质数相乘,加上所有合数 乘 比它小的所有质数,所得到列表。就是质数合数列表

  1. 两个质数相乘必然为质数
  2. 一个质数乘以一个合数必然为合数。两个合数相乘也必然为合数。每个合数都可以通过唯一的一个最小质因子得出。(也就是说,如果一个合数仅仅是通过其最小质因子标识的,那么它只会被访问一次,而不会重复访问。

从小到大依次用质数标记一个数x时,每个合数的最小质因数都可以最先标记它。也就是一个合数x,从小到大依次取余比它小的质数prime,第一次为0时,prime就是它的最小质因子

解题思路:时间复杂度O( n n n),空间复杂度O( n n n)
  1. 多维护一个数组,保存依次找到的素数
  2. 不再当x为质数时进行标记,而是对每个整数x都进行标记。
  3. x不再通过倍数进行标记,而是通过质数集合中的数与其相乘进行标记。
  4. 每个合数x要遵循被自己的最小质因子标记。这就是优化的关键。

优化思路:如果文字不好理解可以参考这个视频https://www.bilibili.com/video/BV1LR4y1Z7pm

  1. 如果当前x是合数的话,它会优先被最小质因子prime整除,也就是 x % p r i m e = = 0 x \% prime == 0 x%prime==0.此时需要立即终止对x的继续标记。否则会破坏每个合数被最小质因子标识的规则,就会发生重复访问。

因为合数乘质数一定 = 合数。当x找到其最小质因子prime后,标识x*prime,此时依然符合每个合数被最小质因子标识。但如果继续用更大的prime对x进行标识,就会破坏这个规则。

  1. 例如x = 4时,我们一定已经统计了2和3这两个素数
  1. 先将2*4 = 8标记为合数。此时就立即停止,因为如果我们继续将3*4 = 12标记为合数的话,12的最小质因子是2,此时它居然通过了3这个质数进行标识。这样就破坏了规则
  2. 当x = 6时,6是合数。我们一定统计了2,3,5这3个素数。我们标识2*6=12时,就会发现,它被重复标记了。此时如果继续标记6*3 = 18和6*5 = 30
  3. 用x = 15时举例,15是合数。我们标识2*15 = 30此时发现又和6*5 = 30重复了,因为5不是30的最小质因子,而2才是30的最小质因子
  1. 因此,只要x是合数,就只从最小质数2,一直乘到x的最小质因子。

例如15这个合数,只标记15*2 = 30和15*3 = 45。因为15的最小质因子是3.这样30和45都保证了被最小质因子所标识的规则,30的最小质因子是2,45的最小质因子是3. 而如果我们继续标识15*5 = 75,而75的最小质因子是3.就会在通过和数25标识25*3 = 75时重复标记

代码:因为取余操作比较慢,而且leetcode的判题规则并不完全按照时间复杂度来算,因此这个的做题效率反而不如埃氏筛。但是如果n足够大,线性筛的效率也会越来越优于埃氏筛。但是埃氏筛更通用一点

在这里插入图片描述

class Solution {
    public int countPrimes(int n) {
        int primes[] = new int[n/2];//保存质数
        int length = 0;
        int[] isPrime = new int[n];
//        Arrays.fill(isPrime, 1);//如果 规定 isPrime[i] == 0为质数,就可以省略这个操作。
        for (int i = 2; i < n; ++i) {
            if (isPrime[i] == 0) {//如果当前i是质数
                primes[length++] = i;//将其记录到质数数组中
            }
            for (int j = 0; j < length && i * primes[j] < n; ++j) {
                isPrime[i * primes[j]] = 1;//标记合数,两个质数相乘 或者 一个质数*一个合数 都为合数
                if (i % primes[j] == 0) {//如果这个条件成立,说明当前质数primes[j]是合数i的最小质因子,
                    break;//立即终止,否则会破坏每个合数都是通过最小质因子发现的规则
                }
            }
        }
        return length;
    }
}

质数性质+奇偶性质+埃氏筛(卷!卷!卷卷卷卷卷卷!)

因为兄弟们越来越卷,搞的上面两种算法的效率都被压下去了,因此我们针对两者的优点和缺点进行优化

  1. 埃氏筛的缺点是具有重复操作合数的问题,例如10这个合数,会被2和5两个质数所处理
  2. 线性筛虽然解决了重复操作的问题,但是每次都需要取余操作判断是否是合数,这个就慢了
解题思路:时间复杂度O( n ∗ l o g 2 l o g 2 n n*log_2{log_2n} nlog2log2n),空间复杂度O( n n n)

理论时间复杂度和经典的埃氏筛一样,但是实际时间复杂度达到了O(n),而且是没有取余操作的O(n).因为这个算法将重复操作都直接跳过了

  1. 使用埃氏筛的基础上,利用如下性质
  1. 质数中,只有一个偶数就是2,其它都是奇数.因此对于小于n的所有质数,最多只有 n 2 \dfrac{n}{2} 2n,也就是最多是n的一半,因为除了2以外的所有偶数都不是质数,直接排除即可。
  2. 每个质数将它的倍数都剔除之后,下一个遇到的奇数一定是质数。

这个很关键,我们顺序遍历数字时,通过3这个质数,会将3*3 = 9这个数标为合数,因此下一个奇数就是5,一定是质数,5*5 = 25也标为合数,然后是7一定是质数,7*7 = 49也是合数。下一个奇数是9我们已经表示为合数,下一个是11一定是质数。

  1. 一个质数 乘 一个大于1的奇数,必然为合数。

例如3是质数,则大于1的奇数为3,5,7,9…因此3*33*53*73*9…都是合数

  1. 因此我们可以将找素数变成找合数。初始因为除了2以外的偶数都是合数,因此我们将剩下的奇数和2都看作是质数。因此初始我们有count = n/2个质数。而我们的dp数组也由保存是否是质数,变成是否是合数。true表示当前i是合数。所以初始dp数组全部是false,表示都是质数
  2. 接下来,我们要跳过所有偶数,因为2以外的偶数都是合数,只遍历奇数。也就是从3开始遍历,每次+2.也就是3,5,7,9,11…
  3. 然后根据性质2:每个质数将它的倍数(合数)都剔除之后,下一个遇到的奇数一定是质数。

我们每访问一个奇数都要查看它是否是被剔除后的数(合数),如果是就跳过。这样下一个没有被剔除的奇数一定是下一个质数

  1. 当拿到质数后,我们要通过性质3:一个质数 乘 一个大于1的奇数,必然为合数,将所有合数标识出来(true),并且每次都count-1.因为我们初始有count个质数(抛弃所有>2的偶数),现在又找到了新的合数(且是奇数),那么count就要-1.

这里标识的true就是第4步需要用的,跳过被剔除的合数

代码:前面效率比这个算法高的,都是打表整活的(根本不能算作算法),还有几个是以前测试用例少的时候提交的代码,他们的代码放到现在提交都到170ms开外了

在这里插入图片描述

class Solution {
    public int countPrimes(int n) {
        //dp数组,当前数(下标)是否是合数,true表示为合数,默认都是质数(false)
        boolean notPrime[] = new boolean[n];
		//如果n小于3的话就没有  因为找到n以内  所以2以内是没有的
		if (n < 3) return 0;
        
		//除了2以外的所有的偶数都不是素数,所有我们可以剔除一半,最多情况下,小于n的所有奇数都是质数
		int count = n / 2;
		
		// 之后我们只要剔除 在这个奇数范围内 不是素数的数就可以了
		// 因为我们已经把偶数去掉了,所以只要剔除奇数的奇数倍就可以了
		for (int i = 3; i * i < n; i += 2) {//第一个奇数是3,i+=2正好是下一个奇数
			//如果i已经标为合数就跳过,避免重复遍历
			if (notPrime[i]) continue;
            //否则i必然是质数,因为一个质数和一个大于1的奇数相乘必然为合数。
            //为了避免冗余,小于i的奇数会被前面的质数处理,因此我们从其本身这个奇数开始乘,也就是j = i*i。例如j = 3*3 = 9
            //之后的奇数都是不断的进行+2操作,例如3+2 = 5,5+2 = 7 实现i*5,i*7,i*9,i*11. 
            //也就是i*(i+2) = i * i + i+2. 而j = i*i,因此接下来不断让 j + i*2 ,
            //也就是i = 3为质数, 让其不断乘以大于等于i的奇数。例如j = 3*3 = 9, j = 3*5 =  15, j = 3*7 = 21, j = 3 * 9 = 27,这些都是合数
			for (int j = i * i; j < n; j += i * 2) {
				if (!notPrime[j]) {//如果当前这个合数还没有访问过
					notPrime[j] = true;//将其标志为合数
					count --;//则质数个数-1
				}
			}
		}
		return count;
    }
}
  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值