1. 简介
素数(质数)定义为:在大于1的自然数中,除了1和它本身外不再有其他因数的自然数,即在区间[2, n-1]
范围内没有因数。本文将介绍几种常见的判断某个数是否为素数(质数)的方法以及寻找在某个范围内的素数个数。
2. 判断素数-枚举
枚举的思路是定义变量i从2开始遍历,如果该数能被i整除,则该数不是素数。程序如下:
bool isPrime(int x) {
for (int i = 2; i < x; ++i) {
if (x % i == 0) return false;
}
return true;
}
上述循环的遍历区间是[2, n-1]
,我们可以尝试对上述循环优化。考虑到如果y是x的因数,那么x/y也是x的因数,因此我们只需要判断y或者x/y即可。而如果我们每次仅判断二者中较小的那个,则较小数一定落在区间[2, sqrt(x)]
内。因此,我们可以将遍历区间从[2, n-1]
优化到[2, sqrt(x)]
。程序如下:
bool isPrime(int x) {
if (x <= 1) return false;
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) return false;
}
return true;
}
我们可以尝试继续优化:实际上,素数有一个特点即它总是等于6x-1或6x+1,其中x是大于等于1的自然数。证明如下:6x不为素数,6x+2不为素数,6x+3不为素数,6x+4不为素数。那么素数只可能出现在6x+1和6x-1位置处。所以我们可以将循环步长设定为6,然后每次仅判断6x+1和6x-1位置处的数即可。程序如下:
bool isPrime(int x) {
// 判断小于5的数字
if (x <= 3) return x > 1;
// 如果不是6x-1或6x+1,则肯定不是素数
if (x % 6 != 1 && x % 6 != 5) return false;
for (int i = 5; i * i <= x; i += 6) {
// i表示6x-1的位置,i+2表示6x+1的位置
if (x % i == 0 || x % (i + 2) == 0) return false;
}
return true;
}
3. 计数素数-埃式筛
计数素数的要求找到在给定区间内的素数个数。如输入为n = 5
,则返回结果为3
(2和3两个素数、小于n)。
直观上,我们可以直接从2到n-1遍历,再使用上一节中判断素数的方法来判断每一个元素。但其实存在更为简便的方法,利用数与数之间的相关性,Eratosthenes提出埃式筛方法。埃式筛的思路是:如果x是素数,那么大于x的x的倍数2x,3x,…一定不是素数,因此我们可以将这些数做上标记以避免重复判断。具体做法是:首先我们定义一个数组isPrime
,大小为n。在遍历的过程中通过对该数组赋值以标识当前元素是否为素数。结合上述思路,程序如下:
int countPrimes(int n) {
vector<int> isPrime(n, 1);
int cnt = 0;
for (int i = 2; i < n; ++i) {
if (isPrime[i]) {
++cnt;
// 合数标记
for (int j = 2 * i; j < n; j += i)
isPrime[j] = 0;
}
}
return cnt;
}
在上述程序中,其实存在大量的重复标记。考虑输入为10,在i
为2时4和8被标记;在i
为4时8又被标记。当输入数字很大时,重复标记的操作会明显增多。实际上,在我们遍历到i
时,i * i
前面的数据肯定已经被标记过。考虑输入为20,在i
为3时即i * i
等于9时,前面的数据已经被全部标记(4被2标记,6被2标记、8被2标记)、在i
为5时即i * i
等于25,此时已经完成全部标记。优化后的程序如下:
int countPrimes(int n) {
vector<int> isPrime(n, 1);
int cnt = 0;
for (int i = 2; i < n; ++i) {
if (isPrime[i]) {
++cnt;
// 合数标记
if ((long long)i * i < n)
for (int j = i * i; j < n; j += i)
isPrime[j] = 0;
}
}
return cnt;
}
4. 计数素数-线性筛
上述优化后的埃式筛其实还是存在重复标记合数的操作,而线性筛的优化目标是仅标记一次合数。其具体做法是多维护一个数组primes
用于存放当前得到的素数集合。与埃式筛中仅当前数为素数时才进行倍数标记不同的是,线性筛将素数集合中的数与当前数相乘(不管其是否为素数),且在出现整除时结束标记。
直观上,如果当前数x
可以被primes[i]
整数,那么对于合数y = x·primes[i + 1]
而言,它一定在后面遍历到x / primes[i]·primes[i + 1]
这个数时会被标记。这就保证了每个合数只会被其最小的质因数标记,即每个合数仅会被标记一次。程序如下:
int countPrimes(int n) {
vector<int> primes;
vector<int> isPrime(n, 1);
for (int i = 2; i < n; ++i) {
// 将素数加入素数集合中
if (isPrime[i]) primes.push_back(i);
for (int j = 0; j < primes.size() && i * primes[j] < n; ++j ){
// 基于素数集合开始标记
isPrime[i * primes[j]] = 0;
// 终止标记
if (i % primes[j] == 0) break;
}
}
return primes.size();
}
5. 素数总结
本文总结了判断素数以及计数素数的几种经典解法,其他方法请参考参考部分。
参考
- https://leetcode-cn.com/problems/count-primes/.