算法 系列 素数

基础数据结构...

Posted by lichao modified on June 20, 2020

素数的定义看起来很简单,如果⼀个数只能被 1 和它本⾝整除,那么这个数就是素数。

返回区间 [2, n) 中有⼏个素数

1
2
3
4
  // 返回区间 [2, n) 中有⼏个素数
  int countPrimes(int n)
  // ⽐如 countPrimes(10) 返回 4
  // 因为 2,3,5,7 是素数

方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  int countPrimes(int n) {
    int count = 0;
    for (int i = 2; i < n; i++)
      if (isPrim(i)) count++;
    return count;
  }
  // 判断整数 n 是否是素数
  boolean isPrime(int n) {
    for (int i = 2; i < n; i++)
      if (n % i == 0)
        // 有其他整除因⼦
        return false;
    return true;
  }

这样写的话时间复杂度 O(n^2),问题很⼤

方法二

修改⼀下上⾯的 isPrim 代码中的 for 循环条件:

1
2
3
4
5
6
  boolean isPrime(int n) {
    for (int i = 2; i * i <= n; i++)
      if (n % i == 0)
          // 有其他整除因⼦
          return false;
  }

如果在 [2,sqrt(n)] 这个区间之内没有发现可整除因⼦,就可以直接断定 n 是素数了,因为在区间 [sqrt(n),n] 也⼀定不会发现可整除因⼦

isPrime 函数的时间复杂度降为 O(sqrt(N))

方法三

⾼效解决这个问题的核⼼思路是和上⾯的常规思路反着来:

  • ⾸先从 2 开始,我们知道 2 是⼀个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8… 都不可能是素数了。
  • 然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12… 也都不可能 是素数了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  int countPrimes(int n) {
    boolean[] isPrim = new boolean[n];
    // 将数组都初始化为 true
    Arrays.fill(isPrim, true);
    for (int i = 2; i < n; i++)
      if (isPrim[i])
        // i 的倍数不可能是素数了
        for (int j = 2 * i; j < n; j += i)
          isPrim[j] = false;
    int count = 0;
    for (int i = 2; i < n; i++)
      if (isPrim[i]) count++;
    return count;
}

方法四

⾸先,回想刚才判断⼀个数是否是素数的 isPrime 函数,由于因⼦的对称性,其中的 for 循环只需要遍历 [2,sqrt(n)] 就够了。这⾥也是类似的,我们外层的 for 循环也只需要遍历到 sqrt(n) :

内层的 for 循环也可以优化,⽐如 n = 25 , i = 4 时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,但是这两个数字已经被 i = 2 和 i = 3 的 2 × 4 和 3 × 4 标记了。我们可以稍微优化⼀下,让 j 从 i 的平⽅开始遍历,⽽不是从 2 * i 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  int countPrimes(int n) {
      boolean[] isPrim = new boolean[n];
      // 将数组都初始化为 true
      Arrays.fill(isPrim, true);
      for (int i = 2; i * i < n; i++)
        if (isPrim[i])
          // i 的倍数不可能是素数了
          for (int j = i * i; j < n; j += i)
            isPrim[j] = false;
      int count = 0;
      for (int i = 2; i < n; i++)
        if (isPrim[i]) count++;
      return count;
  }

该算法的时间复杂度⽐较难算,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:

1
n/2 + n/3 + n/5 + n/7 + ... = n × (1/2 + 1/3 + 1/5 + 1/7...)

括号中是素数的倒数。其最终结果是 O(N * loglogN)