给出一个正整数,求出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)。
参考资料:微信公众号labuladong