高效求解素数

给出一个正整数,求出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

 

猜你喜欢

转载自www.cnblogs.com/GuoYuying/p/11674824.html