读完本文,你能够去力扣拿下以下题目:java
204.计数质数git
-----------github
素数的定义看起来很简单,若是一个数若是只能被 1 和它自己整除,那么这个数就是素数。算法
不要以为素数的定义简单,恐怕没多少人真的能把素数相关的算法写得高效。好比让你写这样一个函数:数组
// 返回区间 [2, n) 中有几个素数 int countPrimes(int n) // 好比 countPrimes(10) 返回 4 // 由于 2,3,5,7 是素数
你会如何写这个函数?我想你们应该会这样写:函数
int countPrimes(int n) { int count = 0; for (int i = 2; i < n; i++) if (isPrim(i)) count++; return count; } // 判断整数 n 是不是素数 boolean isPrime(int n) { for (int i = 2; i < n; i++) if (n % i == 0) // 有其余整除因子 return false; return true; }
这样写的话时间复杂度 O(n^2),问题很大。首先你用 isPrime 函数来辅助的思路就不够高效;并且就算你要用 isPrime 函数,这样写算法也是存在计算冗余的。优化
先来简单说下若是你要判断一个数是否是素数,应该如何写算法。只需稍微修改一下上面的 isPrim 代码中的 for 循环条件:code
boolean isPrime(int n) { for (int i = 2; i * i <= n; i++) ... }
换句话说,i
不须要遍历到 n
,而只须要到 sqrt(n)
便可。为何呢,咱们举个例子,假设 n = 12
。leetcode
12 = 2 × 6 12 = 3 × 4 12 = sqrt(12) × sqrt(12) 12 = 4 × 3 12 = 6 × 2
能够看到,后两个乘积就是前面两个反过来,反转临界点就在 sqrt(n)
。get
换句话说,若是在 [2,sqrt(n)]
这个区间以内没有发现可整除因子,就能够直接判定 n
是素数了,由于在区间 [sqrt(n),n]
也必定不会发现可整除因子。
如今,isPrime
函数的时间复杂度降为 O(sqrt(N)),可是咱们实现 countPrimes
函数其实并不须要这个函数,以上只是但愿读者明白 sqrt(n)
的含义,由于等会还会用到。
PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。
countPrimes
高效解决这个问题的核心思路是和上面的常规思路反着来:
首先从 2 开始,咱们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不多是素数了。
而后咱们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不多是素数了。
看到这里,你是否有点明白这个排除法的逻辑了呢?先看咱们的初版代码:
int countPrimes(int n) { boolean[] isPrim = new boolean[n]; // 将数组都初始化为 true Arrays.fill(isPrim, true); for (int i = 2; i < n; i++) if (isPrim[i]) // i 的倍数不多是素数了 for (int j = 2 * i; j < n; j += i) isPrim[j] = false; int count = 0; for (int i = 2; i < n; i++) if (isPrim[i]) count++; return count; }
若是上面这段代码你可以理解,那么你已经掌握了总体思路,可是还有两个细微的地方能够优化。
首先,回想刚才判断一个数是不是素数的 isPrime
函数,因为因子的对称性,其中的 for 循环只须要遍历 [2,sqrt(n)]
就够了。这里也是相似的,咱们外层的 for 循环也只须要遍历到 sqrt(n)
:
for (int i = 2; i * i < n; i++) if (isPrim[i]) ...
除此以外,很难注意到内层的 for 循环也能够优化。咱们以前的作法是:
for (int j = 2 * i; j < n; j += i) isPrim[j] = false;
这样能够把 i
的整数倍都标记为 false
,可是仍然存在计算冗余。
好比 n = 25
,i = 4
时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,可是这两个数字已经被 i = 2
和 i = 3
的 2 × 4 和 3 × 4 标记了。
咱们能够稍微优化一下,让 j
从 i
的平方开始遍历,而不是从 2 * i
开始:
for (int j = i * i; j < n; j += i) isPrim[j] = false;
PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。
这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫作 Sieve of Eratosthenes。看下完整的最终代码:
int countPrimes(int n) { boolean[] isPrim = new boolean[n]; Arrays.fill(isPrim, true); for (int i = 2; i * i < n; i++) if (isPrim[i]) for (int j = i * i; j < n; j += i) isPrim[j] = false; int count = 0; for (int i = 2; i < n; i++) if (isPrim[i]) count++; return count; }
该算法的时间复杂度比较难算,显然时间跟这两个嵌套的 for 循环有关,其操做数应该是:
n/2 + n/3 + n/5 + n/7 + ...
= n × (1/2 + 1/3 + 1/5 + 1/7...)
括号中是素数的倒数。其最终结果是 O(N * loglogN),有兴趣的读者能够查一下该算法的时间复杂度证实。
以上就是素数算法相关的所有内容。怎么样,是否是看似简单的问题却有很多细节能够打磨呀?
_____________
个人 在线电子书 有 100 篇原创文章,手把手带刷 200 道力扣题目,建议收藏!对应的 GitHub 算法仓库 已经得到了 70k star,欢迎标星!