Nameless Site

But one day, you will stand before its decrepit gate,without really knowing why.

0%

计数质数

来自Leetcode第204题计数质数

统计所有小于非负整数 n 的质数的数量。

示例:

1
2
3
输入: 10
输出: 4
解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

筛选

来自labuladong

首先从 2 开始,我们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8… 都不可能是素数了。

然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12… 也都不可能是素数了。

看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

1.gif

首先,回想刚才判断一个数是否是素数的 isPrime 函数,由于因子的对称性,其中的 for 循环只需要遍历 [2,sqrt(n)] 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 sqrt(n)

1
2
3
for (int i = 2; i * i < n; i++) 
if (isPrim[i])
...

除此之外,很难注意到内层的 for 循环也可以优化。我们之前的做法是:

1
2
for (int j = 2 * i; j < n; j += i) 
isPrim[j] = false;

这样可以把 i 的整数倍都标记为 false,但是仍然存在计算冗余。

比如 n = 25i = 4 时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,但是这两个数字已经被 i = 2i = 3 的 2 × 4 和 3 × 4 标记了。

我们可以稍微优化一下,让 ji 的平方开始遍历,而不是从 2 * i 开始:

1
2
for (int j = i * i; j < n; j += i) 
isPrim[j] = false;

这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}