【LeetCode】统计N以内的素数(质数)

题述

给定一个值n,统计2~n以内,素数的个数
说明:
素数,即质数。除0、1以外,只能被1和自身整除的自然数,即为素数。
例:
输入:100
输出:25

解决办法

方式一:(暴力解法)

思路

直接从2开始遍历,判断是否能被2到自身之间的数整除

代码示例

public class CountPrime {
    
    

    public static void main(String[] args) {
    
    
        System.out.println(countPrime(100));
    }

    private static int countPrime(int n) {
    
    
        int count = 0;
        for (int i = 2; i < n; i++) {
    
    
            if (isPrime(i)) {
    
    
                count++;
            }
        }

        return count;
    }

    /**
     * 判断输入的数是否为质数
     *
     * @param x 输入的数
     * @return true:是;false:否
     */
    private static boolean isPrime(int x) {
    
    

        for (int i = 2; i < x; i++) {
    
    
            if (x % i == 0) {
    
    
                return false;
            }
        }
        return true;
    }
}

改进思路

上面的暴力解法虽然可以用,但其实还是存在可以优化的地方的。为啥,因为【乘法结合律】的存在。

2 * 50=100; 50 * 2 = 100;
4 * 25=100; 25 * 4 = 100;
以此类推。 如果 a能被x整除得到b,那么b也能被x整除得到a

所以,可以这么说,因为【乘法结合律】,所以我们在计算数能否被某个数整除的时候,可以不用完全遍历,只需要遍历到根号x就可以,如下图所示:(但是根号x不容易表示,所以换一个方式,i * i <= x)

    private static boolean isPrime(int x) {
    
    

        for (int i = 2; i * i <= x; i++) {
    
    
            if (x % i == 0) {
    
    
                return false;
            }
        }
        return true;
    }

方式二:(埃氏筛选法)

思路

利用合数的概念(非素数),素数*m必然是合数,因此可以从2开始遍历,将所有的合数做上标记,用空间换时间。什么意思呢?
比如,3是素数,那么肯定有如下规律:

3 * 2 = 6; 合数
3 * 3 = 9; 合数
3 * 4 = 12; 合数
3 * 5 = 15; 合数
以此类推,3 * m必然是合数,一直到 3 * m < n 。将这些提前给标记上

代码示例

public class CountPrime {
    
    

    public static void main(String[] args) {
    
    
        System.out.println(eratosthenes(100));
    }

    /**
     * 判断输入的数是否为质数
     *
     * @param x 输入的数
     * @return true:是;false:否
     */
    private static int eratosthenes(int x) {
    
    
        boolean[] isPrime = new boolean[x];
        int count = 0;
        for (int i = 2; i < x; i++) {
    
    
            if (!isPrime[i]) {
    
    
                count++;

                // 标记合数
                for (int j = 2 * i; j < x; j += i) {
    
    
                    isPrime[j] = true;
                }
            }
        }
        return count;
    }
}

上面的标记合数循环,大家可能一时间反应不过来。j+=i的原理是【加法分配律】。3 * i = (2 + 1) * i = 2 * i + 1 * i。所以,为了累加i的系数m,每次+i就可以了。

改进思路

上面的算法还可以继续改进,跟方式一的改进思路差不多。在内部循环标记合数的时候,存在重复标记情况。比如:(假定输入的n=100)

  1. i=2时,会标记2 * k (k>=2, 2 * k < x) ,即 2 * 2,2 * 3,2 * 4,··· 2 * 50,起码已经标记了50 - 2 + 1个元素了;
  2. i=3时,会标记3 * k (k>=2, 3 * k < x) ,即 3 * 2,3 * 3,3 * 4,··· 3 * 33,起码已经标记了33 - 2 + 1个元素了;
  3. i=4时,会标记4 * k (k>=2, 4 * k < x) ,即 4 * 2,4 * 3,4 * 4,··· 4 * 25,起码已经标记了25 - 2 + 1个元素了。
  4. i=5时,会标记5 * k (k>=2, 5 * k < x),事实上,5 * 3早在前面就被标记过了;5 * 4 = 20,则早在i =2时标记过了

显然,如果我们按部就班的标记,肯定会存在交叉、冗余标记。
从上面的描述我们可以总结出一个规律,当i > 2 (2 * i) ~ (i-1) * i的部分肯定已经被标记过了。所以,修改内部循环算法如下:

        private static int eratosthenes(int x) {
    
    
        long start = System.currentTimeMillis();

        // 默认全部标记为素数
        boolean[] isNotPrime = new boolean[x];
        int count = 0;

        outer:
        for (int i = 2; i < x; i++) {
    
    
            if (!isNotPrime[i]) {
    
    
                count++;

                // 标记合数
                for (int j = i * i; j > 0 && j < x; j += i) {
    
    
                    isNotPrime[j] = true;
                }
            }
        }

        System.out.println("耗时:" + (System.currentTimeMillis()-start));
        return count;
    }

注意:当i比较大的时候,可能会出现i * i > Integer.MAX_VALUE的情况,所以需要判断j > 0才能继续操作

猜你喜欢

转载自blog.csdn.net/qq_32681589/article/details/132059584
今日推荐