素数测试的好处是你只除以素数。
private static boolean checkMod( int num) {
for (int i : primes){
if( num % i == 0){
return false;
}
}
return true;
}
不好的是你除以 所有 到目前为止找到的素数,即所有小于候选的素数。这意味着对于低于 100 万的最大素数 999983,您除以 78497 个素数以发现这个数字是素数。这是很多工作。事实上,当达到 100 万时,在这个算法中花费在素数上的工作占所有工作的 99.9% 左右,在更高的限制中占比更大。而且该算法几乎是二次的,要以这种方式找到n 的素数,您需要执行约
n² / (2*(log n)²)
部门。
一个简单的改进是提前停止除法。令n 为合数(即大于1 且除数除1 和n 之外的数),令d 为n 的除数。
现在,d 是 n 的除数意味着 n/d 是一个整数,也是 n 的除数:n/(n/d) = d。
所以我们可以很自然地将n的除数分组,每个除数d产生一对(d, n/d)。
对于这样的一对,有两种可能:
-
d = n/d,表示n = d²,或d = √n。
- 两者不同,然后其中一个小于另一个,例如
d < n/d。但这会立即转化为d² < n 或d < √n。
因此,无论哪种方式,每对除数包含(至少)一个不超过√n,因此,如果n 是一个合数,它的最小除数(1 以外)不超过@ 987654347@.
所以当我们到达√n时我们可以停止审判部门:
private static boolean checkMod( int num) {
for (int i : primes){
if (i*i > n){
// We have not found a divisor less than √n, so it's a prime
return true;
}
if( num % i == 0){
return false;
}
}
return true;
}
注意:这取决于按升序迭代的素数列表。如果语言不能保证这一点,您必须使用不同的方法,通过ArrayList 或类似的东西按索引迭代。
停止在候选的平方根处进行除法,对于 100 万以下的最大素数 999983,我们现在只需将其除以 1000 以下的 168 个素数。这比以前少了很多工作。在平方根处停止试除法,并且只除以素数,这与试除法一样好,并且需要大约
2*n^1.5 / (3*(log n)²)
对于n = 1000000 的划分,大约是 750 倍,还不错吧?
但这仍然不是很有效,找到n 以下所有素数的最有效方法是筛子。易于实现的是经典的Sieve of Eratosthenes。在 O(n*log log n) 操作中找到低于n 的素数,通过一些增强(预先排除多个小素数的倍数),其复杂性可以降低到 O(n) 操作。一个相对较新的具有更好渐近行为的筛子是Sieve of Atkin,它在 O(n) 操作中找到n 的素数,或者通过消除一些小素数的倍数的增强,在 O(n/log log n ) 操作。
阿特金筛子的实现更复杂,因此埃拉托色尼筛子的良好实现可能比阿特金筛子的简单实现表现更好。对于类似优化级别的实现,性能差异很小,除非限制变大(大于 1010;而且在实践中,埃拉托色尼筛比阿特金筛更好的扩展性并不罕见那是因为更好的内存访问模式)。因此,我建议从 Eratosthenes 筛开始,并且仅当尽管诚实地优化优化但其性能仍不能令人满意时,才深入研究 Atkin 筛。或者,如果您不想自己实现它,请找到其他人已经认真调整过的良好实现。
我在an answer 中进行了更详细的设置,但设置略有不同,问题在于找到第 n 个素数。一些或多或少有效方法的实现与该答案相关联,特别是埃拉托色尼筛的一两个可用(尽管没有多少优化)实现。