题目描述
给出一个正整数,求出2-正整数之间的所有素数。所谓素数,就是除了1和它本身外不能被任何数整除的数。
素数求解的问题是刚开始接触C语言就接触到的简单问题,也许你会写出下面的代码:
int Prime_num(int end_num) // 求解从1-end_num间的所有素数
{
int result = 0;
for(int i = 2; i < end_num; ++i){
if(IsPrime(i)){
++result;
}
}
return result;
}
bool IsPrime(int num) // 判断num是否为素数
{
int i;
for(i = 2; i < num; ++i){
if(!(num % i)){
return false;
}
}
return true;
}
该代码套用两层循环,从2遍历至end_num,对每一个数进行素数判断。时间复杂度O(n^2)。
但是我们发现该算法在判断num是否为素数还有可优化的地方
比如当num = 12 时判断num是否为素数:
12 = 2 * 6
12 = 3 * 4
12 = sqrt(12) * sqrt(12)
12 = 4 * 3
12 = 6 * 2
观察到以sqrt(12)为分界点,前后是相同的乘积式,这样我们只需要在IsPrime()函数中遍历到sqrt(num)就好了呢。
好了,改进一下IsPrime()函数
bool IsPrime(int num)
{
int i;
for(i = 2; i <= sqrt(num); ++i){ //只需要遍历到 sqrt(num)就可以了哦
if(!(num % i)){
return false;
}
}
return true;
}
但是这还不是最高效的算法!
在之前的素数求解过程中,我们从2开始遍历,2为素数,并且 2 * 2 = 4, 2 * 3 = 6, 2 * 4 = 8…都不会是素数了
接下来的3也是素数,3 * 2 = 6, 3 * 3 = 9, 3 * 4 = 12…也都不是素数
也就是说,只要我们在遍历过程中将该数的所有倍数排除掉,那么就会节省很大一部分的时间
看下代码:
int Prime_num(int end_num)
{
int result = 0;
bool IsPrime[end_num];
// 将所有元素初始化为true
for(int i = 0; i < end_num; ++i){
IsPrime[i] = true;
}
//遍历
for(int i = 2; i < end_num; ++i){
if(IsPrime[i]){
// 将 i 的倍数全部排除掉
for(int j = i * 2; j < end_num; j += i){
IsPrime[j] = false; // 非素数
}
++result;
cout << i << endl;
}
}
return result;
}
这里有一个细节需要说明,因为我们使用的是逆向思维。
在数组IsPrime中,我们将每一个元素都初始化为true,即假设每一个元素都是素数。遍历每一个元素,并排除掉该元素的所有倍数(保留该元素本身),这样最终还是true的元素就是素数啦。
而第一个素数是 2 ,所以我们从2 开始遍历。
将 2 的所有倍数全部排除掉,用 IsPrime[i] = false来标记,4,6,8,10,…,100(不能排除2本身哦,因为2是素数)
接下来我们将 3 的所有倍数都排除掉,用IsPrime[i] = false来标记,6,9,12,…,99(同样3本身不能被排除)
下一个数字是 4 ,这个时候我们需要对 4 的倍数进行排除吗?想清楚哦,由于 4 是 2 的倍数,所以 4 在 遍历到 2 的时候就已经被排除掉啦,所以现在IsPrime[4] == false,不执行排除操作(这里可能会产生疑问,不排除4的倍数,会不会有非素数数字被漏掉没有排除呢?不会啦,因为4是2的倍数,是4的倍数的数字也一定是2的倍数,在遍历2时就已经被排除光光啦)
再下一个数字是 5 ,这个时候我们需要对 5 的倍数进行排除吗?答案是肯定的,因为5就是我们遇到的下一个素数(IsPrime[5] == true)。也就是说从除了1 和 5之外,5这个数不能被2,3,4整除(因为我们在遍历2,3,4的时候都没有把5排除掉),这不正好满足素数的定义吗?所以IsPrime[5] == true,5是我们要找的下一个素数i,将5的所有倍数全部排除掉。
…
这样看来,是不是我们的算法就已经优化了很多啦
但仔细看得话这里还有两个可以优化的点
for(int i = 2; i < end_num; ++i){
if(IsPrime[i]){
....
还记得上面我们的sqrt(end_num)吗?由于因子具有对称性,因此这里我们也可以将这层循环缩减为sqrt(end_num)。
还有一个地方
for(int j = i * 2; j < end_num; j += i){
IsPrime[j] = false; // 非素数
}
就是我们的排除过程
在遍历到2时,我们要排除的是 2 * 2 = 4,… ,2 * 7 = 14,…,2 * 14 = 28,…,2 * 21 = 42…
在遍历到3时,我们要排除的是 3 * 2 = 6, 3 * 3 = 9,…3 * 7 = 21 …
在遍历到5时,我们要排除的是 5 * 2 = 10,…,5 * 7 = 35…
在遍历到7时,我们要排除的是 7 * 2 = 14, 7 * 3 = 21, 7 * 4 = 28, 7 * 5 = 35, 7 * 6 = 42, 7 * 7 = 49,…
发现了吗?在遍历到7的时候,我们在 7 * 7之前的表达式都已经在之前的过程中被刷掉了呢。
所以在该层循环我们从 j = i * i的位置开始就可以节省不少时间
下面为最终优化后的代码
int Prime_num(int end_num)
{
int result = 0;
bool IsPrime[end_num];
// 将所有元素初始化为true
for(int i = 0; i < end_num; ++i){
IsPrime[i] = true;
}
//遍历
for(int i = 2; i*i < end_num; ++i){
if(IsPrime[i]){
// 将 i 的倍数全部排除掉
for(int j = i * i; j < end_num; j += i){
IsPrime[j] = false; // 非素数
}
}
}
for(int i = 2; i < end_num; ++i){
if(IsPrime[i]){
++result;
}
}
return result;
}
该算法的时间复杂度比较难算,最终结果是O(N*loglogN)。